|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.DocumentHighlighting;
using Microsoft.CodeAnalysis.Editor.Host;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.FindSymbols.Finders;
using Microsoft.CodeAnalysis.FindUsages;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Shell.FindAllReferences;
using Microsoft.VisualStudio.Shell.TableControl;
using Microsoft.VisualStudio.Shell.TableManager;
using Roslyn.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.FindUsages
{
internal partial class StreamingFindUsagesPresenter
{
private abstract class AbstractTableDataSourceFindUsagesContext :
FindUsagesContext, ITableDataSource, ITableEntriesSnapshotFactory
{
/// <summary>
/// Cancellation token we own that we will trigger if the presenter for this particular
/// search is either closed, or repurposed to show results from another search. Clients
/// using the <see cref="IStreamingFindUsagesPresenter"/> should use this token if they
/// are populating the presenter in a fire-and-forget manner. In other words if they kick
/// off work to compute the results that they themselves are not waiting on. If they are
/// *not* kickign off work in a fire-and-forget manner, and are instead populating the
/// presenter on their own thread, they should have their own cancellation token (for example
/// backed by a threaded-wait-dialog or CommandExecutionContext) that controls their scenario
/// which a client can use to cancel that work.
/// </summary>
/// <remarks>
/// Importantly, no code in this context or the presenter should actually examine this token
/// to see if their work is cancelled. Instead, any cancellable work should have a cancellation
/// token passed in from the caller that should be used instead.
/// </remarks>
public readonly CancellationTokenSource CancellationTokenSource = new();
private ITableDataSink _tableDataSink;
public readonly StreamingFindUsagesPresenter Presenter;
private readonly IFindAllReferencesWindow _findReferencesWindow;
private readonly IGlobalOptionService _globalOptions;
protected readonly IThreadingContext ThreadingContext;
protected readonly IWpfTableControl2 TableControl;
private readonly AsyncBatchingWorkQueue<(int current, int maximum)> _progressQueue;
protected readonly object Gate = new();
#region Fields that should be locked by _gate
/// <summary>
/// If we've been cleared or not. If we're cleared we'll just return an empty
/// list of results whenever queried for the current snapshot.
/// </summary>
private bool _cleared;
/// <summary>
/// Message we show if we find no definitions. Consumers of the streaming presenter can set their own title.
/// </summary>
protected string NoDefinitionsFoundMessage = ServicesVSResources.Search_found_no_results;
/// <summary>
/// The list of all definitions we've heard about. This may be a superset of the
/// keys in <see cref="_definitionToBucket"/> because we may encounter definitions
/// we don't create definition buckets for. For example, if the definition asks
/// us to not display it if it has no references, and we don't run into any
/// references for it (common with implicitly declared symbols).
/// </summary>
protected readonly List<DefinitionItem> Definitions = new();
/// <summary>
/// We will hear about the same definition over and over again. i.e. for each reference
/// to a definition, we will be told about the same definition. However, we only want to
/// create a single actual <see cref="DefinitionBucket"/> for the definition. To accomplish
/// this we keep a map from the definition to the task that we're using to create the
/// bucket for it. The first time we hear about a definition we'll make a single task
/// and then always return that for all future references found.
/// </summary>
private readonly Dictionary<DefinitionItem, RoslynDefinitionBucket> _definitionToBucket =
new();
/// <summary>
/// We want to hide declarations of a symbol if the user is grouping by definition.
/// With such grouping on, having both the definition group and the declaration item
/// is just redundant. To make life easier we keep around two groups of entries.
/// One group for when we are grouping by definition, and one when we're not.
/// </summary>
private bool _currentlyGroupingByDefinition;
protected ImmutableList<Entry> EntriesWhenNotGroupingByDefinition = ImmutableList<Entry>.Empty;
protected ImmutableList<Entry> EntriesWhenGroupingByDefinition = ImmutableList<Entry>.Empty;
private TableEntriesSnapshot? _lastSnapshot;
public int CurrentVersionNumber { get; protected set; }
#endregion
protected AbstractTableDataSourceFindUsagesContext(
StreamingFindUsagesPresenter presenter,
IFindAllReferencesWindow findReferencesWindow,
ImmutableArray<ITableColumnDefinition> customColumns,
IGlobalOptionService globalOptions,
bool includeContainingTypeAndMemberColumns,
bool includeKindColumn,
IThreadingContext threadingContext)
{
presenter.AssertIsForeground();
Presenter = presenter;
_findReferencesWindow = findReferencesWindow;
_globalOptions = globalOptions;
ThreadingContext = threadingContext;
TableControl = (IWpfTableControl2)findReferencesWindow.TableControl;
TableControl.GroupingsChanged += OnTableControlGroupingsChanged;
// If the window is closed, cancel any work we're doing.
_findReferencesWindow.Closed += OnFindReferencesWindowClosed;
DetermineCurrentGroupingByDefinitionState();
Debug.Assert(_findReferencesWindow.Manager.Sources.Count == 0);
// Add ourselves as the source of results for the window.
// Additionally, add applicable custom columns to display custom reference information
_findReferencesWindow.Manager.AddSource(
this,
SelectCustomColumnsToInclude(customColumns, includeContainingTypeAndMemberColumns, includeKindColumn));
// After adding us as the source, the manager should immediately call into us to
// tell us what the data sink is.
RoslynDebug.Assert(_tableDataSink != null);
// https://devdiv.visualstudio.com/web/wi.aspx?pcguid=011b8bdf-6d56-4f87-be0d-0092136884d9&id=359162
// VS actually responds to each SetProgess call by queuing a UI task to do the
// progress bar update. This can made FindReferences feel extremely slow when
// thousands of SetProgress calls are made.
//
// To ensure a reasonable experience, we instead add the progress into a queue and
// only update the UI a few times a second so as to not overload it.
_progressQueue = new AsyncBatchingWorkQueue<(int current, int maximum)>(
TimeSpan.FromMilliseconds(250),
this.UpdateTableProgressAsync,
presenter._asyncListener,
CancellationTokenSource.Token);
}
protected abstract Task OnCompletedAsyncWorkerAsync(CancellationToken cancellationToken);
protected abstract ValueTask OnDefinitionFoundWorkerAsync(DefinitionItem definition, CancellationToken cancellationToken);
protected abstract ValueTask OnReferenceFoundWorkerAsync(SourceReferenceItem reference, CancellationToken cancellationToken);
public override ValueTask<FindUsagesOptions> GetOptionsAsync(string language, CancellationToken cancellationToken)
=> ValueTaskFactory.FromResult(_globalOptions.GetFindUsagesOptions(language));
private static ImmutableArray<string> SelectCustomColumnsToInclude(ImmutableArray<ITableColumnDefinition> customColumns, bool includeContainingTypeAndMemberColumns, bool includeKindColumn)
{
var customColumnsToInclude = ArrayBuilder<string>.GetInstance();
foreach (var column in customColumns)
{
switch (column.Name)
{
case AbstractReferenceFinder.ContainingMemberInfoPropertyName:
case AbstractReferenceFinder.ContainingTypeInfoPropertyName:
if (includeContainingTypeAndMemberColumns)
{
customColumnsToInclude.Add(column.Name);
}
break;
case StandardTableColumnDefinitions2.SymbolKind:
if (includeKindColumn)
{
customColumnsToInclude.Add(column.Name);
}
break;
}
}
customColumnsToInclude.Add(StandardTableKeyNames.Repository);
customColumnsToInclude.Add(StandardTableKeyNames.ItemOrigin);
return customColumnsToInclude.ToImmutableAndFree();
}
protected void NotifyChange()
=> _tableDataSink.FactorySnapshotChanged(this);
private void OnFindReferencesWindowClosed(object sender, EventArgs e)
{
Presenter.AssertIsForeground();
CancelSearch();
_findReferencesWindow.Closed -= OnFindReferencesWindowClosed;
TableControl.GroupingsChanged -= OnTableControlGroupingsChanged;
}
private void OnTableControlGroupingsChanged(object sender, EventArgs e)
{
Presenter.AssertIsForeground();
UpdateGroupingByDefinition();
}
private void UpdateGroupingByDefinition()
{
Presenter.AssertIsForeground();
var changed = DetermineCurrentGroupingByDefinitionState();
if (changed)
{
// We changed from grouping-by-definition to not (or vice versa).
// Change which list we show the user.
lock (Gate)
{
CurrentVersionNumber++;
}
// Let all our subscriptions know that we've updated. That way they'll refresh
// and we'll show/hide declarations as appropriate.
NotifyChange();
}
}
private bool DetermineCurrentGroupingByDefinitionState()
{
Presenter.AssertIsForeground();
var definitionColumn = _findReferencesWindow.GetDefinitionColumn();
lock (Gate)
{
var oldGroupingByDefinition = _currentlyGroupingByDefinition;
_currentlyGroupingByDefinition = definitionColumn?.GroupingPriority > 0;
return oldGroupingByDefinition != _currentlyGroupingByDefinition;
}
}
private void CancelSearch()
{
Presenter.AssertIsForeground();
// Cancel any in flight find work that is going on. Note: disposal happens in our own
// implementation of IDisposable.Dispose.
CancellationTokenSource.Cancel();
}
public void Clear()
{
this.Presenter.AssertIsForeground();
// Stop all existing work.
this.CancelSearch();
// Clear the title of the window. It will go back to the default editor title.
_findReferencesWindow.Title = null;
lock (Gate)
{
// Mark ourselves as clear so that no further changes are made.
// Note: we don't actually mutate any of our entry-lists. Instead,
// GetCurrentSnapshot will simply ignore them if it sees that _cleared
// is true. This way we don't have to do anything complicated if we
// keep hearing about definitions/references on the background.
_cleared = true;
CurrentVersionNumber++;
}
// Let all our subscriptions know that we've updated. That way they'll refresh
// and remove all the data.
NotifyChange();
}
#region ITableDataSource
public string DisplayName => "Roslyn Data Source";
public string Identifier
=> StreamingFindUsagesPresenter.RoslynFindUsagesTableDataSourceIdentifier;
public string SourceTypeIdentifier
=> StreamingFindUsagesPresenter.RoslynFindUsagesTableDataSourceSourceTypeIdentifier;
public IDisposable Subscribe(ITableDataSink sink)
{
Presenter.AssertIsForeground();
Debug.Assert(_tableDataSink == null);
_tableDataSink = sink;
_tableDataSink.AddFactory(this, removeAllFactories: true);
_tableDataSink.IsStable = false;
return this;
}
#endregion
#region FindUsagesContext overrides.
public sealed override ValueTask SetSearchTitleAsync(string title, CancellationToken cancellationToken)
{
// Note: IFindAllReferenceWindow.Title is safe to set from any thread.
_findReferencesWindow.Title = title;
return default;
}
public sealed override async ValueTask OnCompletedAsync(CancellationToken cancellationToken)
{
await OnCompletedAsyncWorkerAsync(cancellationToken).ConfigureAwait(false);
_tableDataSink.IsStable = true;
}
public sealed override ValueTask OnDefinitionFoundAsync(DefinitionItem definition, CancellationToken cancellationToken)
{
try
{
lock (Gate)
{
Definitions.Add(definition);
}
return OnDefinitionFoundWorkerAsync(definition, cancellationToken);
}
catch (Exception ex) when (FatalError.ReportAndPropagateUnlessCanceled(ex, cancellationToken))
{
throw ExceptionUtilities.Unreachable();
}
}
protected async Task<Entry?> TryCreateDocumentSpanEntryAsync(
RoslynDefinitionBucket definitionBucket,
DocumentSpan documentSpan,
HighlightSpanKind spanKind,
SymbolUsageInfo symbolUsageInfo,
ImmutableDictionary<string, string> additionalProperties,
CancellationToken cancellationToken)
{
var document = documentSpan.Document;
var options = _globalOptions.GetClassificationOptions(document.Project.Language);
var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
var (excerptResult, lineText) = await ExcerptAsync(sourceText, documentSpan, options, cancellationToken).ConfigureAwait(false);
var mappedDocumentSpan = await AbstractDocumentSpanEntry.TryMapAndGetFirstAsync(documentSpan, sourceText, cancellationToken).ConfigureAwait(false);
if (mappedDocumentSpan == null)
{
// this will be removed from the result
return null;
}
var (guid, projectName, projectFlavor) = GetGuidAndProjectInfo(document);
return DocumentSpanEntry.TryCreate(
this,
definitionBucket,
guid,
projectName,
projectFlavor,
document.FilePath,
documentSpan.SourceSpan,
spanKind,
mappedDocumentSpan.Value,
excerptResult,
lineText,
symbolUsageInfo,
additionalProperties,
ThreadingContext);
}
private static async Task<(ExcerptResult, SourceText)> ExcerptAsync(
SourceText sourceText, DocumentSpan documentSpan, ClassificationOptions options, CancellationToken cancellationToken)
{
var excerptService = documentSpan.Document.Services.GetService<IDocumentExcerptService>();
if (excerptService != null)
{
var result = await excerptService.TryExcerptAsync(documentSpan.Document, documentSpan.SourceSpan, ExcerptMode.SingleLine, options, cancellationToken).ConfigureAwait(false);
if (result != null)
{
return (result.Value, AbstractDocumentSpanEntry.GetLineContainingPosition(result.Value.Content, result.Value.MappedSpan.Start));
}
}
var classificationResult = await ClassifiedSpansAndHighlightSpanFactory.ClassifyAsync(documentSpan, options, cancellationToken).ConfigureAwait(false);
// need to fix the span issue tracking here - https://github.com/dotnet/roslyn/issues/31001
var excerptResult = new ExcerptResult(
sourceText,
classificationResult.HighlightSpan,
classificationResult.ClassifiedSpans,
documentSpan.Document,
documentSpan.SourceSpan);
return (excerptResult, AbstractDocumentSpanEntry.GetLineContainingPosition(sourceText, documentSpan.SourceSpan.Start));
}
public sealed override async ValueTask OnReferenceFoundAsync(SourceReferenceItem reference, CancellationToken cancellationToken)
{
try
{
await OnReferenceFoundWorkerAsync(reference, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex) when (FatalError.ReportAndPropagateUnlessCanceled(ex, cancellationToken))
{
throw ExceptionUtilities.Unreachable();
}
}
protected RoslynDefinitionBucket GetOrCreateDefinitionBucket(DefinitionItem definition, bool expandedByDefault)
{
lock (Gate)
{
if (!_definitionToBucket.TryGetValue(definition, out var bucket))
{
bucket = RoslynDefinitionBucket.Create(Presenter, this, definition, expandedByDefault, ThreadingContext);
_definitionToBucket.Add(definition, bucket);
}
return bucket;
}
}
public sealed override ValueTask ReportMessageAsync(string message, CancellationToken cancellationToken)
{
lock (Gate)
{
NoDefinitionsFoundMessage = message;
}
return ValueTaskFactory.CompletedTask;
}
public sealed override async ValueTask ReportInformationalMessageAsync(string message, CancellationToken cancellationToken)
{
await this.Presenter.ReportInformationalMessageAsync(message, cancellationToken).ConfigureAwait(false);
}
protected sealed override ValueTask ReportProgressAsync(int current, int maximum, CancellationToken cancellationToken)
{
_progressQueue.AddWork((current, maximum));
return default;
}
private ValueTask UpdateTableProgressAsync(ImmutableSegmentedList<(int current, int maximum)> nextBatch, CancellationToken _)
{
if (!nextBatch.IsEmpty)
{
var (current, maximum) = nextBatch.Last();
// Do not update the UI if the current progress is zero. It will switch us from the indeterminate
// progress bar (which conveys to the user that we're working) to showing effectively nothing (which
// makes it appear as if the search is complete). So the user sees:
//
// indeterminate->complete->progress
//
// instead of:
//
// indeterminate->progress
if (current > 0)
_findReferencesWindow.SetProgress(current, maximum);
}
return ValueTaskFactory.CompletedTask;
}
protected static DefinitionItem CreateNoResultsDefinitionItem(string message)
=> DefinitionItem.CreateNonNavigableItem(
GlyphTags.GetTags(Glyph.StatusInformation),
ImmutableArray.Create(new TaggedText(TextTags.Text, message)));
#endregion
#region ITableEntriesSnapshotFactory
public ITableEntriesSnapshot GetCurrentSnapshot()
{
lock (Gate)
{
// If our last cached snapshot matches our current version number, then we
// can just return it. Otherwise, we need to make a snapshot that matches
// our version.
if (_lastSnapshot?.VersionNumber != CurrentVersionNumber)
{
// If we've been cleared, then just return an empty list of entries.
// Otherwise return the appropriate list based on how we're currently
// grouping.
var entries = _cleared
? ImmutableList<Entry>.Empty
: _currentlyGroupingByDefinition
? EntriesWhenGroupingByDefinition
: EntriesWhenNotGroupingByDefinition;
_lastSnapshot = new TableEntriesSnapshot(entries, CurrentVersionNumber);
}
return _lastSnapshot;
}
}
public ITableEntriesSnapshot? GetSnapshot(int versionNumber)
{
lock (Gate)
{
if (_lastSnapshot?.VersionNumber == versionNumber)
{
return _lastSnapshot;
}
if (versionNumber == CurrentVersionNumber)
{
return GetCurrentSnapshot();
}
}
// We didn't have this version. Notify the sinks that something must have changed
// so that they call back into us with the latest version.
NotifyChange();
return null;
}
void IDisposable.Dispose()
{
this.Presenter.AssertIsForeground();
// VS is letting go of us. i.e. because a new FAR call is happening, or because
// of some other event (like the solution being closed). Remove us from the set
// of sources for the window so that the existing data is cleared out.
Debug.Assert(_findReferencesWindow.Manager.Sources.Count == 1);
Debug.Assert(_findReferencesWindow.Manager.Sources[0] == this);
_findReferencesWindow.Manager.RemoveSource(this);
// Remove ourselves from the list of contexts that are currently active.
Presenter._currentContexts.Remove(this);
CancelSearch();
CancellationTokenSource.Dispose();
}
#endregion
}
}
}
|