Skip to content

Commit

Permalink
Merge pull request #35667 from sharwell/quickinfo-styles
Browse files Browse the repository at this point in the history
Implement Quick Info styles
  • Loading branch information
sharwell authored Jul 25, 2019
2 parents 83eac99 + bcb448a commit a90f658
Show file tree
Hide file tree
Showing 25 changed files with 1,590 additions and 346 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ internal static class DocumentationCommentXmlNames
public const string ValueElementName = "value";

public const string CrefAttributeName = "cref";
public const string HrefAttributeName = "href";
public const string FileAttributeName = "file";
public const string InstanceAttributeName = "instance";
public const string LangwordAttributeName = "langword";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.QuickInfo;
using Microsoft.CodeAnalysis.Editor.Host;
using Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo;
using Microsoft.CodeAnalysis.Editor.UnitTests.QuickInfo;
using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
Expand Down Expand Up @@ -291,7 +292,8 @@ protected override async Task AssertContentIsAsync(
Assert.NotEqual(0, info.RelatedSpans.Length);

var trackingSpan = new Mock<ITrackingSpan>(MockBehavior.Strict);
var quickInfoItem = await IntellisenseQuickInfoBuilder.BuildItemAsync(trackingSpan.Object, info, snapshot, document, CancellationToken.None);
var streamingPresenter = workspace.ExportProvider.GetExport<IStreamingFindUsagesPresenter>();
var quickInfoItem = await IntellisenseQuickInfoBuilder.BuildItemAsync(trackingSpan.Object, info, snapshot, document, streamingPresenter, CancellationToken.None);
var containerElement = quickInfoItem.Item as ContainerElement;

var textElements = containerElement.Elements.OfType<ClassifiedTextElement>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.Implementation
{
[ExportWorkspaceService(typeof(INavigateToLinkService), layer: ServiceLayer.Default)]
[Shared]
internal sealed class DefaultNavigateToLinkService : INavigateToLinkService
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public DefaultNavigateToLinkService()
{
}

public Task<bool> TryNavigateToLinkAsync(Uri uri, CancellationToken cancellationToken)
=> SpecializedTasks.False;
}
}
14 changes: 14 additions & 0 deletions src/EditorFeatures/Core/Implementation/INavigateToLinkService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;

