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

Implement Quick Info styles #35667

Merged
merged 10 commits into from
Jul 25, 2019
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;
Copy link
Member

Choose a reason for hiding this comment

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

could we consider extracting out everything above (into something like StartContainerElement(...))? I thnk it would make it much easier to understand things if we just had a top level lopo, with the high level decision making, and then have the individual functions that are responsible for each piece of work. intermingling them makes it harder to keep track and understand what's going on for me.

}
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++;
}
sharwell marked this conversation as resolved.
Show resolved Hide resolved

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;
Copy link
Member

Choose a reason for hiding this comment

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

def not liking this.

  1. symbolkey shoudn't be throwing.
  2. not happy about all this sync blocking. can we not be async here?

Worst case, when someone navigates, we can do a TWD and make it cancellable if something goes off the rails.

Copy link
Member Author

@sharwell sharwell May 14, 2019

Choose a reason for hiding this comment

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

not happy about all this sync blocking. can we not be async here?

Limited by the editor API. @gundermanc how would you handle this?

Copy link
Member

Choose a reason for hiding this comment

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

note: sync blocking is ok with me. it's more the lack of cancellability. That should hopefully be something we can fix up with a TWD.

Copy link
Member

Choose a reason for hiding this comment

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

I think the only option is to do whatever the equivalent of a Dispatcher.InvokeAsync() is in this layer. From the IDE side, we usually use JTF.RunAsync(). I understand this is the portable editor features, so, there likely is no dispatcher.

For my own edification, would it be preferable to have actions like this be async? I didn't think it was necessary originally because we're de facto going to have to InvokeAsync() somewhere but I acknowledge things are a bit more constrained for Roslyn since this layer has to work on Mac.

Copy link
Member

Choose a reason for hiding this comment

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

I'd be fine with this being async (which honestly makes sense to me given that any of thse clicks might cause major work to happen). Or, if sync, at least have it be under a TWD.

}

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;
sharwell marked this conversation as resolved.
Show resolved Hide resolved
}

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