Skip to content

Commit

Permalink
Merge pull request #73723 from CyrusNajmabadi/codeDefWindow
Browse files Browse the repository at this point in the history
  • Loading branch information
CyrusNajmabadi authored May 26, 2024
2 parents 13dd08c + 836ccdc commit 1f5f096
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,21 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.FindUsages;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.MetadataAsSource;
using Microsoft.CodeAnalysis.Navigation;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Threading;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;

Expand All @@ -38,23 +37,38 @@ namespace Microsoft.CodeAnalysis.CodeDefinitionWindow;
[Export(typeof(DefinitionContextTracker))]
[ContentType(ContentTypeNames.RoslynContentType)]
[TextViewRole(PredefinedTextViewRoles.Interactive)]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal class DefinitionContextTracker(
IMetadataAsSourceFileService metadataAsSourceFileService,
ICodeDefinitionWindowService codeDefinitionWindowService,
IThreadingContext threadingContext,
IGlobalOptionService globalOptions,
IAsynchronousOperationListenerProvider listenerProvider) : ITextViewConnectionListener
internal class DefinitionContextTracker : ITextViewConnectionListener
{
private readonly HashSet<ITextView> _subscribedViews = [];
private readonly IMetadataAsSourceFileService _metadataAsSourceFileService = metadataAsSourceFileService;
private readonly ICodeDefinitionWindowService _codeDefinitionWindowService = codeDefinitionWindowService;
private readonly IThreadingContext _threadingContext = threadingContext;
private readonly IAsynchronousOperationListener _asyncListener = listenerProvider.GetListener(FeatureAttribute.CodeDefinitionWindow);
private readonly IGlobalOptionService _globalOptions = globalOptions;

private CancellationTokenSource? _currentUpdateCancellationToken;
private readonly IMetadataAsSourceFileService _metadataAsSourceFileService;
private readonly ICodeDefinitionWindowService _codeDefinitionWindowService;
private readonly IThreadingContext _threadingContext;
private readonly IAsynchronousOperationListener _asyncListener;
private readonly IGlobalOptionService _globalOptions;

private readonly AsyncBatchingWorkQueue<SnapshotPoint> _workQueue;

[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public DefinitionContextTracker(
IMetadataAsSourceFileService metadataAsSourceFileService,
ICodeDefinitionWindowService codeDefinitionWindowService,
IThreadingContext threadingContext,
IGlobalOptionService globalOptions,
IAsynchronousOperationListenerProvider listenerProvider)
{
_metadataAsSourceFileService = metadataAsSourceFileService;
_codeDefinitionWindowService = codeDefinitionWindowService;
_threadingContext = threadingContext;
_asyncListener = listenerProvider.GetListener(FeatureAttribute.CodeDefinitionWindow);
_globalOptions = globalOptions;

_workQueue = new AsyncBatchingWorkQueue<SnapshotPoint>(
DelayTimeSpan.Short,
ProcessWorkAsync,
_asyncListener,
_threadingContext.DisposalToken);
}

void ITextViewConnectionListener.SubjectBuffersConnected(ITextView textView, ConnectionReason reason, IReadOnlyCollection<ITextBuffer> subjectBuffers)
{
Expand All @@ -75,12 +89,10 @@ void ITextViewConnectionListener.SubjectBuffersDisconnected(ITextView textView,
Contract.ThrowIfFalse(_threadingContext.JoinableTaskContext.IsOnMainThread);

if (reason == ConnectionReason.TextViewLifetime ||
!textView.BufferGraph.GetTextBuffers(b => b.ContentType.IsOfType(ContentTypeNames.RoslynContentType)).Any())
textView.BufferGraph.GetTextBuffers(b => b.ContentType.IsOfType(ContentTypeNames.RoslynContentType)).Count == 0)
{
if (_subscribedViews.Remove(textView))
{
textView.Caret.PositionChanged -= OnTextViewCaretPositionChanged;
}
}
}

Expand All @@ -95,67 +107,47 @@ private void QueueUpdateForCaretPosition(CaretPosition caretPosition)
{
Contract.ThrowIfFalse(_threadingContext.JoinableTaskContext.IsOnMainThread);

// Cancel any pending update for this view
_currentUpdateCancellationToken?.Cancel();

// See if we moved somewhere else in a projection that we care about
var pointInRoslynSnapshot = caretPosition.Point.GetPoint(tb => tb.ContentType.IsOfType(ContentTypeNames.RoslynContentType), caretPosition.Affinity);
if (pointInRoslynSnapshot == null)
{
return;
}

_currentUpdateCancellationToken = new CancellationTokenSource();

var cancellationToken = _currentUpdateCancellationToken.Token;
var asyncToken = _asyncListener.BeginAsyncOperation(nameof(DefinitionContextTracker) + "." + nameof(QueueUpdateForCaretPosition));
UpdateForCaretPositionAsync(pointInRoslynSnapshot.Value, cancellationToken).CompletesAsyncOperation(asyncToken);
_workQueue.AddWork(pointInRoslynSnapshot.Value, cancelExistingWork: true);
}

private async Task UpdateForCaretPositionAsync(SnapshotPoint pointInRoslynSnapshot, CancellationToken cancellationToken)
private async ValueTask ProcessWorkAsync(ImmutableSegmentedList<SnapshotPoint> points, CancellationToken cancellationToken)
{
try
{
await _asyncListener.Delay(DelayTimeSpan.Short, cancellationToken).ConfigureAwait(false);
var lastPoint = points.Last();

// If it's not open, don't do anything, since if we are going to show locations in metadata that might
// be expensive. This doesn't cause a functional issue, since opening the window clears whatever was previously there
// so the user won't notice we weren't doing anything when it was open.
if (!await _codeDefinitionWindowService.IsWindowOpenAsync(cancellationToken).ConfigureAwait(false))
return;

var snapshot = pointInRoslynSnapshot.Snapshot;
var workspace = snapshot.TextBuffer.GetWorkspace();
var document = snapshot.GetOpenDocumentInCurrentContextWithChanges();
if (workspace is null || document is null)
return;
// If it's not open, don't do anything, since if we are going to show locations in metadata that might
// be expensive. This doesn't cause a functional issue, since opening the window clears whatever was previously there
// so the user won't notice we weren't doing anything when it was open.
if (!await _codeDefinitionWindowService.IsWindowOpenAsync(cancellationToken).ConfigureAwait(false))
return;

// Ensure we're off the UI thread for the rest of this since we don't want to be computing locations on the UI thread.
await TaskScheduler.Default;
var snapshot = lastPoint.Snapshot;
var document = snapshot.GetOpenDocumentInCurrentContextWithChanges();
if (document is null)
return;

var locations = await GetContextFromPointAsync(workspace, document, pointInRoslynSnapshot, cancellationToken).ConfigureAwait(true);
await _codeDefinitionWindowService.SetContextAsync(locations, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
}
catch (Exception ex) when (FatalError.ReportAndCatch(ex))
{
}
var locations = await GetContextFromPointAsync(document, lastPoint, cancellationToken).ConfigureAwait(true);
await _codeDefinitionWindowService.SetContextAsync(locations, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Internal for testing purposes.
/// </summary>
internal async Task<ImmutableArray<CodeDefinitionWindowLocation>> GetContextFromPointAsync(
Workspace workspace, Document document, int position, CancellationToken cancellationToken)
Document document, int position, CancellationToken cancellationToken)
{
using var locations = TemporaryArray<CodeDefinitionWindowLocation>.Empty;

var workspace = document.Project.Solution.Workspace;
var navigableItems = await GetNavigableItemsAsync(document, position, cancellationToken).ConfigureAwait(false);
if (navigableItems.Length > 0)
{
var navigationService = workspace.Services.GetRequiredService<IDocumentNavigationService>();

using var _ = PooledObjects.ArrayBuilder<CodeDefinitionWindowLocation>.GetInstance(navigableItems.Length, out var builder);
foreach (var item in navigableItems)
{
if (await navigationService.CanNavigateToSpanAsync(workspace, item.Document.Id, item.SourceSpan, cancellationToken).ConfigureAwait(false))
Expand All @@ -164,45 +156,41 @@ internal async Task<ImmutableArray<CodeDefinitionWindowLocation>> GetContextFrom
var linePositionSpan = text.Lines.GetLinePositionSpan(item.SourceSpan);

if (item.Document.FilePath != null)
{
builder.Add(new CodeDefinitionWindowLocation(item.DisplayTaggedParts.JoinText(), item.Document.FilePath, linePositionSpan.Start));
}
locations.Add(new CodeDefinitionWindowLocation(item.DisplayTaggedParts.JoinText(), item.Document.FilePath, linePositionSpan.Start));
}
}

return builder.ToImmutableAndClear();
}

// We didn't have regular source references, but possibly:
// 1. Another language (like XAML) will take over via ISymbolNavigationService
// 2. There are no locations from source, so we'll try to generate a metadata as source file and use that
var symbol = await SymbolFinder.FindSymbolAtPositionAsync(
document,
position,
cancellationToken: cancellationToken).ConfigureAwait(false);

if (symbol == null)
else
{
return [];
}
// We didn't have regular source references, but possibly:
// 1. Another language (like XAML) will take over via ISymbolNavigationService
// 2. There are no locations from source, so we'll try to generate a metadata as source file and use that
var symbol = await SymbolFinder.FindSymbolAtPositionAsync(
document, position, cancellationToken: cancellationToken).ConfigureAwait(false);

var symbolNavigationService = workspace.Services.GetRequiredService<ISymbolNavigationService>();
var definitionItem = symbol.ToNonClassifiedDefinitionItem(document.Project.Solution, includeHiddenLocations: false);
var result = await symbolNavigationService.GetExternalNavigationSymbolLocationAsync(definitionItem, cancellationToken).ConfigureAwait(false);
if (symbol != null)
{
var symbolNavigationService = workspace.Services.GetRequiredService<ISymbolNavigationService>();
var definitionItem = symbol.ToNonClassifiedDefinitionItem(document.Project.Solution, includeHiddenLocations: false);
var result = await symbolNavigationService.GetExternalNavigationSymbolLocationAsync(definitionItem, cancellationToken).ConfigureAwait(false);

if (result != null)
{
return [new CodeDefinitionWindowLocation(symbol.ToDisplayString(), result.Value.filePath, result.Value.linePosition)];
}
else if (_metadataAsSourceFileService.IsNavigableMetadataSymbol(symbol))
{
var options = _globalOptions.GetMetadataAsSourceOptions(document.Project.Services);
var declarationFile = await _metadataAsSourceFileService.GetGeneratedFileAsync(workspace, document.Project, symbol, signaturesOnly: false, options, cancellationToken).ConfigureAwait(false);
var identifierSpan = declarationFile.IdentifierLocation.GetLineSpan().Span;
return [new CodeDefinitionWindowLocation(symbol.ToDisplayString(), declarationFile.FilePath, identifierSpan.Start)];
if (result != null)
{
locations.Add(new CodeDefinitionWindowLocation(
symbol.ToDisplayString(), result.Value.filePath, result.Value.linePosition));
}
else if (_metadataAsSourceFileService.IsNavigableMetadataSymbol(symbol))
{
var options = _globalOptions.GetMetadataAsSourceOptions(document.Project.Services);
var declarationFile = await _metadataAsSourceFileService.GetGeneratedFileAsync(workspace, document.Project, symbol, signaturesOnly: false, options, cancellationToken).ConfigureAwait(false);
var identifierSpan = declarationFile.IdentifierLocation.GetLineSpan().Span;
locations.Add(new CodeDefinitionWindowLocation(
symbol.ToDisplayString(), declarationFile.FilePath, identifierSpan.Start));
}
}
}

return [];
return locations.ToImmutableAndClear();
}

private static async Task<ImmutableArray<INavigableItem>> GetNavigableItemsAsync(Document document, int position, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces
Imports Microsoft.CodeAnalysis.Host.Mef

Namespace Microsoft.CodeAnalysis.Editor.CodeDefinitionWindow.UnitTests

Public MustInherit Class AbstractCodeDefinitionWindowTests
Public Shared ReadOnly TestComposition As TestComposition =
EditorTestCompositions.EditorFeatures _
Expand Down Expand Up @@ -55,10 +54,7 @@ Namespace Microsoft.CodeAnalysis.Editor.CodeDefinitionWindow.UnitTests

Dim definitionContextTracker = workspace.ExportProvider.GetExportedValue(Of DefinitionContextTracker)
Dim locations = Await definitionContextTracker.GetContextFromPointAsync(
workspace,
document,
hostDocument.CursorPosition.Value,
CancellationToken.None)
document, hostDocument.CursorPosition.Value, CancellationToken.None)

Dim location = Assert.Single(locations)
Assert.Equal(displayName, location.DisplayName)
Expand All @@ -81,10 +77,7 @@ Namespace Microsoft.CodeAnalysis.Editor.CodeDefinitionWindow.UnitTests

Dim definitionContextTracker = workspace.ExportProvider.GetExportedValue(Of DefinitionContextTracker)
Dim locations = Await definitionContextTracker.GetContextFromPointAsync(
workspace,
triggerDocument,
triggerHostDocument.CursorPosition.Value,
CancellationToken.None)
triggerDocument, triggerHostDocument.CursorPosition.Value, CancellationToken.None)

Dim expectedHostDocument = workspace.Documents.Single(Function(d) d.SelectedSpans.Any())
Dim expectedDocument = workspace.CurrentSolution.GetDocument(expectedHostDocument.Id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,7 @@ Namespace Microsoft.CodeAnalysis.Editor.CodeDefinitionWindow.UnitTests

Dim definitionContextTracker = workspace.ExportProvider.GetExportedValue(Of DefinitionContextTracker)
Dim locations = Await definitionContextTracker.GetContextFromPointAsync(
workspace,
document,
hostDocument.CursorPosition.Value,
CancellationToken.None)
document, hostDocument.CursorPosition.Value, CancellationToken.None)

Dim expectedLocation = New CodeDefinitionWindowLocation(
"DisplayText",
Expand Down

0 comments on commit 1f5f096

Please # to comment.