namespace Microsoft.CodeAnalysis.Editor
{
internal interface INavigateToLinkService : IWorkspaceService
{
Task<bool> TryNavigateToLinkAsync(Uri uri, CancellationToken cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
Expand All @@ -8,6 +9,7 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Completion.Providers;
using Microsoft.CodeAnalysis.Editor.Host;
using Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.Completion;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
Expand Down Expand Up @@ -61,10 +63,13 @@ internal class CompletionSource : ForegroundThreadAffinitizedObject, IAsyncCompl
private readonly ITextView _textView;
private readonly bool _isDebuggerTextView;
private readonly ImmutableHashSet<string> _roles;
private readonly Lazy<IStreamingFindUsagesPresenter> _streamingPresenter;

internal CompletionSource(ITextView textView, IThreadingContext threadingContext) : base(threadingContext)
internal CompletionSource(ITextView textView, Lazy<IStreamingFindUsagesPresenter> streamingPresenter, IThreadingContext threadingContext)
: base(threadingContext)
{
_textView = textView;
_streamingPresenter = streamingPresenter;
_isDebuggerTextView = textView is IDebuggerTextView;
_roles = textView.Roles.ToImmutableHashSet();
}
Expand Down Expand Up @@ -294,15 +299,14 @@ public async Task<object> GetDescriptionAsync(IAsyncCompletionSession session, V
}

var service = document.GetLanguageService<CompletionService>();

if (service == null)
{
return null;
}

var description = await service.GetDescriptionAsync(document, roslynItem, cancellationToken).ConfigureAwait(false);

var elements = IntelliSense.Helpers.BuildClassifiedTextElements(description.TaggedParts).ToArray();
var elements = IntelliSense.Helpers.BuildInteractiveTextElements(description.TaggedParts, document, _streamingPresenter).ToArray();
if (elements.Length == 0)
{
return new ClassifiedTextElement();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using System;
using System.ComponentModel.Composition;
using Microsoft.CodeAnalysis.Editor.Host;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion;
Expand All @@ -16,15 +17,19 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncComplet
internal class CompletionSourceProvider : IAsyncCompletionSourceProvider
{
private readonly IThreadingContext _threadingContext;
private readonly Lazy<IStreamingFindUsagesPresenter> _streamingPresenter;

[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public CompletionSourceProvider(IThreadingContext threadingContext)
public CompletionSourceProvider(
IThreadingContext threadingContext,
Lazy<IStreamingFindUsagesPresenter> streamingPresenter)
{
_threadingContext = threadingContext;
_streamingPresenter = streamingPresenter;
}

public IAsyncCompletionSource GetOrCreate(ITextView textView)
=> new CompletionSource(textView, _threadingContext);
=> new CompletionSource(textView, _streamingPresenter, _threadingContext);
}
}
135 changes: 130 additions & 5 deletions src/EditorFeatures/Core/Implementation/IntelliSense/Helpers.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,87 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Editor.GoToDefinition;
using Microsoft.CodeAnalysis.Editor.Host;
using Microsoft.VisualStudio.Text.Adornments;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense
{
internal static class Helpers
{
internal static IEnumerable<object> BuildClassifiedTextElements(ImmutableArray<TaggedText> taggedTexts)
internal static IReadOnlyCollection<object> BuildInteractiveTextElements(ImmutableArray<TaggedText> taggedTexts, Document document, Lazy<IStreamingFindUsagesPresenter> streamingPresenter)
{
var index = 0;
return BuildInteractiveTextElements(taggedTexts, ref index, document, streamingPresenter);
}

private static IReadOnlyCollection<object> BuildInteractiveTextElements(ImmutableArray<TaggedText> taggedTexts, ref int index, Document document, Lazy<IStreamingFindUsagesPresenter> streamingPresenter)
{
// 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<ClassifiedTextElement>();
var currentParagraph = new List<object>();

// Each line is constructed from one or more inline elements
var currentRuns = new List<ClassifiedTextRun>();

foreach (var part in taggedTexts)
while (index < taggedTexts.Length)
{
var part = taggedTexts[index];
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, document, streamingPresenter);
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 == TextTags.ContainerStart
|| part.Tag == TextTags.ContainerEnd)
{
index++;
continue;
}

if (part.Tag == TextTags.LineBreak)
{
if (currentRuns.Count > 0)
Expand Down Expand Up @@ -53,8 +113,20 @@ internal static IEnumerable<object> BuildClassifiedTextElements(ImmutableArray<T
else
{
// This is tagged text getting added to the current line we are building.
currentRuns.Add(new ClassifiedTextRun(part.Tag.ToClassificationTypeName(), part.Text));
var style = GetClassifiedTextRunStyle(part.Style);
if (part.NavigationTarget is object)
{
var target = part.NavigationTarget;
var tooltip = part.NavigationHint;
currentRuns.Add(new ClassifiedTextRun(part.Tag.ToClassificationTypeName(), part.Text, () => NavigateToQuickInfoTarget(target, document, streamingPresenter.Value), tooltip, style));
}
else
{
currentRuns.Add(new ClassifiedTextRun(part.Tag.ToClassificationTypeName(), part.Text, style));
}
}

index++;
}

if (currentRuns.Count > 0)
Expand All @@ -72,7 +144,60 @@ internal static IEnumerable<object> BuildClassifiedTextElements(ImmutableArray<T
return paragraphs;
}

internal static object CreateParagraphFromLines(IReadOnlyList<ClassifiedTextElement> lines)
private static void NavigateToQuickInfoTarget(string navigationTarget, Document document, IStreamingFindUsagesPresenter streamingPresenter)
{
var navigateToLinkService = document.Project.Solution.Workspace.Services.GetRequiredService<INavigateToLinkService>();
if (Uri.TryCreate(navigationTarget, UriKind.Absolute, out var absoluteUri))
{
navigateToLinkService.TryNavigateToLinkAsync(absoluteUri, CancellationToken.None);
return;
}

SymbolKeyResolution resolvedSymbolKey;
try
{
resolvedSymbolKey = SymbolKey.ResolveString(navigationTarget, document.Project.GetCompilationAsync(CancellationToken.None).WaitAndGetResult(CancellationToken.None), cancellationToken: CancellationToken.None);
}
catch
{
// Ignore symbol resolution failures. It likely is just a badly formed URI.
return;
}

if (resolvedSymbolKey.GetAnySymbol() is { } symbol)
{
GoToDefinitionHelpers.TryGoToDefinition(symbol, document.Project, streamingPresenter, CancellationToken.None);
return;
}
}

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);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Editor.Host;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.QuickInfo;
Expand All @@ -24,6 +26,7 @@ internal static async Task<IntellisenseQuickInfoItem> BuildItemAsync(ITrackingSp
CodeAnalysisQuickInfoItem quickInfoItem,
ITextSnapshot snapshot,
Document document,
Lazy<IStreamingFindUsagesPresenter> streamingPresenter,
CancellationToken cancellationToken)
{
// Build the first line of QuickInfo item, the images and the Description section should be on the first line with Wrapped style
Expand All @@ -46,7 +49,7 @@ internal static async Task<IntellisenseQuickInfoItem> BuildItemAsync(ITrackingSp
if (descSection != null)
{
var isFirstElement = true;
foreach (var element in Helpers.BuildClassifiedTextElements(descSection.TaggedParts))
foreach (var element in Helpers.BuildInteractiveTextElements(descSection.TaggedParts, document, streamingPresenter))
{
if (isFirstElement)
{
Expand All @@ -68,7 +71,7 @@ internal static async Task<IntellisenseQuickInfoItem> BuildItemAsync(ITrackingSp
if (documentationCommentSection != null)
{
var isFirstElement = true;
foreach (var element in Helpers.BuildClassifiedTextElements(documentationCommentSection.TaggedParts))
foreach (var element in Helpers.BuildInteractiveTextElements(documentationCommentSection.TaggedParts, document, streamingPresenter))
{
if (isFirstElement)
{
Expand All @@ -92,7 +95,7 @@ internal static async Task<IntellisenseQuickInfoItem> BuildItemAsync(ITrackingSp
// Add the remaining sections as Stacked style
elements.AddRange(
quickInfoItem.Sections.Where(s => s.Kind != QuickInfoSectionKinds.Description && s.Kind != QuickInfoSectionKinds.DocumentationComments)
.SelectMany(s => Helpers.BuildClassifiedTextElements(s.TaggedParts)));
.SelectMany(s => Helpers.BuildInteractiveTextElements(s.TaggedParts, document, streamingPresenter)));

// build text for RelatedSpan
if (quickInfoItem.RelatedSpans.Any())
Expand All @@ -107,7 +110,7 @@ internal static async Task<IntellisenseQuickInfoItem> BuildItemAsync(ITrackingSp
var tabSize = document.Project.Solution.Workspace.Options.GetOption(FormattingOptions.TabSize, document.Project.Language);
var text = await document.GetTextAsync().ConfigureAwait(false);
var spans = IndentationHelper.GetSpansWithAlignedIndentation(text, classifiedSpanList.ToImmutableArray(), tabSize);
var textRuns = spans.Select(s => new ClassifiedTextRun(s.ClassificationType, snapshot.GetText(s.TextSpan.ToSpan())));
var textRuns = spans.Select(s => new ClassifiedTextRun(s.ClassificationType, snapshot.GetText(s.TextSpan.ToSpan()), ClassifiedTextRunStyle.UseClassificationFont));

if (textRuns.Any())
{
Expand Down
Loading

0 comments on commit a90f658

Please sign in to comment.