Skip to content

Commit 00ec0ac

Browse files
Find results in related 'partial type parts' when doing a scoped nav-to search. (#77074)
2 parents 25227ea + 274fead commit 00ec0ac

File tree

6 files changed

+238
-15
lines changed

6 files changed

+238
-15
lines changed

src/EditorFeatures/CSharpTest/NavigateTo/NavigateToSearcherTests.cs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6+
using System.Collections.Concurrent;
67
using System.Collections.Immutable;
78
using System.Linq;
89
using System.Threading;
@@ -12,6 +13,7 @@
1213
using Microsoft.CodeAnalysis.NavigateTo;
1314
using Microsoft.CodeAnalysis.Navigation;
1415
using Microsoft.CodeAnalysis.PatternMatching;
16+
using Microsoft.CodeAnalysis.Shared.Extensions;
1517
using Microsoft.CodeAnalysis.Test.Utilities;
1618
using Microsoft.CodeAnalysis.Text;
1719
using Moq;
@@ -354,6 +356,151 @@ public class D
354356
Assert.True(searchGeneratedDocumentsAsyncCalled);
355357
}
356358

359+
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/77051")]
360+
public async Task DocumentScopeRelatedDocuments_Inheritance()
361+
{
362+
using var workspace = EditorTestWorkspace.Create("""
363+
<Workspace>
364+
<Project Language="C#" AssemblyName="Assembly1" CommonReferences="true">
365+
<Document FilePath="z:\\file1.cs">
366+
public class C : Base
367+
{
368+
// Starting search here.
369+
void Goo1() { }
370+
}
371+
</Document>
372+
<Document FilePath="z:\\file2.cs">
373+
public class Base
374+
{
375+
// Should find this.
376+
void Goo2() { }
377+
}
378+
public class Other
379+
{
380+
// Should not find this.
381+
void Goo3() { }
382+
}
383+
</Document>
384+
</Project>
385+
</Workspace>
386+
""", composition: FirstActiveAndVisibleComposition);
387+
388+
var hostMock = new Mock<INavigateToSearcherHost>(MockBehavior.Strict);
389+
hostMock.Setup(h => h.IsFullyLoadedAsync(It.IsAny<CancellationToken>())).Returns(() => new ValueTask<bool>(true));
390+
391+
var project = workspace.CurrentSolution.Projects.Single();
392+
var searchService = project.GetRequiredLanguageService<INavigateToSearchService>();
393+
394+
hostMock.Setup(h => h.GetNavigateToSearchService(It.IsAny<Project>())).Returns(() => searchService);
395+
396+
var callback = new TestNavigateToSearchCallback();
397+
398+
var searcher = NavigateToSearcher.Create(
399+
workspace.CurrentSolution,
400+
callback,
401+
"Goo",
402+
kinds: searchService.KindsProvided,
403+
hostMock.Object);
404+
405+
await searcher.SearchAsync(NavigateToSearchScope.Document, CancellationToken.None);
406+
407+
Assert.Equal(2, callback.Results.Count);
408+
409+
var firstDocument = project.Documents.Single(d => d.FilePath!.Contains("file1"));
410+
var secondDocument = project.Documents.Single(d => d.FilePath!.Contains("file2"));
411+
412+
var firstDocumentResult = Assert.Single(callback.Results, r => r.NavigableItem.Document.Id == firstDocument.Id);
413+
var secondDocumentResult = Assert.Single(callback.Results, r => r.NavigableItem.Document.Id == secondDocument.Id);
414+
415+
Assert.Equal("Goo1", firstDocumentResult.Name);
416+
Assert.Equal("Goo2", secondDocumentResult.Name);
417+
}
418+
419+
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/77051")]
420+
public async Task DocumentScopeRelatedDocuments_Partial()
421+
{
422+
using var workspace = EditorTestWorkspace.Create("""
423+
<Workspace>
424+
<Project Language="C#" AssemblyName="Assembly1" CommonReferences="true">
425+
<Document FilePath="z:\\file1.cs">
426+
public partial class C
427+
{
428+
// Starting search here.
429+
void Goo1() { }
430+
}
431+
</Document>
432+
<Document FilePath="z:\\file2.cs">
433+
public class Base
434+
{
435+
// Should not find this.
436+
void Goo2() { }
437+
}
438+
public partial class C
439+
{
440+
// Should find this.
441+
void Goo3() { }
442+
}
443+
</Document>
444+
</Project>
445+
</Workspace>
446+
""", composition: FirstActiveAndVisibleComposition);
447+
448+
var hostMock = new Mock<INavigateToSearcherHost>(MockBehavior.Strict);
449+
hostMock.Setup(h => h.IsFullyLoadedAsync(It.IsAny<CancellationToken>())).Returns(() => new ValueTask<bool>(true));
450+
451+
var project = workspace.CurrentSolution.Projects.Single();
452+
var searchService = project.GetRequiredLanguageService<INavigateToSearchService>();
453+
454+
hostMock.Setup(h => h.GetNavigateToSearchService(It.IsAny<Project>())).Returns(() => searchService);
455+
456+
var callback = new TestNavigateToSearchCallback();
457+
458+
var searcher = NavigateToSearcher.Create(
459+
workspace.CurrentSolution,
460+
callback,
461+
"Goo",
462+
kinds: searchService.KindsProvided,
463+
hostMock.Object);
464+
465+
await searcher.SearchAsync(NavigateToSearchScope.Document, CancellationToken.None);
466+
467+
Assert.Equal(2, callback.Results.Count);
468+
469+
var firstDocument = project.Documents.Single(d => d.FilePath!.Contains("file1"));
470+
var secondDocument = project.Documents.Single(d => d.FilePath!.Contains("file2"));
471+
472+
var firstDocumentResult = Assert.Single(callback.Results, r => r.NavigableItem.Document.Id == firstDocument.Id);
473+
var secondDocumentResult = Assert.Single(callback.Results, r => r.NavigableItem.Document.Id == secondDocument.Id);
474+
475+
Assert.Equal("Goo1", firstDocumentResult.Name);
476+
Assert.Equal("Goo3", secondDocumentResult.Name);
477+
}
478+
479+
private sealed class TestNavigateToSearchCallback : INavigateToSearchCallback
480+
{
481+
public readonly ConcurrentBag<INavigateToSearchResult> Results = [];
482+
483+
public void Done(bool isFullyLoaded)
484+
{
485+
}
486+
487+
public void ReportIncomplete()
488+
{
489+
}
490+
491+
public Task AddResultsAsync(ImmutableArray<INavigateToSearchResult> results, CancellationToken cancellationToken)
492+
{
493+
foreach (var result in results)
494+
this.Results.Add(result);
495+
496+
return Task.CompletedTask;
497+
}
498+
499+
public void ReportProgress(int current, int maximum)
500+
{
501+
}
502+
}
503+
357504
private sealed class MockAdvancedNavigateToSearchService : IAdvancedNavigateToSearchService
358505
{
359506
public IImmutableSet<string> KindsProvided => AbstractNavigateToSearchService.AllKinds;

src/Features/CSharp/Portable/NavigateTo/CSharpNavigateToSearchService.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,4 @@ namespace Microsoft.CodeAnalysis.CSharp.NavigateTo;
1212
[ExportLanguageService(typeof(INavigateToSearchService), LanguageNames.CSharp), Shared]
1313
[method: ImportingConstructor]
1414
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
15-
internal class CSharpNavigateToSearchService() : AbstractNavigateToSearchService
16-
{
17-
}
15+
internal sealed class CSharpNavigateToSearchService() : AbstractNavigateToSearchService;

src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.NormalSearch.cs

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@
88
using System.Linq;
99
using System.Threading;
1010
using System.Threading.Tasks;
11+
using Microsoft.CodeAnalysis.LanguageService;
1112
using Microsoft.CodeAnalysis.PatternMatching;
13+
using Microsoft.CodeAnalysis.PooledObjects;
1214
using Microsoft.CodeAnalysis.Remote;
15+
using Microsoft.CodeAnalysis.Shared;
16+
using Microsoft.CodeAnalysis.Shared.Extensions;
1317
using Microsoft.CodeAnalysis.Shared.Utilities;
18+
using Microsoft.CodeAnalysis.Text;
1419
using Microsoft.CodeAnalysis.Threading;
1520
using Roslyn.Utilities;
1621

@@ -36,16 +41,16 @@ public async Task SearchDocumentAsync(
3641
await client.TryInvokeAsync<IRemoteNavigateToSearchService>(
3742
document.Project,
3843
(service, solutionInfo, callbackId, cancellationToken) =>
39-
service.SearchDocumentAsync(solutionInfo, document.Id, searchPattern, [.. kinds], callbackId, cancellationToken),
44+
service.SearchDocumentAndRelatedDocumentsAsync(solutionInfo, document.Id, searchPattern, [.. kinds], callbackId, cancellationToken),
4045
callback, cancellationToken).ConfigureAwait(false);
4146

4247
return;
4348
}
4449

45-
await SearchDocumentInCurrentProcessAsync(document, searchPattern, kinds, onItemsFound, cancellationToken).ConfigureAwait(false);
50+
await SearchDocumentAndRelatedDocumentsInCurrentProcessAsync(document, searchPattern, kinds, onItemsFound, cancellationToken).ConfigureAwait(false);
4651
}
4752

