|
// 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.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.DocumentHighlighting;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.FindUsages;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Shell.FindAllReferences;
using Microsoft.VisualStudio.Shell.TableControl;
using Roslyn.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.FindUsages;
internal partial class StreamingFindUsagesPresenter
{
/// <summary>
/// Context to be used for FindAllReferences (as opposed to FindImplementations/GoToDef).
/// This context supports showing reference items, and will display appropriate messages
/// about no-references being found for a definition at the end of the search.
/// </summary>
private sealed class WithReferencesFindUsagesContext(
StreamingFindUsagesPresenter presenter,
IFindAllReferencesWindow findReferencesWindow,
ImmutableArray<ITableColumnDefinition> customColumns,
IGlobalOptionService globalOptions,
bool includeContainingTypeAndMemberColumns,
bool includeKindColumn,
IThreadingContext threadingContext)
: AbstractTableDataSourceFindUsagesContext(
presenter,
findReferencesWindow,
customColumns,
globalOptions,
includeContainingTypeAndMemberColumns,
includeKindColumn,
threadingContext)
{
protected override async ValueTask OnDefinitionFoundWorkerAsync(DefinitionItem definition, CancellationToken cancellationToken)
{
// If this is a definition we always want to show, then create entries for all the declaration locations
// immediately. Otherwise, we'll create them on demand when we hear about references for this
// definition.
if (definition.DisplayIfNoReferences)
await AddDeclarationEntriesAsync(definition, expandedByDefault: true, cancellationToken).ConfigureAwait(false);
}
private async Task AddDeclarationEntriesAsync(DefinitionItem definition, bool expandedByDefault, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
// Don't do anything if we already have declaration entries for this definition
// (i.e. another thread beat us to this).
if (HasDeclarationEntries(definition))
{
return;
}
var definitionBucket = GetOrCreateDefinitionBucket(definition, expandedByDefault);
// We could do this inside the lock. but that would mean async activity in a
// lock, and I'd like to avoid that. That does mean that we might do extra
// work if multiple threads end up down this path. But only one of them will
// win when we access the lock below.
using var _1 = ArrayBuilder<Entry>.GetInstance(out var entries);
using var _2 = PooledHashSet<(string? filePath, TextSpan span)>.GetInstance(out var seenLocations);
await AddDocumentSpanEntriesAsync(entries, definitionBucket, definition, cancellationToken).ConfigureAwait(false);
var changed = false;
var isPrimary = IsPrimary(definition);
lock (Gate)
{
// Do one final check to ensure that no other thread beat us here.
if (!HasDeclarationEntries(definition))
{
// We only include declaration entries in the entries we show when
// not grouping by definition.
AddRange(EntriesWhenNotGroupingByDefinition, entries, isPrimary);
CurrentVersionNumber++;
changed = true;
}
}
if (changed)
{
// Let all our subscriptions know that we've updated.
NotifyChange();
}
}
private bool HasDeclarationEntries(DefinitionItem definition)
{
lock (Gate)
{
foreach (var entry in EntriesWhenNotGroupingByDefinition.primary)
{
if (entry.DefinitionBucket.DefinitionItem == definition)
return true;
}
foreach (var entry in EntriesWhenNotGroupingByDefinition.nonPrimary)
{
if (entry.DefinitionBucket.DefinitionItem == definition)
return true;
}
return false;
}
}
protected override ValueTask OnReferenceFoundWorkerAsync(SourceReferenceItem reference, CancellationToken cancellationToken)
{
// Normal references go into both sets of entries. We ensure an entry for the definition, and an entry
// for the reference itself.
return OnEntryFoundAsync(
reference.Definition,
bucket => TryCreateDocumentSpanEntryAsync(
bucket, reference.SourceSpan, reference.ClassifiedSpans,
reference.IsWrittenTo ? HighlightSpanKind.WrittenReference : HighlightSpanKind.Reference,
reference.SymbolUsageInfo,
reference.AdditionalProperties,
cancellationToken),
addToEntriesWhenGroupingByDefinition: true,
addToEntriesWhenNotGroupingByDefinition: true,
expandedByDefault: true,
cancellationToken);
}
private async ValueTask OnEntryFoundAsync(
DefinitionItem definition,
Func<RoslynDefinitionBucket, Task<Entry?>> createEntryAsync,
bool addToEntriesWhenGroupingByDefinition,
bool addToEntriesWhenNotGroupingByDefinition,
bool expandedByDefault,
CancellationToken cancellationToken)
{
Debug.Assert(addToEntriesWhenGroupingByDefinition || addToEntriesWhenNotGroupingByDefinition);
cancellationToken.ThrowIfCancellationRequested();
// OK, we got a *reference* to some definition item. This may have been a reference for some definition
// that we haven't created any declaration entries for (i.e. because it had DisplayIfNoReferences =
// false). Because we've now found a reference, we want to make sure all its declaration entries are
// added.
await AddDeclarationEntriesAsync(definition, expandedByDefault, cancellationToken).ConfigureAwait(false);
// First find the bucket corresponding to our definition.
var definitionBucket = GetOrCreateDefinitionBucket(definition, expandedByDefault);
var entry = await createEntryAsync(definitionBucket).ConfigureAwait(false);
// Proceed, even if we didn't create an entry. It's possible that we augmented
// an existing entry and we want the UI to refresh to show the results of that.
var isPrimary = IsPrimary(definition);
lock (Gate)
{
if (entry != null)
{
// Once we can make the new entry, add it to the appropriate list.
if (addToEntriesWhenGroupingByDefinition)
Add(EntriesWhenGroupingByDefinition, entry, isPrimary);
if (addToEntriesWhenNotGroupingByDefinition)
Add(EntriesWhenNotGroupingByDefinition, entry, isPrimary);
}
CurrentVersionNumber++;
}
// Let all our subscriptions know that we've updated.
NotifyChange();
}
protected override async Task OnCompletedAsyncWorkerAsync(CancellationToken cancellationToken)
{
// Now that we know the search is over, create and display any error messages
// for definitions that were not found.
await CreateMissingReferenceEntriesIfNecessaryAsync(cancellationToken).ConfigureAwait(false);
await CreateNoResultsFoundEntryIfNecessaryAsync(cancellationToken).ConfigureAwait(false);
}
private async Task CreateMissingReferenceEntriesIfNecessaryAsync(CancellationToken cancellationToken)
{
await CreateMissingReferenceEntriesIfNecessaryAsync(whenGroupingByDefinition: true, cancellationToken).ConfigureAwait(false);
await CreateMissingReferenceEntriesIfNecessaryAsync(whenGroupingByDefinition: false, cancellationToken).ConfigureAwait(false);
}
private async Task CreateMissingReferenceEntriesIfNecessaryAsync(
bool whenGroupingByDefinition, CancellationToken cancellationToken)
{
// Go through and add dummy entries for any definitions that
// that we didn't find any references for.
var definitions = GetDefinitionsToCreateMissingReferenceItemsFor(whenGroupingByDefinition);
foreach (var definition in definitions)
{
if (definition.IsExternal)
{
await OnEntryFoundAsync(
definition,
bucket => SimpleMessageEntry.CreateAsync(bucket, bucket, ServicesVSResources.External_reference_found)!,
addToEntriesWhenGroupingByDefinition: whenGroupingByDefinition,
addToEntriesWhenNotGroupingByDefinition: !whenGroupingByDefinition,
expandedByDefault: true,
cancellationToken).ConfigureAwait(false);
}
else
{
// Create a fake reference to this definition that says "no references found to <symbolname>".
//
// We'll place this under a single bucket called "Symbols without references" and we'll allow
// the user to navigate on that text entry to that definition if possible.
await OnEntryFoundAsync(
SymbolsWithoutReferencesDefinitionItem,
bucket => SimpleMessageEntry.CreateAsync(
definitionBucket: bucket,
navigationBucket: RoslynDefinitionBucket.Create(Presenter, this, definition, expandedByDefault: false, this.ThreadingContext),
string.Format(ServicesVSResources.No_references_found_to_0, definition.NameDisplayParts.JoinText()))!,
addToEntriesWhenGroupingByDefinition: whenGroupingByDefinition,
addToEntriesWhenNotGroupingByDefinition: !whenGroupingByDefinition,
expandedByDefault: false,
cancellationToken).ConfigureAwait(false);
}
}
}
private ImmutableArray<DefinitionItem> GetDefinitionsToCreateMissingReferenceItemsFor(
bool whenGroupingByDefinition)
{
lock (Gate)
{
var (primary, nonPrimary) = whenGroupingByDefinition
? EntriesWhenGroupingByDefinition
: EntriesWhenNotGroupingByDefinition;
// Find any definitions that we didn't have any references to. But only show
// them if they want to be displayed without any references. This will
// ensure that we still see things like overrides and whatnot, but we
// won't show property-accessors.
var seenDefinitions = primary.Concat(nonPrimary)
.Select(r => r.DefinitionBucket.DefinitionItem)
.ToSet();
var q = from definition in Definitions
where !seenDefinitions.Contains(definition) &&
definition.DisplayIfNoReferences
select definition;
// If we find at least one of these types of definitions, then just return those.
var result = ImmutableArray.CreateRange(q);
if (result.Length > 0)
return result;
// We found no definitions that *want* to be displayed. However, we still want to show something. So,
// if necessary, show at lest the first definition even if we found no references and even if it would
// prefer to not be seen.
if (primary.Count == 0 && nonPrimary.Count == 0 && Definitions.Count > 0)
return [Definitions.First()];
return [];
}
}
private async Task CreateNoResultsFoundEntryIfNecessaryAsync(CancellationToken cancellationToken)
{
string message;
lock (Gate)
{
// If we got definitions, then no need to show the 'no results found' message.
if (this.Definitions.Count > 0)
return;
message = NoDefinitionsFoundMessage;
}
// Create a fake definition/reference called "search found no results"
await OnEntryFoundAsync(
CreateNoResultsDefinitionItem(message),
bucket => SimpleMessageEntry.CreateAsync(bucket, navigationBucket: null, message)!,
addToEntriesWhenGroupingByDefinition: true,
addToEntriesWhenNotGroupingByDefinition: true,
expandedByDefault: true,
cancellationToken).ConfigureAwait(false);
}
private static readonly DefinitionItem SymbolsWithoutReferencesDefinitionItem =
DefinitionItem.CreateNonNavigableItem(
GlyphTags.GetTags(Glyph.StatusInformation),
[new TaggedText(
TextTags.Text,
ServicesVSResources.Symbols_without_references)]);
}
}
|