|
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Editor.Host;
using Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.GoToDefinition;
using Microsoft.CodeAnalysis.Navigation;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.Text.Adornments;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense
{
internal static class Helpers
{
internal static IReadOnlyCollection<object> BuildInteractiveTextElements(
ImmutableArray<TaggedText> taggedTexts,
IntellisenseQuickInfoBuilderContext? context)
{
var index = 0;
return BuildInteractiveTextElements(taggedTexts, ref index, context);
}
private static IReadOnlyCollection<object> BuildInteractiveTextElements(
ImmutableArray<TaggedText> taggedTexts,
ref int index,
IntellisenseQuickInfoBuilderContext? context)
{
// This method produces a sequence of zero or more paragraphs
var paragraphs = new List<object>();
// Each paragraph is constructed from one or more lines
var currentParagraph = new List<object>();
// Each line is constructed from one or more inline elements
var currentRuns = new List<ClassifiedTextRun>();
while (index < taggedTexts.Length)
{
var part = taggedTexts[index];
// These tags can be ignored - they are for markdown formatting only.
if (part.Tag is TextTags.CodeBlockStart or TextTags.CodeBlockEnd)
{
index++;
continue;
}
if (part.Tag == TextTags.ContainerStart)
{
if (currentRuns.Count > 0)
{
// This line break means the end of a line within a paragraph.
currentParagraph.Add(new ClassifiedTextElement(currentRuns));
currentRuns.Clear();
}
index++;
var nestedElements = BuildInteractiveTextElements(taggedTexts, ref index, context);
if (nestedElements.Count <= 1)
{
currentParagraph.Add(new ContainerElement(
ContainerElementStyle.Wrapped,
new ClassifiedTextElement(new ClassifiedTextRun(ClassificationTypeNames.Text, part.Text)),
new ContainerElement(ContainerElementStyle.Stacked, nestedElements)));
}
else
{
currentParagraph.Add(new ContainerElement(
ContainerElementStyle.Wrapped,
new ClassifiedTextElement(new ClassifiedTextRun(ClassificationTypeNames.Text, part.Text)),
new ContainerElement(
ContainerElementStyle.Stacked,
nestedElements.First(),
new ContainerElement(
ContainerElementStyle.Stacked | ContainerElementStyle.VerticalPadding,
nestedElements.Skip(1)))));
}
index++;
continue;
}
else if (part.Tag == TextTags.ContainerEnd)
{
// Return the current result and let the caller continue
break;
}
if (part.Tag is TextTags.ContainerStart
or TextTags.ContainerEnd)
{
index++;
continue;
}
if (part.Tag == TextTags.LineBreak)
{
if (currentRuns.Count > 0)
{
// This line break means the end of a line within a paragraph.
currentParagraph.Add(new ClassifiedTextElement(currentRuns));
currentRuns.Clear();
}
else
{
// This line break means the end of a paragraph. Empty paragraphs are ignored, but could appear
// in the input to this method:
//
// * Empty <para> elements
// * Explicit line breaks at the start of a comment
// * Multiple line breaks between paragraphs
if (currentParagraph.Count > 0)
{
// The current paragraph is not empty, so add it to the result collection
paragraphs.Add(CreateParagraphFromLines(currentParagraph));
currentParagraph.Clear();
}
else
{
// The current paragraph is empty, so we simply ignore it.
}
}
}
else
{
// This is tagged text getting added to the current line we are building.
var style = GetClassifiedTextRunStyle(part.Style);
if (part.NavigationTarget is not null &&
context?.ThreadingContext is { } threadingContext &&
context?.OperationExecutor is { } operationExecutor &&
context?.AsynchronousOperationListener is { } asyncListener &&
context?.StreamingPresenter is { } streamingPresenter)
{
var document = context.Document;
if (Uri.TryCreate(part.NavigationTarget, UriKind.Absolute, out var absoluteUri))
{
var target = new QuickInfoHyperLink(document.Project.Solution.Workspace, absoluteUri);
var tooltip = part.NavigationHint;
currentRuns.Add(new ClassifiedTextRun(part.Tag.ToClassificationTypeName(), part.Text, target.NavigationAction, tooltip, style));
}
else
{
// ⚠ PERF: avoid capturing Solution (including indirectly through Project or Document
// instances) as part of the navigationAction delegate.
var target = part.NavigationTarget;
var tooltip = part.NavigationHint;
var documentId = document.Id;
var workspace = document.Project.Solution.Workspace;
currentRuns.Add(new ClassifiedTextRun(
part.Tag.ToClassificationTypeName(), part.Text,
() => _ = NavigateToQuickInfoTargetAsync(target, workspace, documentId, threadingContext, operationExecutor, asyncListener, streamingPresenter.Value),
tooltip, style));
}
}
else
{
currentRuns.Add(new ClassifiedTextRun(part.Tag.ToClassificationTypeName(), part.Text, style));
}
}
index++;
}
if (currentRuns.Count > 0)
{
// Add the final line to the final paragraph.
currentParagraph.Add(new ClassifiedTextElement(currentRuns));
}
if (currentParagraph.Count > 0)
{
// Add the final paragraph to the result.
paragraphs.Add(CreateParagraphFromLines(currentParagraph));
}
return paragraphs;
}
private static async Task NavigateToQuickInfoTargetAsync(
string navigationTarget,
Workspace workspace,
DocumentId documentId,
IThreadingContext threadingContext,
IUIThreadOperationExecutor operationExecutor,
IAsynchronousOperationListener asyncListener,
IStreamingFindUsagesPresenter streamingPresenter)
{
try
{
using var token = asyncListener.BeginAsyncOperation(nameof(NavigateToQuickInfoTargetAsync));
using var context = operationExecutor.BeginExecute(EditorFeaturesResources.IntelliSense, EditorFeaturesResources.Navigating, allowCancellation: true, showProgress: false);
var cancellationToken = context.UserCancellationToken;
var solution = workspace.CurrentSolution;
SymbolKeyResolution resolvedSymbolKey;
try
{
var project = solution.GetRequiredProject(documentId.ProjectId);
var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
resolvedSymbolKey = SymbolKey.ResolveString(navigationTarget, compilation, cancellationToken: cancellationToken);
}
catch
{
// Ignore symbol resolution failures. It likely is just a badly formed URI.
return;
}
if (resolvedSymbolKey.GetAnySymbol() is { } symbol)
{
var location = await GoToDefinitionHelpers.GetDefinitionLocationAsync(
symbol, solution, threadingContext, streamingPresenter, cancellationToken).ConfigureAwait(false);
await location.TryNavigateToAsync(threadingContext, new NavigationOptions(PreferProvisionalTab: true, ActivateTab: true), cancellationToken).ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
}
catch (Exception ex) when (FatalError.ReportAndCatch(ex, ErrorSeverity.Critical))
{
}
}
private static ClassifiedTextRunStyle GetClassifiedTextRunStyle(TaggedTextStyle style)
{
var result = ClassifiedTextRunStyle.Plain;
if ((style & TaggedTextStyle.Emphasis) == TaggedTextStyle.Emphasis)
{
result |= ClassifiedTextRunStyle.Italic;
}
if ((style & TaggedTextStyle.Strong) == TaggedTextStyle.Strong)
{
result |= ClassifiedTextRunStyle.Bold;
}
if ((style & TaggedTextStyle.Underline) == TaggedTextStyle.Underline)
{
result |= ClassifiedTextRunStyle.Underline;
}
if ((style & TaggedTextStyle.Code) == TaggedTextStyle.Code)
{
result |= ClassifiedTextRunStyle.UseClassificationFont;
}
return result;
}
internal static object CreateParagraphFromLines(IReadOnlyList<object> lines)
{
Contract.ThrowIfFalse(lines.Count > 0);
if (lines.Count == 1)
{
// The paragraph contains only one line, so it doesn't need to be added to a container. Avoiding the
// wrapping container here also avoids a wrapping element in the WPF elements used for rendering,
// improving efficiency.
return lines[0];
}
else
{
// The lines of a multi-line paragraph are stacked to produce the full paragraph.
return new ContainerElement(ContainerElementStyle.Stacked, lines);
}
}
}
}
|