Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid rooting document instances in NavigateToSearchResult #68068

Merged
merged 7 commits into from
May 13, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ public TestNavigateToSearchResult(TestWorkspace workspace, TextSpan sourceSpan)
_sourceSpan = sourceSpan;
}

public Document Document => _workspace.CurrentSolution.Projects.Single().Documents.Single();
public INavigableItem.NavigableDocument Document => INavigableItem.NavigableDocument.FromDocument(_workspace.CurrentSolution.Projects.Single().Documents.Single());
public TextSpan SourceSpan => _sourceSpan;

public string AdditionalInformation => throw new NotImplementedException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@

#nullable disable

using Microsoft.CodeAnalysis.Navigation;
using Microsoft.VisualStudio.Language.NavigateTo.Interfaces;
using Microsoft.VisualStudio.Shell.Interop;

namespace Microsoft.CodeAnalysis.Editor.Implementation.NavigateTo
{
internal sealed class DefaultNavigateToPreviewService : INavigateToPreviewService
{
public int GetProvisionalViewingStatus(Document document)
=> 0;
public __VSPROVISIONALVIEWINGSTATUS GetProvisionalViewingStatus(INavigableItem.NavigableDocument document)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't love exposing this API directly. Can we have our own enum to wrap?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ This non-public API is tightly coupled to Visual Studio, so wrapping seems unnecessary. In addition to duplicating values that already exist, it makes it more difficult to find documentation and other references to the same structure/values.

=> __VSPROVISIONALVIEWINGSTATUS.PVS_Disabled;

public bool CanPreview(Document document)
=> true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
#nullable disable

using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Navigation;
using Microsoft.VisualStudio.Language.NavigateTo.Interfaces;
using Microsoft.VisualStudio.Shell.Interop;

namespace Microsoft.CodeAnalysis.Editor.Implementation.NavigateTo
{
internal interface INavigateToPreviewService : IWorkspaceService
{
int GetProvisionalViewingStatus(Document document);
__VSPROVISIONALVIEWINGSTATUS GetProvisionalViewingStatus(INavigableItem.NavigableDocument document);
bool CanPreview(Document document);
void PreviewItem(INavigateToItemDisplay itemDisplay);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Imaging.Interop;
using Microsoft.VisualStudio.Language.NavigateTo.Interfaces;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
Expand Down Expand Up @@ -63,7 +64,7 @@ private ReadOnlyCollection<DescriptionItem> CreateDescriptionItems()
return new List<DescriptionItem>().AsReadOnly();
}

var sourceText = document.GetTextSynchronously(CancellationToken.None);
var sourceText = document.GetTextSynchronously(document.Workspace.CurrentSolution, CancellationToken.None);
var span = NavigateToUtilities.GetBoundedSpan(_searchResult.NavigableItem, sourceText);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ick. i never noticed this before. This really violates a core principle of navto (that no work is done client side to avoid costly UI perf). This is on the UI thread and goes and potentially does IO. Can you make issue that says that server should compute and add this info to items (ideally only if "show descriptions" is selected by user), and the client just has to present. no need for GetTextSynchronously then.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ #68104


var items = new List<DescriptionItem>
Expand Down Expand Up @@ -111,13 +112,13 @@ public int GetProvisionalViewingStatus()
var document = _searchResult.NavigableItem.Document;
if (document == null)
{
return 0;
return (int)__VSPROVISIONALVIEWINGSTATUS.PVS_Disabled;
}

var workspace = document.Project.Solution.Workspace;
var workspace = document.Workspace;
var previewService = workspace.Services.GetService<INavigateToPreviewService>();

return previewService.GetProvisionalViewingStatus(document);
return (int)previewService.GetProvisionalViewingStatus(document);
}

public void PreviewItem()
Expand All @@ -128,7 +129,7 @@ public void PreviewItem()
return;
}

var workspace = document.Project.Solution.Workspace;
var workspace = document.Workspace;
sharwell marked this conversation as resolved.
Show resolved Hide resolved
var previewService = workspace.Services.GetService<INavigateToPreviewService>();

previewService.PreviewItem(this);
Expand Down
2 changes: 1 addition & 1 deletion src/EditorFeatures/Core.Wpf/Peek/PeekableItemSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ private static async IAsyncEnumerable<IPeekableItem> GetPeekableItemsForNavigabl
if (await navigationService.CanNavigateToPositionAsync(
workspace, document.Id, item.SourceSpan.Start, cancellationToken).ConfigureAwait(false))
{
var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
var text = await document.GetTextAsync(project.Solution, cancellationToken).ConfigureAwait(false);
var linePositionSpan = text.Lines.GetLinePositionSpan(item.SourceSpan);
if (document.FilePath != null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ internal async Task<ImmutableArray<CodeDefinitionWindowLocation>> GetContextFrom
{
if (await navigationService.CanNavigateToSpanAsync(workspace, item.Document.Id, item.SourceSpan, cancellationToken).ConfigureAwait(false))
{
var text = await item.Document.GetTextAsync(cancellationToken).ConfigureAwait(false);
var text = await item.Document.GetTextAsync(document.Project.Solution, cancellationToken).ConfigureAwait(false);
var linePositionSpan = text.Lines.GetLinePositionSpan(item.SourceSpan);

if (item.Document.FilePath != null)
Expand Down
2 changes: 1 addition & 1 deletion src/EditorFeatures/Core/NavigateTo/NavigateToHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ private static async Task NavigateToAsync(
if (document == null)
return;

var workspace = document.Project.Solution.Workspace;
var workspace = document.Workspace;
var navigationService = workspace.Services.GetRequiredService<IDocumentNavigationService>();

// Document tabs opened by NavigateTo are carefully created as preview or regular tabs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ Namespace Microsoft.CodeAnalysis.Editor.CodeDefinitionWindow.UnitTests
Private Class FakeNavigableItem
Implements INavigableItem

Private ReadOnly _document As Document
Private ReadOnly _document As INavigableItem.NavigableDocument

Public Sub New(document As Document)
_document = document
_document = INavigableItem.NavigableDocument.FromDocument(document)
End Sub

Public ReadOnly Property ChildItems As ImmutableArray(Of INavigableItem) Implements INavigableItem.ChildItems
Expand All @@ -46,7 +46,7 @@ Namespace Microsoft.CodeAnalysis.Editor.CodeDefinitionWindow.UnitTests
End Get
End Property

Public ReadOnly Property Document As Document Implements INavigableItem.Document
Public ReadOnly Property Document As INavigableItem.NavigableDocument Implements INavigableItem.Document
Get
Return _document
End Get
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript
internal sealed class VSTypeScriptNavigableItemWrapper : INavigableItem
{
private readonly IVSTypeScriptNavigableItem _navigableItem;
private readonly INavigableItem.NavigableDocument _navigableDocument;

public VSTypeScriptNavigableItemWrapper(IVSTypeScriptNavigableItem navigableItem)
{
_navigableItem = navigableItem;
_navigableDocument = INavigableItem.NavigableDocument.FromDocument(navigableItem.Document);
}

public Glyph Glyph => _navigableItem.Glyph;
Expand All @@ -26,7 +28,7 @@ public VSTypeScriptNavigableItemWrapper(IVSTypeScriptNavigableItem navigableItem

public bool IsImplicitlyDeclared => _navigableItem.IsImplicitlyDeclared;

public Document Document => _navigableItem.Document;
public INavigableItem.NavigableDocument Document => _navigableDocument;
sharwell marked this conversation as resolved.
Show resolved Hide resolved

public TextSpan SourceSpan => _navigableItem.SourceSpan;

Expand Down
40 changes: 19 additions & 21 deletions src/Features/Core/Portable/NavigateTo/RoslynNavigateToItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.Serialization;
using System.Threading;
Expand All @@ -16,7 +15,6 @@
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.NavigateTo
{
Expand Down Expand Up @@ -108,7 +106,7 @@ private class NavigateToSearchResult : INavigateToSearchResult, INavigableItem
/// <summary>
/// The <see cref="Document"/> that <see cref="_item"/> is contained within.
/// </summary>
private readonly Document _itemDocument;
private readonly INavigableItem.NavigableDocument _itemDocument;

/// <summary>
/// The document the user was editing when they invoked the navigate-to operation.
Expand All @@ -124,35 +122,35 @@ public NavigateToSearchResult(
Document? activeDocument)
{
_item = item;
_itemDocument = itemDocument;
_itemDocument = INavigableItem.NavigableDocument.FromDocument(itemDocument);
if (activeDocument is not null)
_activeDocument = (activeDocument.Id, activeDocument.Folders);

_additionalInformation = ComputeAdditionalInformation();
_additionalInformation = ComputeAdditionalInformation(in item, itemDocument);
_secondarySort = new Lazy<string>(ComputeSecondarySort);
}

private string ComputeAdditionalInformation()
private static string ComputeAdditionalInformation(in RoslynNavigateToItem item, Document itemDocument)
{
// For partial types, state what file they're in so the user can disambiguate the results.
var combinedProjectName = ComputeCombinedProjectName();
return (_item.DeclaredSymbolInfo.IsPartial, IsNonNestedNamedType()) switch
var combinedProjectName = ComputeCombinedProjectName(in item, itemDocument);
return (item.DeclaredSymbolInfo.IsPartial, IsNonNestedNamedType(in item)) switch
{
(true, true) => string.Format(FeaturesResources._0_dash_1, _itemDocument.Name, combinedProjectName),
(true, false) => string.Format(FeaturesResources.in_0_1_2, _item.DeclaredSymbolInfo.ContainerDisplayName, _itemDocument.Name, combinedProjectName),
(true, true) => string.Format(FeaturesResources._0_dash_1, itemDocument.Name, combinedProjectName),
(true, false) => string.Format(FeaturesResources.in_0_1_2, item.DeclaredSymbolInfo.ContainerDisplayName, itemDocument.Name, combinedProjectName),
(false, true) => string.Format(FeaturesResources.project_0, combinedProjectName),
(false, false) => string.Format(FeaturesResources.in_0_project_1, _item.DeclaredSymbolInfo.ContainerDisplayName, combinedProjectName),
(false, false) => string.Format(FeaturesResources.in_0_project_1, item.DeclaredSymbolInfo.ContainerDisplayName, combinedProjectName),
};
}

private string ComputeCombinedProjectName()
private static string ComputeCombinedProjectName(in RoslynNavigateToItem item, Document itemDocument)
{
// If there aren't any additional matches in other projects, we don't need to merge anything.
if (_item.AdditionalMatchingProjects.Length > 0)
if (item.AdditionalMatchingProjects.Length > 0)
{
// First get the simple project name and flavor for the actual project we got a hit in. If we can't
// figure this out, we can't create a merged name.
var firstProject = _itemDocument.Project;
var firstProject = itemDocument.Project;
var (firstProjectName, firstProjectFlavor) = firstProject.State.NameAndFlavor;

if (firstProjectName != null)
Expand All @@ -165,7 +163,7 @@ private string ComputeCombinedProjectName()
// Now, do the same for the other projects where we had a match. As above, if we can't figure out the
// simple name/flavor, or if the simple project name doesn't match the simple project name we started
// with then we can't merge these.
foreach (var additionalProjectId in _item.AdditionalMatchingProjects)
foreach (var additionalProjectId in item.AdditionalMatchingProjects)
{
var additionalProject = solution.GetRequiredProject(additionalProjectId);
var (projectName, projectFlavor) = additionalProject.State.NameAndFlavor;
Expand All @@ -181,17 +179,17 @@ private string ComputeCombinedProjectName()
}

// Couldn't compute a merged project name (or only had one project). Just return the name of hte project itself.
return _itemDocument.Project.Name;
return itemDocument.Project.Name;
}

string INavigateToSearchResult.AdditionalInformation => _additionalInformation;

private bool IsNonNestedNamedType()
=> !_item.DeclaredSymbolInfo.IsNestedType && IsNamedType();
private static bool IsNonNestedNamedType(in RoslynNavigateToItem item)
=> !item.DeclaredSymbolInfo.IsNestedType && IsNamedType(in item);

private bool IsNamedType()
private static bool IsNamedType(in RoslynNavigateToItem item)
{
switch (_item.DeclaredSymbolInfo.Kind)
switch (item.DeclaredSymbolInfo.Kind)
{
case DeclaredSymbolInfoKind.Class:
case DeclaredSymbolInfoKind.Record:
Expand Down Expand Up @@ -356,7 +354,7 @@ ImmutableArray<TaggedText> INavigableItem.DisplayTaggedParts
/// </summary>
bool INavigableItem.IsImplicitlyDeclared => false;

Document INavigableItem.Document => _itemDocument;
INavigableItem.NavigableDocument INavigableItem.Document => _itemDocument;

TextSpan INavigableItem.SourceSpan => _item.DeclaredSymbolInfo.Span;

Expand Down
43 changes: 40 additions & 3 deletions src/Features/Core/Portable/Navigation/INavigableItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.Navigation
Expand Down Expand Up @@ -33,7 +36,7 @@ internal interface INavigableItem
/// </summary>
bool IsImplicitlyDeclared { get; }

Document Document { get; }
NavigableDocument Document { get; }
sharwell marked this conversation as resolved.
Show resolved Hide resolved
TextSpan SourceSpan { get; }

/// <summary>
Expand All @@ -45,5 +48,39 @@ internal interface INavigableItem
bool IsStale { get; }

ImmutableArray<INavigableItem> ChildItems { get; }

public record NavigableDocument(NavigableProject Project, string Name, string? FilePath, IReadOnlyList<string> Folders, DocumentId Id)
{
public required Workspace Workspace { get; init; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why have some methods be parameters, and some be required props?

Copy link
Member Author

@sharwell sharwell May 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ Moved all to parameters


public static NavigableDocument FromDocument(Document document)
=> new(NavigableProject.FromProject(document.Project), document.Name, document.FilePath, document.Folders, document.Id) { Workspace = document.Project.Solution.Workspace };

internal async ValueTask<Document> GetDocumentAsync(Solution solution, CancellationToken cancellationToken)
{
if (solution.GetDocument(Id) is { } document)
return document;

return await solution.GetRequiredDocumentAsync(Id, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false);
sharwell marked this conversation as resolved.
Show resolved Hide resolved
}

internal async Task<SourceText> GetTextAsync(Solution solution, CancellationToken cancellationToken)
{
var document = await GetDocumentAsync(solution, cancellationToken).ConfigureAwait(false);
return await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
}

internal SourceText GetTextSynchronously(Solution solution, CancellationToken cancellationToken)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's teh scenario where the text is needed synchronously?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ This method contains an extra parameter (solution) relative to the original use, so all calls to this method will appear in the diff if you search for GetTextSynchronously. There is one call in NavigateToItemDisplay.cs.

{
var document = solution.GetRequiredDocument(Id);
return document.GetTextSynchronously(cancellationToken);
}
}

public record struct NavigableProject(string Name, ProjectId Id)
{
public static NavigableProject FromProject(Project project)
=> new(project.Name, project.Id);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
#nullable disable

using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Navigation
{
Expand All @@ -17,6 +19,7 @@ private class SymbolLocationNavigableItem : INavigableItem
private readonly Solution _solution;
private readonly ISymbol _symbol;
private readonly Location _location;
private StrongBox<INavigableItem.NavigableDocument> _lazyDocument;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe doc this? That null means, uncomputed, but we could compute and still point at null?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⏱️ Enabling nullable reference types on this file to improve clarity

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ Ended up documenting this, but not changing the nullability. There is a discrepancy in nullability which existed prior to this work, and I opted to defer fixing that.


public SymbolLocationNavigableItem(
Solution solution,
Expand All @@ -38,8 +41,21 @@ public SymbolLocationNavigableItem(

public bool IsImplicitlyDeclared => _symbol.IsImplicitlyDeclared;

public Document Document
=> _location.IsInSource ? _solution.GetDocument(_location.SourceTree) : null;
public INavigableItem.NavigableDocument Document
{
get
{
return LazyInitialization.EnsureInitialized(
ref _lazyDocument,
static self =>
{
return (self._location.IsInSource && self._solution.GetDocument(self._location.SourceTree) is { } document)
? INavigableItem.NavigableDocument.FromDocument(document)
: null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is concerning. We can return null, but the API says it's not null.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear which item you are referring to (there are two return values in question).

This overload of EnsureInitialized operates on a StrongBox<T> specifically because it allows the initialized value to be null.

/// <summary>
/// Ensure that the given target value is initialized in a thread-safe manner. This overload supports the
/// initialization of value types, and reference type fields where <see langword="null"/> is considered an
/// initialized value.
/// </summary>
/// <typeparam name="T">The type of the target value.</typeparam>
/// <param name="target">A target value box to initialize.</param>
/// <typeparam name="U">The type of the <paramref name="state"/> argument passed to the value factory.</typeparam>
/// <param name="valueFactory">A factory delegate to create a new instance of the target value. Note that this delegate may be called
/// more than once by multiple threads, but only one of those values will successfully be written to the target.</param>
/// <param name="state">An argument passed to the value factory.</param>
/// <returns>The target value.</returns>
public static T? EnsureInitialized<T, U>([NotNull] ref StrongBox<T?>? target, Func<U, T?> valueFactory, U state)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear which item you are referring to

Api is: public INavigableItem.NavigableDocument Document which is non-null. my guess is the file is not NRT annotated though. If that's the case, you don't have to change anything.

If this is NRT annotated, something is wrong :)

Copy link
Member Author

@sharwell sharwell May 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ This file does not have null annotations enabled currently.

},
this);
}
}

public TextSpan SourceSpan => _location.SourceSpan;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ public AbstractGoToDefinitionHandler(IMetadataAsSourceFileService metadataAsSour
}

var location = await ProtocolConversions.TextSpanToLocationAsync(
definition.Document, definition.SourceSpan, definition.IsStale, cancellationToken).ConfigureAwait(false);
await definition.Document.GetDocumentAsync(document.Project.Solution, cancellationToken).ConfigureAwait(false),
definition.SourceSpan,
definition.IsStale,
cancellationToken).ConfigureAwait(false);
locations.AddIfNotNull(location);
}
}
Expand Down
Loading