提交 70ac333a 编写于 作者: S Sam Harwell

Support URL navigation for href in Quick Info

上级 217f1b38
......@@ -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";
......
......@@ -294,15 +294,16 @@ public async Task<object> GetDescriptionAsync(IAsyncCompletionSession session, V
}
var service = document.GetLanguageService<CompletionService>();
if (service == null)
{
return null;
}
var navigateToLinkService = document.Project.Solution.Workspace.Services.GetRequiredService<INavigateToLinkService>();
var description = await service.GetDescriptionAsync(document, roslynItem, cancellationToken).ConfigureAwait(false);
var elements = IntelliSense.Helpers.BuildClassifiedTextElements(description.TaggedParts).ToArray();
var elements = IntelliSense.Helpers.BuildClassifiedTextElements(description.TaggedParts, navigateToLinkService).ToArray();
if (elements.Length == 0)
{
return new ClassifiedTextElement();
......
// 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.VisualStudio.Text.Adornments;
using Roslyn.Utilities;
......@@ -11,13 +13,13 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense
{
internal static class Helpers
{
internal static IEnumerable<object> BuildClassifiedTextElements(ImmutableArray<TaggedText> taggedTexts)
internal static IEnumerable<object> BuildClassifiedTextElements(ImmutableArray<TaggedText> taggedTexts, INavigateToLinkService navigateToLinkService)
{
var index = 0;
return BuildClassifiedTextElements(taggedTexts, ref index);
return BuildClassifiedTextElements(taggedTexts, ref index, navigateToLinkService);
}
private static IReadOnlyCollection<object> BuildClassifiedTextElements(ImmutableArray<TaggedText> taggedTexts, ref int index)
private static IReadOnlyCollection<object> BuildClassifiedTextElements(ImmutableArray<TaggedText> taggedTexts, ref int index, INavigateToLinkService navigateToLinkService)
{
// This method produces a sequence of zero or more paragraphs
var paragraphs = new List<object>();
......@@ -41,7 +43,7 @@ private static IReadOnlyCollection<object> BuildClassifiedTextElements(Immutable
}
index++;
var nestedElements = BuildClassifiedTextElements(taggedTexts, ref index);
var nestedElements = BuildClassifiedTextElements(taggedTexts, ref index, navigateToLinkService);
if (nestedElements.Count <= 1)
{
currentParagraph.Add(new ContainerElement(
......@@ -108,7 +110,15 @@ private static IReadOnlyCollection<object> BuildClassifiedTextElements(Immutable
{
// This is tagged text getting added to the current line we are building.
var style = GetClassifiedTextRunStyle(part.Style);
currentRuns.Add(new ClassifiedTextRun(part.Tag.ToClassificationTypeName(), part.Text, style));
if (part.NavigationTarget is object)
{
var tooltip = part.NavigationTarget;
currentRuns.Add(new ClassifiedTextRun(part.Tag.ToClassificationTypeName(), part.Text, () => NavigateToQuickInfoTarget(tooltip, navigateToLinkService), tooltip, style));
}
else
{
currentRuns.Add(new ClassifiedTextRun(part.Tag.ToClassificationTypeName(), part.Text, style));
}
}
}
......@@ -127,6 +137,15 @@ private static IReadOnlyCollection<object> BuildClassifiedTextElements(Immutable
return paragraphs;
}
private static void NavigateToQuickInfoTarget(string navigationTarget, INavigateToLinkService navigateToLinkService)
{
if (Uri.TryCreate(navigationTarget, UriKind.Absolute, out var absoluteUri))
{
navigateToLinkService.TryNavigateToLinkAsync(absoluteUri, CancellationToken.None);
return;
}
}
private static ClassifiedTextRunStyle GetClassifiedTextRunStyle(TaggedTextStyle style)
{
var result = ClassifiedTextRunStyle.Plain;
......
......@@ -43,10 +43,11 @@ internal static class IntellisenseQuickInfoBuilder
var elements = new List<object>();
var descSection = quickInfoItem.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.Description);
var navigateToLinkService = document.Project.Solution.Workspace.Services.GetRequiredService<INavigateToLinkService>();
if (descSection != null)
{
var isFirstElement = true;
foreach (var element in Helpers.BuildClassifiedTextElements(descSection.TaggedParts))
foreach (var element in Helpers.BuildClassifiedTextElements(descSection.TaggedParts, navigateToLinkService))
{
if (isFirstElement)
{
......@@ -68,7 +69,7 @@ internal static class IntellisenseQuickInfoBuilder
if (documentationCommentSection != null)
{
var isFirstElement = true;
foreach (var element in Helpers.BuildClassifiedTextElements(documentationCommentSection.TaggedParts))
foreach (var element in Helpers.BuildClassifiedTextElements(documentationCommentSection.TaggedParts, navigateToLinkService))
{
if (isFirstElement)
{
......@@ -92,7 +93,7 @@ internal static class IntellisenseQuickInfoBuilder
// 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.BuildClassifiedTextElements(s.TaggedParts, navigateToLinkService)));
// build text for RelatedSpan
if (quickInfoItem.RelatedSpans.Any())
......
......@@ -189,6 +189,11 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense
If classifiedTextRun IsNot Nothing Then
Dim classification = GetKnownClassification(classifiedTextRun.ClassificationTypeName)
result.Append($"{classification}, ""{classifiedTextRun.Text.Replace("""", """""")}""")
If classifiedTextRun.NavigationAction IsNot Nothing OrElse Not String.IsNullOrEmpty(classifiedTextRun.Tooltip) Then
Dim tooltip = If(classifiedTextRun.Tooltip IsNot Nothing, $"""{classifiedTextRun.Tooltip.Replace("""", """""")}""", "Nothing")
result.Append($", navigationAction:=Sub() Return, {tooltip}")
End If
If classifiedTextRun.Style <> ClassifiedTextRunStyle.Plain Then
result.Append($", {TextRunStyleToString(classifiedTextRun.Style)}")
End If
......
' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
Imports Microsoft.CodeAnalysis.Classification
Imports Microsoft.VisualStudio.Core.Imaging
Imports Microsoft.VisualStudio.Imaging
Imports Microsoft.VisualStudio.Text.Adornments
Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense
Public Class IntellisenseQuickInfoBuilderTests_Links
Inherits AbstractIntellisenseQuickInfoBuilderTests
<WpfTheory, Trait(Traits.Feature, Traits.Features.QuickInfo)>
<InlineData("see")>
<InlineData("a")>
Public Async Sub QuickInfoForPlainHyperlink(tag As String)
Dim workspace =
<Workspace>
<Project Language="C#" CommonReferences="true">
<Document>
using System.Threading;
class MyClass {
/// &lt;summary&gt;
/// This contains a link to &lt;<%= tag %> href="https://github.com/dotnet/roslyn"/&gt;.
/// &lt;/summary&gt;
void MyMethod() {
MyM$$ethod();
}
}
</Document>
</Project>
</Workspace>
Dim intellisenseQuickInfo = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp)
Dim expected = New ContainerElement(
ContainerElementStyle.Stacked Or ContainerElementStyle.VerticalPadding,
New ContainerElement(
ContainerElementStyle.Stacked,
New ContainerElement(
ContainerElementStyle.Wrapped,
New ImageElement(New ImageId(KnownImageIds.ImageCatalogGuid, KnownImageIds.MethodPrivate)),
New ClassifiedTextElement(
New ClassifiedTextRun(ClassificationTypeNames.Keyword, "void"),
New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "),
New ClassifiedTextRun(ClassificationTypeNames.ClassName, "MyClass"),
New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "."),
New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod"),
New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "("),
New ClassifiedTextRun(ClassificationTypeNames.Punctuation, ")"))),
New ClassifiedTextElement(
New ClassifiedTextRun(ClassificationTypeNames.Text, "This contains a link to"),
New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "),
New ClassifiedTextRun(ClassificationTypeNames.Text, "https://github.com/dotnet/roslyn", navigationAction:=Sub() Return, "https://github.com/dotnet/roslyn"),
New ClassifiedTextRun(ClassificationTypeNames.Text, "."))))
AssertEqualAdornments(expected, intellisenseQuickInfo.Item)
End Sub
<WpfTheory, Trait(Traits.Feature, Traits.Features.QuickInfo)>
<InlineData("see")>
<InlineData("a")>
Public Async Sub QuickInfoForHyperlinkWithText(tag As String)
Dim workspace =
<Workspace>
<Project Language="C#" CommonReferences="true">
<Document>
using System.Threading;
class MyClass {
/// &lt;summary&gt;
/// This contains a link to &lt;<%= tag %> href="https://github.com/dotnet/roslyn"&gt;dotnet/roslyn&lt;/<%= tag %>&gt;.
/// &lt;/summary&gt;
void MyMethod() {
MyM$$ethod();
}
}
</Document>
</Project>
</Workspace>
Dim intellisenseQuickInfo = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp)
Dim expected = New ContainerElement(
ContainerElementStyle.Stacked Or ContainerElementStyle.VerticalPadding,
New ContainerElement(
ContainerElementStyle.Stacked,
New ContainerElement(
ContainerElementStyle.Wrapped,
New ImageElement(New ImageId(KnownImageIds.ImageCatalogGuid, KnownImageIds.MethodPrivate)),
New ClassifiedTextElement(
New ClassifiedTextRun(ClassificationTypeNames.Keyword, "void"),
New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "),
New ClassifiedTextRun(ClassificationTypeNames.ClassName, "MyClass"),
New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "."),
New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod"),
New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "("),
New ClassifiedTextRun(ClassificationTypeNames.Punctuation, ")"))),
New ClassifiedTextElement(
New ClassifiedTextRun(ClassificationTypeNames.Text, "This contains a link to"),
New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "),
New ClassifiedTextRun(ClassificationTypeNames.Text, "dotnet/roslyn", navigationAction:=Sub() Return, "https://github.com/dotnet/roslyn"),
New ClassifiedTextRun(ClassificationTypeNames.Text, "."))))
AssertEqualAdornments(expected, intellisenseQuickInfo.Item)
End Sub
End Class
End Namespace
......@@ -31,13 +31,19 @@ namespace Microsoft.CodeAnalysis
/// </summary>
internal TaggedTextStyle Style { get; }
/// <summary>
/// Gets the navigation target for the text, or <see langword="null"/> if the text does not have a navigation
/// target.
/// </summary>
internal string NavigationTarget { get; }
/// <summary>
/// Creates a new instance of <see cref="TaggedText"/>
/// </summary>
/// <param name="tag">A descriptive tag from <see cref="TextTags"/>.</param>
/// <param name="text">The actual text to be displayed.</param>
public TaggedText(string tag, string text)
: this(tag, text, TaggedTextStyle.None)
: this(tag, text, TaggedTextStyle.None, navigationTarget: null)
{
}
......@@ -47,11 +53,13 @@ public TaggedText(string tag, string text)
/// <param name="tag">A descriptive tag from <see cref="TextTags"/>.</param>
/// <param name="text">The actual text to be displayed.</param>
/// <param name="style">The style(s) to apply to the text.</param>
internal TaggedText(string tag, string text, TaggedTextStyle style)
/// <param name="navigationTarget">The navigation target for the text, or <see langword="null"/> if the text does not have a navigation target.</param>
internal TaggedText(string tag, string text, TaggedTextStyle style, string navigationTarget)
{
Tag = tag ?? throw new ArgumentNullException(nameof(tag));
Text = text ?? throw new ArgumentNullException(nameof(text));
Style = style;
NavigationTarget = navigationTarget;
}
public override string ToString()
......@@ -73,7 +81,7 @@ public static ImmutableArray<TaggedText> ToTaggedText(this IEnumerable<SymbolDis
}
return displayParts.SelectAsArray(d =>
new TaggedText(SymbolDisplayPartKindTags.GetTag(d.Kind), d.ToString(), style));
new TaggedText(SymbolDisplayPartKindTags.GetTag(d.Kind), d.ToString(), style, navigationTarget: null));
}
public static string JoinText(this ImmutableArray<TaggedText> values)
......
......@@ -31,10 +31,12 @@ private class FormatterState
internal readonly List<TaggedText> Builder = new List<TaggedText>();
private readonly List<(DocumentationCommentListType type, int index, bool renderedItem)> _listStack = new List<(DocumentationCommentListType type, int index, bool renderedItem)>();
private readonly Stack<string> _navigationTargetStack = new Stack<string>();
private readonly Stack<TaggedTextStyle> _styleStack = new Stack<TaggedTextStyle>();
public FormatterState()
{
_navigationTargetStack.Push(null);
_styleStack.Push(TaggedTextStyle.None);
}
......@@ -51,6 +53,7 @@ public bool AtBeginning
public SymbolDisplayFormat Format { get; internal set; }
internal string NavigationTarget => _navigationTargetStack.Peek();
internal TaggedTextStyle Style => _styleStack.Peek();
public void AppendSingleSpace()
......@@ -62,7 +65,7 @@ public void AppendString(string s)
{
EmitPendingChars();
Builder.Add(new TaggedText(TextTags.Text, s, Style));
Builder.Add(new TaggedText(TextTags.Text, s, Style, NavigationTarget));
_anyNonWhitespaceSinceLastPara = true;
}
......@@ -115,6 +118,16 @@ public void PopList()
MarkBeginOrEndPara();
}
public void PushNavigationTarget(string navigationTarget)
{
_navigationTargetStack.Push(navigationTarget);
}
public void PopNavigationTarget()
{
_navigationTargetStack.Pop();
}
public void PushStyle(TaggedTextStyle style)
{
_styleStack.Push(_styleStack.Peek() | style);
......@@ -272,9 +285,11 @@ private static void AppendTextFromNode(FormatterState state, XNode node, Compila
var name = element.Name.LocalName;
var needPopStyle = false;
string navigationTarget = null;
if (name == DocumentationCommentXmlNames.SeeElementName ||
name == DocumentationCommentXmlNames.SeeAlsoElementName)
name == DocumentationCommentXmlNames.SeeAlsoElementName ||
name == "a")
{
if (element.IsEmpty || element.FirstNode == null)
{
......@@ -285,6 +300,14 @@ private static void AppendTextFromNode(FormatterState state, XNode node, Compila
return;
}
else
{
navigationTarget = GetNavigationTarget(element);
if (navigationTarget is object)
{
state.PushNavigationTarget(navigationTarget);
}
}
}
else if (name == DocumentationCommentXmlNames.ParameterReferenceElementName ||
name == DocumentationCommentXmlNames.TypeParameterReferenceElementName)
......@@ -381,6 +404,11 @@ private static void AppendTextFromNode(FormatterState state, XNode node, Compila
state.PopStyle();
}
if (navigationTarget is object)
{
state.PopNavigationTarget();
}
if (name == DocumentationCommentXmlNames.TermElementName)
{
state.AppendSingleSpace();
......@@ -388,6 +416,17 @@ private static void AppendTextFromNode(FormatterState state, XNode node, Compila
}
}
private static string GetNavigationTarget(XElement element)
{
var hrefAttribute = element.Attribute(DocumentationCommentXmlNames.HrefAttributeName);
if (hrefAttribute is object)
{
return hrefAttribute.Value;
}
return null;
}
private static void AppendTextFromAttribute(FormatterState state, XElement element, XAttribute attribute, string attributeNameToParse, SymbolDisplayPartKind kind)
{
var attributeName = attribute.Name.LocalName;
......@@ -401,7 +440,12 @@ private static void AppendTextFromAttribute(FormatterState state, XElement eleme
var displayKind = attributeName == DocumentationCommentXmlNames.LangwordAttributeName
? TextTags.Keyword
: TextTags.Text;
state.AppendParts(SpecializedCollections.SingletonEnumerable(new TaggedText(displayKind, attribute.Value, state.Style)));
var text = attribute.Value;
var style = state.Style;
var navigationTarget = attributeName == DocumentationCommentXmlNames.HrefAttributeName
? attribute.Value
: null;
state.AppendParts(SpecializedCollections.SingletonEnumerable(new TaggedText(displayKind, text, style, navigationTarget)));
}
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册