48-
public static async Task SearchDocumentInCurrentProcessAsync(
53+
public static async Task SearchDocumentAndRelatedDocumentsInCurrentProcessAsync(
4954
Document document,
5055
string searchPattern,
5156
IImmutableSet<string> kinds,
@@ -55,12 +60,85 @@ public static async Task SearchDocumentInCurrentProcessAsync(
5560
var (patternName, patternContainerOpt) = PatternMatcher.GetNameAndContainer(searchPattern);
5661
var declaredSymbolInfoKindsSet = new DeclaredSymbolInfoKindSet(kinds);
5762

58-
var results = new ConcurrentSet<RoslynNavigateToItem>();
59-
await SearchSingleDocumentAsync(
60-
document, patternName, patternContainerOpt, declaredSymbolInfoKindsSet, t => results.Add(t), cancellationToken).ConfigureAwait(false);
63+
// In parallel, search both the document requested, and any relevant 'related documents' we find for it. For the
64+
// original document, search the entirety of it (by passing 'null' in for the 'spans' argument). For related
65+
// documents, only search the spans of the partial-types/inheriting-types that we find for the types in this
66+
// starting document.
67+
await Task.WhenAll(
68+
SearchDocumentsInCurrentProcessAsync([(document, spans: null)]),
69+
SearchRelatedDocumentsInCurrentProcessAsync()).ConfigureAwait(false);
70+
71+
Task SearchDocumentsInCurrentProcessAsync(ImmutableArray<(Document document, NormalizedTextSpanCollection? spans)> documentAndSpans)
72+
=> ProducerConsumer<RoslynNavigateToItem>.RunParallelAsync(
73+
documentAndSpans,
74+
produceItems: static async (documentAndSpan, onItemFound, args, cancellationToken) =>
75+
{
76+
var (patternName, patternContainerOpt, declaredSymbolInfoKindsSet, onItemsFound) = args;
77+
await SearchSingleDocumentAsync(
78+
documentAndSpan.document, patternName, patternContainerOpt, declaredSymbolInfoKindsSet,
79+
item =>
80+
{
81+
// Ensure that the results found while searching the single document intersect the desired
82+
// subrange of the document we're searching in. For the primary document this will always
83+
// succeed (since we're searching the full document). But for related documents this may fail
84+
// if the results is not in the span of any of the types in those files we're searching.
85+
if (documentAndSpan.spans is null || documentAndSpan.spans.IntersectsWith(item.DeclaredSymbolInfo.Span))
86+
onItemFound(item);
87+
},
88+
cancellationToken).ConfigureAwait(false);
89+
},
90+
consumeItems: static (values, args, cancellationToken) => args.onItemsFound(values, default, cancellationToken),
91+
args: (patternName, patternContainerOpt, declaredSymbolInfoKindsSet, onItemsFound),
92+
cancellationToken);
93+
94+
async Task SearchRelatedDocumentsInCurrentProcessAsync()
95+
{
96+
var relatedDocuments = await GetRelatedDocumentsAsync().ConfigureAwait(false);
97+
await SearchDocumentsInCurrentProcessAsync(relatedDocuments).ConfigureAwait(false);
98+
}
6199

62-
if (results.Count > 0)
63-
await onItemsFound([.. results], default, cancellationToken).ConfigureAwait(false);
100+
async Task<ImmutableArray<(Document document, NormalizedTextSpanCollection? spans)>> GetRelatedDocumentsAsync()
101+
{
102+
// For C#/VB we define 'related documents' as those containing types in the inheritance chain of types in
103+
// the originating file (as well as all partial parts of the original and inheritance types). This way a
104+
// user can search for symbols scoped to the 'current document' and still get results for the members found
105+
// in partial parts.
106+
107+
var solution = document.Project.Solution;
108+
var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
109+
var semanticModel = await document.GetRequiredNullableDisabledSemanticModelAsync(cancellationToken).ConfigureAwait(false);
110+
var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
111+
112+
using var _ = ArrayBuilder<SyntaxNode>.GetInstance(out var topLevelNodes);
113+
syntaxFacts.AddTopLevelMembers(root, topLevelNodes);
114+
115+
// Keep track of all of the interesting spans in each document we find. Note: we will convert this to a
116+
// NormalizedTextSpanCollection before returning it. That way the span of an outer partial type will
117+
// encompass the span of an inner one and we won't get duplicates for the same symbol.
118+
var documentToTextSpans = new MultiDictionary<Document, TextSpan>();
119+
120+
foreach (var topLevelMember in topLevelNodes)
121+
{
122+
if (semanticModel.GetDeclaredSymbol(topLevelMember, cancellationToken) is not INamedTypeSymbol namedTypeSymbol)
123+
continue;
124+
125+
foreach (var type in namedTypeSymbol.GetBaseTypesAndThis())
126+
{
127+
foreach (var reference in type.DeclaringSyntaxReferences)
128+
{
129+
var relatedDocument = solution.GetDocument(reference.SyntaxTree);
130+
if (relatedDocument is null)
131+
continue;
132+
133+
documentToTextSpans.Add(relatedDocument, reference.Span);
134+
}
135+
}
136+
}
137+
138+
// Ensure we don't search the original document we were already searching.
139+
documentToTextSpans.Remove(document);
140+
return documentToTextSpans.SelectAsArray(kvp => (kvp.Key, new NormalizedTextSpanCollection(kvp.Value)))!;
141+
}
64142
}
65143

66144
public async Task SearchProjectsAsync(

src/Features/Core/Portable/NavigateTo/IRemoteNavigateToSearchService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ namespace Microsoft.CodeAnalysis.NavigateTo;
1818

1919
internal interface IRemoteNavigateToSearchService
2020
{
21-
ValueTask SearchDocumentAsync(Checksum solutionChecksum, DocumentId documentId, string searchPattern, ImmutableArray<string> kinds, RemoteServiceCallbackId callbackId, CancellationToken cancellationToken);
21+
ValueTask SearchDocumentAndRelatedDocumentsAsync(Checksum solutionChecksum, DocumentId documentId, string searchPattern, ImmutableArray<string> kinds, RemoteServiceCallbackId callbackId, CancellationToken cancellationToken);
2222
ValueTask SearchProjectsAsync(Checksum solutionChecksum, ImmutableArray<ProjectId> projectIds, ImmutableArray<DocumentId> priorityDocumentIds, string searchPattern, ImmutableArray<string> kinds, RemoteServiceCallbackId callbackId, CancellationToken cancellationToken);
2323

2424
ValueTask SearchGeneratedDocumentsAsync(Checksum solutionChecksum, ImmutableArray<ProjectId> projectIds, string searchPattern, ImmutableArray<string> kinds, RemoteServiceCallbackId callbackId, CancellationToken cancellationToken);

src/Features/VisualBasic/Portable/NavigateTo/VisualBasicNavigateToSearchService.vb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Imports Microsoft.CodeAnalysis.NavigateTo
88

99
Namespace Microsoft.CodeAnalysis.VisualBasic.NavigateTo
1010
<ExportLanguageService(GetType(INavigateToSearchService), LanguageNames.VisualBasic), [Shared]>
11-
Friend Class VisualBasicNavigateToSearchService
11+
Friend NotInheritable Class VisualBasicNavigateToSearchService
1212
Inherits AbstractNavigateToSearchService
1313

1414
<ImportingConstructor>

src/Workspaces/Remote/ServiceHub/Services/NavigateToSearch/RemoteNavigateToSearchService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public ValueTask HydrateAsync(Checksum solutionChecksum, CancellationToken cance
4545
return RunServiceAsync(solutionChecksum, solution => ValueTaskFactory.CompletedTask, cancellationToken);
4646
}
4747

48-
public ValueTask SearchDocumentAsync(
48+
public ValueTask SearchDocumentAndRelatedDocumentsAsync(
4949
Checksum solutionChecksum,
5050
DocumentId documentId,
5151
string searchPattern,
@@ -58,7 +58,7 @@ public ValueTask SearchDocumentAsync(
5858
var document = solution.GetRequiredDocument(documentId);
5959
var (onItemsFound, onProjectCompleted) = GetCallbacks(callbackId, cancellationToken);
6060

61-
await AbstractNavigateToSearchService.SearchDocumentInCurrentProcessAsync(
61+
await AbstractNavigateToSearchService.SearchDocumentAndRelatedDocumentsInCurrentProcessAsync(
6262
document, searchPattern, kinds.ToImmutableHashSet(), onItemsFound, cancellationToken).ConfigureAwait(false);
6363
}, cancellationToken);
6464
}

0 commit comments

Comments
 (0)