diff --git a/src/Compilers/Core/Portable/InternalUtilities/DocumentationCommentXmlNames.cs b/src/Compilers/Core/Portable/InternalUtilities/DocumentationCommentXmlNames.cs index 15a2e232900d..6eacb0ee608d 100644 --- a/src/Compilers/Core/Portable/InternalUtilities/DocumentationCommentXmlNames.cs +++ b/src/Compilers/Core/Portable/InternalUtilities/DocumentationCommentXmlNames.cs @@ -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"; diff --git a/src/EditorFeatures/CSharpTest/QuickInfo/SyntacticQuickInfoSourceTests.cs b/src/EditorFeatures/CSharpTest/QuickInfo/SyntacticQuickInfoSourceTests.cs index bdd850469bec..a45d97fc4037 100644 --- a/src/EditorFeatures/CSharpTest/QuickInfo/SyntacticQuickInfoSourceTests.cs +++ b/src/EditorFeatures/CSharpTest/QuickInfo/SyntacticQuickInfoSourceTests.cs @@ -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; @@ -291,7 +292,8 @@ protected override async Task AssertContentIsAsync( Assert.NotEqual(0, info.RelatedSpans.Length); var trackingSpan = new Mock(MockBehavior.Strict); - var quickInfoItem = await IntellisenseQuickInfoBuilder.BuildItemAsync(trackingSpan.Object, info, snapshot, document, CancellationToken.None); + var streamingPresenter = workspace.ExportProvider.GetExport(); + var quickInfoItem = await IntellisenseQuickInfoBuilder.BuildItemAsync(trackingSpan.Object, info, snapshot, document, streamingPresenter, CancellationToken.None); var containerElement = quickInfoItem.Item as ContainerElement; var textElements = containerElement.Elements.OfType(); diff --git a/src/EditorFeatures/Core/Implementation/DefaultNavigateToLinkService.cs b/src/EditorFeatures/Core/Implementation/DefaultNavigateToLinkService.cs new file mode 100644 index 000000000000..82b4421d0bac --- /dev/null +++ b/src/EditorFeatures/Core/Implementation/DefaultNavigateToLinkService.cs @@ -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 TryNavigateToLinkAsync(Uri uri, CancellationToken cancellationToken) + => SpecializedTasks.False; + } +} diff --git a/src/EditorFeatures/Core/Implementation/INavigateToLinkService.cs b/src/EditorFeatures/Core/Implementation/INavigateToLinkService.cs new file mode 100644 index 000000000000..2a4fe2b1141c --- /dev/null +++ b/src/EditorFeatures/Core/Implementation/INavigateToLinkService.cs @@ -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 TryNavigateToLinkAsync(Uri uri, CancellationToken cancellationToken); + } +} diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionSource.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionSource.cs index df98d8b31a06..a6366f31a50c 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionSource.cs +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionSource.cs @@ -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; @@ -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; @@ -61,10 +63,13 @@ internal class CompletionSource : ForegroundThreadAffinitizedObject, IAsyncCompl private readonly ITextView _textView; private readonly bool _isDebuggerTextView; private readonly ImmutableHashSet _roles; + private readonly Lazy _streamingPresenter; - internal CompletionSource(ITextView textView, IThreadingContext threadingContext) : base(threadingContext) + internal CompletionSource(ITextView textView, Lazy streamingPresenter, IThreadingContext threadingContext) + : base(threadingContext) { _textView = textView; + _streamingPresenter = streamingPresenter; _isDebuggerTextView = textView is IDebuggerTextView; _roles = textView.Roles.ToImmutableHashSet(); } @@ -294,7 +299,6 @@ public async Task GetDescriptionAsync(IAsyncCompletionSession session, V } var service = document.GetLanguageService(); - if (service == null) { return null; @@ -302,7 +306,7 @@ public async Task GetDescriptionAsync(IAsyncCompletionSession session, V 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(); diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionSourceProvider.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionSourceProvider.cs index 11b9541d90ae..412dc8ce1d59 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionSourceProvider.cs +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionSourceProvider.cs @@ -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; @@ -16,15 +17,19 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncComplet internal class CompletionSourceProvider : IAsyncCompletionSourceProvider { private readonly IThreadingContext _threadingContext; + private readonly Lazy _streamingPresenter; [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CompletionSourceProvider(IThreadingContext threadingContext) + public CompletionSourceProvider( + IThreadingContext threadingContext, + Lazy streamingPresenter) { _threadingContext = threadingContext; + _streamingPresenter = streamingPresenter; } public IAsyncCompletionSource GetOrCreate(ITextView textView) - => new CompletionSource(textView, _threadingContext); + => new CompletionSource(textView, _streamingPresenter, _threadingContext); } } diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/Helpers.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/Helpers.cs index b122f7461928..f85122c659ba 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/Helpers.cs +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/Helpers.cs @@ -1,7 +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 Microsoft.CodeAnalysis.Classification; +using Microsoft.CodeAnalysis.Editor.GoToDefinition; +using Microsoft.CodeAnalysis.Editor.Host; using Microsoft.VisualStudio.Text.Adornments; using Roslyn.Utilities; @@ -9,19 +15,73 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense { internal static class Helpers { - internal static IEnumerable BuildClassifiedTextElements(ImmutableArray taggedTexts) + internal static IReadOnlyCollection BuildInteractiveTextElements(ImmutableArray taggedTexts, Document document, Lazy streamingPresenter) + { + var index = 0; + return BuildInteractiveTextElements(taggedTexts, ref index, document, streamingPresenter); + } + + private static IReadOnlyCollection BuildInteractiveTextElements(ImmutableArray taggedTexts, ref int index, Document document, Lazy streamingPresenter) { // This method produces a sequence of zero or more paragraphs var paragraphs = new List(); // Each paragraph is constructed from one or more lines - var currentParagraph = new List(); + var currentParagraph = new List(); // Each line is constructed from one or more inline elements var currentRuns = new List(); - 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) @@ -53,8 +113,20 @@ internal static IEnumerable BuildClassifiedTextElements(ImmutableArray NavigateToQuickInfoTarget(target, document, streamingPresenter.Value), tooltip, style)); + } + else + { + currentRuns.Add(new ClassifiedTextRun(part.Tag.ToClassificationTypeName(), part.Text, style)); + } } + + index++; } if (currentRuns.Count > 0) @@ -72,7 +144,60 @@ internal static IEnumerable BuildClassifiedTextElements(ImmutableArray lines) + private static void NavigateToQuickInfoTarget(string navigationTarget, Document document, IStreamingFindUsagesPresenter streamingPresenter) + { + var navigateToLinkService = document.Project.Solution.Workspace.Services.GetRequiredService(); + 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 lines) { Contract.ThrowIfFalse(lines.Count > 0); diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilder.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilder.cs index 6b581eb44c39..1f9052fdd05b 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilder.cs +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilder.cs @@ -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; @@ -24,6 +26,7 @@ internal static async Task BuildItemAsync(ITrackingSp CodeAnalysisQuickInfoItem quickInfoItem, ITextSnapshot snapshot, Document document, + Lazy 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 @@ -46,7 +49,7 @@ internal static async Task 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) { @@ -68,7 +71,7 @@ internal static async Task 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) { @@ -92,7 +95,7 @@ internal static async Task 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()) @@ -107,7 +110,7 @@ internal static async Task 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()) { diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/QuickInfoSourceProvider.QuickInfoSource.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/QuickInfoSourceProvider.QuickInfoSource.cs index 2adff626eb67..e2965aabeeaa 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/QuickInfoSourceProvider.QuickInfoSource.cs +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/QuickInfoSourceProvider.QuickInfoSource.cs @@ -3,6 +3,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editor.Host; using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.QuickInfo; @@ -21,10 +22,12 @@ internal partial class QuickInfoSourceProvider private class QuickInfoSource : IAsyncQuickInfoSource { private readonly ITextBuffer _subjectBuffer; + private readonly Lazy _streamingPresenter; - public QuickInfoSource(ITextBuffer subjectBuffer) + public QuickInfoSource(ITextBuffer subjectBuffer, Lazy streamingPresenter) { _subjectBuffer = subjectBuffer; + _streamingPresenter = streamingPresenter; } public async Task GetQuickInfoItemAsync(IAsyncQuickInfoSession session, CancellationToken cancellationToken) @@ -59,7 +62,7 @@ public async Task GetQuickInfoItemAsync(IAsyncQuickIn { var textVersion = snapshot.Version; var trackingSpan = textVersion.CreateTrackingSpan(item.Span.ToSpan(), SpanTrackingMode.EdgeInclusive); - return await IntellisenseQuickInfoBuilder.BuildItemAsync(trackingSpan, item, snapshot, document, cancellationToken).ConfigureAwait(false); + return await IntellisenseQuickInfoBuilder.BuildItemAsync(trackingSpan, item, snapshot, document, _streamingPresenter, cancellationToken).ConfigureAwait(false); } return null; diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/QuickInfoSourceProvider.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/QuickInfoSourceProvider.cs index c461b5d5a81e..f5012214e99b 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/QuickInfoSourceProvider.cs +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/QuickInfoSourceProvider.cs @@ -1,6 +1,9 @@ // 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.ComponentModel.Composition; +using Microsoft.CodeAnalysis.Editor.Host; +using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.VisualStudio.Language.Intellisense; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Utilities; @@ -12,14 +15,18 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo [Name("RoslynQuickInfoProvider")] internal partial class QuickInfoSourceProvider : IAsyncQuickInfoSourceProvider { + private readonly Lazy _streamingPresenter; + [ImportingConstructor] - public QuickInfoSourceProvider() + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public QuickInfoSourceProvider(Lazy streamingPresenter) { + _streamingPresenter = streamingPresenter; } public IAsyncQuickInfoSource TryCreateQuickInfoSource(ITextBuffer textBuffer) { - return new QuickInfoSource(textBuffer); + return new QuickInfoSource(textBuffer, _streamingPresenter); } } } diff --git a/src/EditorFeatures/Test/DocCommentFormatting/DocCommentFormattingTests.cs b/src/EditorFeatures/Test/DocCommentFormatting/DocCommentFormattingTests.cs index dc1654bcfcd1..d5c2e2e87dbc 100644 --- a/src/EditorFeatures/Test/DocCommentFormatting/DocCommentFormattingTests.cs +++ b/src/EditorFeatures/Test/DocCommentFormatting/DocCommentFormattingTests.cs @@ -48,7 +48,7 @@ public void ExampleAndCodeTags() results in p's having the value (2,8). "; - var expected = "This method changes the point's location by the given x- and y-offsets. For example: Point p = new Point(3,5); p.Translate(-1,3); results in p's having the value (2,8)."; + var expected = "This method changes the point's location by the given x- and y-offsets. For example:\r\n\r\nPoint p = new Point(3,5); p.Translate(-1,3);\r\n\r\nresults in p's having the value (2,8)."; TestFormat(comment, expected); } @@ -66,7 +66,7 @@ public void ListTag() "; - var expected = @"Here is an example of a bulleted list: Item 1. Item 2."; + var expected = "Here is an example of a bulleted list:\r\n\r\n• Item 1.\r\n• Item 2."; TestFormat(comment, expected); } diff --git a/src/EditorFeatures/Test2/IntelliSense/AbstractIntellisenseQuickInfoBuilderTests.vb b/src/EditorFeatures/Test2/IntelliSense/AbstractIntellisenseQuickInfoBuilderTests.vb new file mode 100644 index 000000000000..50beafe253a3 --- /dev/null +++ b/src/EditorFeatures/Test2/IntelliSense/AbstractIntellisenseQuickInfoBuilderTests.vb @@ -0,0 +1,269 @@ +' 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 System.Text +Imports System.Threading +Imports Microsoft.CodeAnalysis.Classification +Imports Microsoft.CodeAnalysis.Editor.Host +Imports Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo +Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces +Imports Microsoft.CodeAnalysis.QuickInfo +Imports Microsoft.VisualStudio.Core.Imaging +Imports Microsoft.VisualStudio.Imaging +Imports Microsoft.VisualStudio.Text +Imports Microsoft.VisualStudio.Text.Adornments +Imports Moq + +Imports VSQuickInfoItem = Microsoft.VisualStudio.Language.Intellisense.QuickInfoItem + +Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense + + Public MustInherit Class AbstractIntellisenseQuickInfoBuilderTests + Protected Async Function GetQuickInfoItemAsync(quickInfoItem As QuickInfoItem) As Task(Of VSQuickInfoItem) + Dim workspaceDefinition = + + + + $$ + + + + + Using workspace = TestWorkspace.Create(workspaceDefinition) + Dim solution = workspace.CurrentSolution + Dim cursorDocument = workspace.Documents.First(Function(d) d.CursorPosition.HasValue) + Dim cursorBuffer = cursorDocument.TextBuffer + + Dim document = workspace.CurrentSolution.GetDocument(cursorDocument.Id) + + Dim trackingSpan = New Mock(Of ITrackingSpan) With { + .DefaultValue = DefaultValue.Mock + } + + Dim streamingPresenter = workspace.ExportProvider.GetExport(Of IStreamingFindUsagesPresenter)() + Return Await IntellisenseQuickInfoBuilder.BuildItemAsync(trackingSpan.Object, quickInfoItem, cursorBuffer.CurrentSnapshot, document, streamingPresenter, CancellationToken.None) + End Using + End Function + + Protected Async Function GetQuickInfoItemAsync(workspaceDefinition As XElement, language As String) As Task(Of VSQuickInfoItem) + Using workspace = TestWorkspace.Create(workspaceDefinition) + Dim solution = workspace.CurrentSolution + Dim cursorDocument = workspace.Documents.First(Function(d) d.CursorPosition.HasValue) + Dim cursorPosition = cursorDocument.CursorPosition.Value + Dim cursorBuffer = cursorDocument.TextBuffer + + Dim document = workspace.CurrentSolution.GetDocument(cursorDocument.Id) + + Dim languageServiceProvider = workspace.Services.GetLanguageServices(language) + Dim quickInfoService = languageServiceProvider.GetRequiredService(Of QuickInfoService) + + Dim codeAnalysisQuickInfoItem = Await quickInfoService.GetQuickInfoAsync(document, cursorPosition, CancellationToken.None).ConfigureAwait(False) + + Dim trackingSpan = New Mock(Of ITrackingSpan) With { + .DefaultValue = DefaultValue.Mock + } + + Dim streamingPresenter = workspace.ExportProvider.GetExport(Of IStreamingFindUsagesPresenter)() + Return Await IntellisenseQuickInfoBuilder.BuildItemAsync(trackingSpan.Object, codeAnalysisQuickInfoItem, cursorBuffer.CurrentSnapshot, document, streamingPresenter, CancellationToken.None) + End Using + End Function + + Protected Shared Sub AssertEqualAdornments(expected As Object, actual As Object) + Try + Assert.IsType(expected.GetType, actual) + + Dim containerElement = TryCast(expected, ContainerElement) + If containerElement IsNot Nothing Then + AssertEqualContainerElement(containerElement, DirectCast(actual, ContainerElement)) + Return + End If + + Dim imageElement = TryCast(expected, ImageElement) + If imageElement IsNot Nothing Then + AssertEqualImageElement(imageElement, DirectCast(actual, ImageElement)) + Return + End If + + Dim classifiedTextElement = TryCast(expected, ClassifiedTextElement) + If classifiedTextElement IsNot Nothing Then + AssertEqualClassifiedTextElement(classifiedTextElement, DirectCast(actual, ClassifiedTextElement)) + Return + End If + + Dim classifiedTextRun = TryCast(expected, ClassifiedTextRun) + If classifiedTextRun IsNot Nothing Then + AssertEqualClassifiedTextRun(classifiedTextRun, DirectCast(actual, ClassifiedTextRun)) + Return + End If + + Throw Roslyn.Utilities.ExceptionUtilities.Unreachable + Catch ex As Exception + Dim renderedExpected = ContainerToString(expected) + Dim renderedActual = ContainerToString(actual) + AssertEx.EqualOrDiff(renderedExpected, renderedActual) + + ' This is not expected to be hit, but it will be hit if the difference cannot be detected within the diff + Throw + End Try + End Sub + + Private Shared Sub AssertEqualContainerElement(expected As ContainerElement, actual As ContainerElement) + Assert.Equal(expected.Style, actual.Style) + Assert.Equal(expected.Elements.Count, actual.Elements.Count) + For Each pair In expected.Elements.Zip(actual.Elements, Function(expectedElement, actualElement) (expectedElement, actualElement)) + AssertEqualAdornments(pair.expectedElement, pair.actualElement) + Next + End Sub + + Private Shared Sub AssertEqualImageElement(expected As ImageElement, actual As ImageElement) + Assert.Equal(expected.ImageId.Guid, actual.ImageId.Guid) + Assert.Equal(expected.ImageId.Id, actual.ImageId.Id) + Assert.Equal(expected.AutomationName, actual.AutomationName) + End Sub + + Private Shared Sub AssertEqualClassifiedTextElement(expected As ClassifiedTextElement, actual As ClassifiedTextElement) + Assert.Equal(expected.Runs.Count, actual.Runs.Count) + For Each pair In expected.Runs.Zip(actual.Runs, Function(expectedRun, actualRun) (expectedRun, actualRun)) + AssertEqualClassifiedTextRun(pair.expectedRun, pair.actualRun) + Next + End Sub + + Private Shared Sub AssertEqualClassifiedTextRun(expected As ClassifiedTextRun, actual As ClassifiedTextRun) + Assert.Equal(expected.ClassificationTypeName, actual.ClassificationTypeName) + Assert.Equal(expected.Text, actual.Text) + Assert.Equal(expected.Tooltip, actual.Tooltip) + Assert.Equal(expected.Style, actual.Style) + End Sub + + Private Shared Function ContainerToString(element As Object) As String + Dim result = New StringBuilder + ContainerToString(element, "", result) + Return result.ToString() + End Function + + Private Shared Sub ContainerToString(element As Object, indent As String, result As StringBuilder) + result.Append($"{indent}New {element.GetType().Name}(") + + Dim container = TryCast(element, ContainerElement) + If container IsNot Nothing Then + result.AppendLine() + indent += " " + result.AppendLine($"{indent}{ContainerStyleToString(container.Style)},") + Dim elements = container.Elements.ToArray() + For i = 0 To elements.Length - 1 + ContainerToString(elements(i), indent, result) + + If i < elements.Length - 1 Then + result.AppendLine(",") + Else + result.Append(")") + End If + Next + + Return + End If + + Dim image = TryCast(element, ImageElement) + If image IsNot Nothing Then + Dim guid = GetKnownImageGuid(image.ImageId.Guid) + Dim id = GetKnownImageId(image.ImageId.Id) + result.Append($"New {NameOf(ImageId)}({guid}, {id}))") + Return + End If + + Dim classifiedTextElement = TryCast(element, ClassifiedTextElement) + If classifiedTextElement IsNot Nothing Then + result.AppendLine() + indent += " " + Dim runs = classifiedTextElement.Runs.ToArray() + For i = 0 To runs.Length - 1 + ContainerToString(runs(i), indent, result) + + If i < runs.Length - 1 Then + result.AppendLine(",") + Else + result.Append(")") + End If + Next + + Return + End If + + Dim classifiedTextRun = TryCast(element, ClassifiedTextRun) + 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 + + result.Append(")") + Return + End If + + Throw Roslyn.Utilities.ExceptionUtilities.Unreachable + End Sub + + Private Shared Function ContainerStyleToString(style As ContainerElementStyle) As String + Dim stringValue = style.ToString() + Return String.Join(" Or ", stringValue.Split({","c, " "c}, StringSplitOptions.RemoveEmptyEntries).Select(Function(value) $"{NameOf(ContainerElementStyle)}.{value}")) + End Function + + Private Shared Function TextRunStyleToString(style As ClassifiedTextRunStyle) As String + Dim stringValue = style.ToString() + Return String.Join(" Or ", stringValue.Split({","c, " "c}, StringSplitOptions.RemoveEmptyEntries).Select(Function(value) $"{NameOf(ClassifiedTextRunStyle)}.{value}")) + End Function + + Private Shared Function GetKnownClassification(classification As String) As String + For Each field In GetType(ClassificationTypeNames).GetFields() + If Not field.IsStatic Then + Continue For + End If + + Dim rawValue = field.GetValue(Nothing) + Dim value = TryCast(rawValue, String) + If value = classification Then + Return $"{NameOf(ClassificationTypeNames)}.{field.Name}" + End If + Next + + Return $"""{classification}""" + End Function + + Private Shared Function GetKnownImageGuid(guid As Guid) As String + For Each field In GetType(KnownImageIds).GetFields() + If Not field.IsStatic Then + Continue For + End If + + Dim rawValue = field.GetValue(Nothing) + Dim value As Guid? = If(TypeOf rawValue Is Guid, DirectCast(rawValue, Guid), Nothing) + If value = guid Then + Return $"{NameOf(KnownImageIds)}.{field.Name}" + End If + Next + + Return guid.ToString() + End Function + + Private Shared Function GetKnownImageId(id As Integer) As String + For Each field In GetType(KnownImageIds).GetFields() + If Not field.IsStatic Then + Continue For + End If + + Dim rawValue = field.GetValue(Nothing) + Dim value As Integer? = If(TypeOf rawValue Is Integer, CInt(rawValue), Nothing) + If value = id Then + Return $"{NameOf(KnownImageIds)}.{field.Name}" + End If + Next + + Return id.ToString() + End Function + End Class +End Namespace diff --git a/src/EditorFeatures/Test2/IntelliSense/IntellisenseQuickInfoBuilderTests.vb b/src/EditorFeatures/Test2/IntelliSense/IntellisenseQuickInfoBuilderTests.vb index 2e218c5f5e11..f8caa13be840 100644 --- a/src/EditorFeatures/Test2/IntelliSense/IntellisenseQuickInfoBuilderTests.vb +++ b/src/EditorFeatures/Test2/IntelliSense/IntellisenseQuickInfoBuilderTests.vb @@ -1,25 +1,18 @@ ' 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 System.Collections.Immutable -Imports System.Text -Imports System.Threading Imports Microsoft.CodeAnalysis.Classification -Imports Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo -Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces Imports Microsoft.CodeAnalysis.QuickInfo Imports Microsoft.CodeAnalysis.Tags Imports Microsoft.CodeAnalysis.Text Imports Microsoft.VisualStudio.Core.Imaging Imports Microsoft.VisualStudio.Imaging -Imports Microsoft.VisualStudio.Text Imports Microsoft.VisualStudio.Text.Adornments -Imports Moq Imports QuickInfoItem = Microsoft.CodeAnalysis.QuickInfo.QuickInfoItem Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense - - Public Class IntellisenseQuickInfoBuilderTests + Inherits AbstractIntellisenseQuickInfoBuilderTests @@ -66,11 +59,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense New TaggedText(TextTags.Punctuation, "."), New TaggedText(TextTags.Class, "IOException"))))) - Dim trackingSpan = New Mock(Of ITrackingSpan) With { - .DefaultValue = DefaultValue.Mock - } - - Dim intellisenseQuickInfo = Await IntellisenseQuickInfoBuilder.BuildItemAsync(trackingSpan.Object, codeAnalysisQuickInfoItem, Nothing, Nothing, Threading.CancellationToken.None) + Dim intellisenseQuickInfo = Await GetQuickInfoItemAsync(codeAnalysisQuickInfoItem) Assert.NotNull(intellisenseQuickInfo) Dim container = Assert.IsType(Of ContainerElement)(intellisenseQuickInfo.Item) @@ -160,11 +149,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense New TaggedText(TextTags.Punctuation, "."), New TaggedText(TextTags.Class, "IOException"))))) - Dim trackingSpan = New Mock(Of ITrackingSpan) With { - .DefaultValue = DefaultValue.Mock - } - - Dim intellisenseQuickInfo = Await IntellisenseQuickInfoBuilder.BuildItemAsync(trackingSpan.Object, codeAnalysisQuickInfoItem, Nothing, Nothing, Threading.CancellationToken.None) + Dim intellisenseQuickInfo = Await GetQuickInfoItemAsync(codeAnalysisQuickInfoItem) Assert.NotNull(intellisenseQuickInfo) Dim container = Assert.IsType(Of ContainerElement)(intellisenseQuickInfo.Item) @@ -264,11 +249,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense New TaggedText(TextTags.Punctuation, "."), New TaggedText(TextTags.Class, "IOException"))))) - Dim trackingSpan = New Mock(Of ITrackingSpan) With { - .DefaultValue = DefaultValue.Mock - } - - Dim intellisenseQuickInfo = Await IntellisenseQuickInfoBuilder.BuildItemAsync(trackingSpan.Object, codeAnalysisQuickInfoItem, Nothing, Nothing, Threading.CancellationToken.None) + Dim intellisenseQuickInfo = Await GetQuickInfoItemAsync(codeAnalysisQuickInfoItem) Assert.NotNull(intellisenseQuickInfo) Dim container = Assert.IsType(Of ContainerElement)(intellisenseQuickInfo.Item) @@ -355,13 +336,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense - Dim codeAnalysisQuickInfoItem = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp) - - Dim trackingSpan = New Mock(Of ITrackingSpan) With { - .DefaultValue = DefaultValue.Mock - } - - Dim intellisenseQuickInfo = Await IntellisenseQuickInfoBuilder.BuildItemAsync(trackingSpan.Object, codeAnalysisQuickInfoItem, Nothing, Nothing, CancellationToken.None) + Dim intellisenseQuickInfo = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp) Assert.NotNull(intellisenseQuickInfo) Dim container = Assert.IsType(Of ContainerElement)(intellisenseQuickInfo.Item) @@ -376,14 +351,14 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense New ClassifiedTextElement( New ClassifiedTextRun(ClassificationTypeNames.Keyword, "void"), New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), - New ClassifiedTextRun(ClassificationTypeNames.ClassName, "MyClass"), + New ClassifiedTextRun(ClassificationTypeNames.ClassName, "MyClass", navigationAction:=Sub() Return, "MyClass"), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "."), - New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod"), + New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod", navigationAction:=Sub() Return, "void MyClass.MyMethod(CancellationToken cancellationToken = default(CancellationToken))"), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "("), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "["), - New ClassifiedTextRun(ClassificationTypeNames.StructName, "CancellationToken"), + New ClassifiedTextRun(ClassificationTypeNames.StructName, "CancellationToken", navigationAction:=Sub() Return, "CancellationToken"), New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), - New ClassifiedTextRun(ClassificationTypeNames.ParameterName, "cancellationToken"), + New ClassifiedTextRun(ClassificationTypeNames.ParameterName, "cancellationToken", navigationAction:=Sub() Return, "CancellationToken cancellationToken = default(CancellationToken)"), New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "="), New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), @@ -410,7 +385,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense New ClassifiedTextRun(ClassificationTypeNames.Text, FeaturesResources.Exceptions_colon)), New ClassifiedTextElement( New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), - New ClassifiedTextRun(ClassificationTypeNames.ClassName, "IOException")))) + New ClassifiedTextRun(ClassificationTypeNames.ClassName, "IOException", navigationAction:=Sub() Return, "IOException")))) AssertEqualAdornments(expected, container) End Sub @@ -445,13 +420,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense - Dim codeAnalysisQuickInfoItem = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp) - - Dim trackingSpan = New Mock(Of ITrackingSpan) With { - .DefaultValue = DefaultValue.Mock - } - - Dim intellisenseQuickInfo = Await IntellisenseQuickInfoBuilder.BuildItemAsync(trackingSpan.Object, codeAnalysisQuickInfoItem, Nothing, Nothing, CancellationToken.None) + Dim intellisenseQuickInfo = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp) Assert.NotNull(intellisenseQuickInfo) Dim container = Assert.IsType(Of ContainerElement)(intellisenseQuickInfo.Item) @@ -466,9 +435,9 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense New ClassifiedTextElement( New ClassifiedTextRun(ClassificationTypeNames.Keyword, "void"), New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), - New ClassifiedTextRun(ClassificationTypeNames.ClassName, "MyClass"), + New ClassifiedTextRun(ClassificationTypeNames.ClassName, "MyClass", navigationAction:=Sub() Return, "MyClass"), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "."), - New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod"), + New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod", navigationAction:=Sub() Return, "void MyClass.MyMethod()"), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "("), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, ")"))), New ClassifiedTextElement( @@ -479,6 +448,105 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense AssertEqualAdornments(expected, container) End Sub + + Public Async Sub InlineCodeElement() + Dim workspace = + + + + using System.IO; + using System.Threading; + class MyClass { + /// <summary> + /// This method returns <c>true</c>. + /// </summary> + bool MyMethod() { + return MyM$$ethod(); + } + } + + + + + Dim intellisenseQuickInfo = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp) + Assert.NotNull(intellisenseQuickInfo) + + Dim container = Assert.IsType(Of ContainerElement)(intellisenseQuickInfo.Item) + + 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, "bool", navigationAction:=Sub() Return, "bool"), + New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), + New ClassifiedTextRun(ClassificationTypeNames.ClassName, "MyClass", navigationAction:=Sub() Return, "MyClass"), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "."), + New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod", navigationAction:=Sub() Return, "bool MyClass.MyMethod()"), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "("), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, ")"))), + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "This method returns"), + New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), + New ClassifiedTextRun(ClassificationTypeNames.Text, "true", ClassifiedTextRunStyle.UseClassificationFont), + New ClassifiedTextRun(ClassificationTypeNames.Text, ".")))) + + AssertEqualAdornments(expected, container) + End Sub + + + Public Async Sub BlockLevelCodeElement() + Dim workspace = + + + + using System.IO; + using System.Threading; + class MyClass { + /// <summary> + /// This method returns <code>true</code>. + /// </summary> + bool MyMethod() { + return MyM$$ethod(); + } + } + + + + + Dim intellisenseQuickInfo = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp) + Assert.NotNull(intellisenseQuickInfo) + + Dim container = Assert.IsType(Of ContainerElement)(intellisenseQuickInfo.Item) + + 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, "bool", navigationAction:=Sub() Return, "bool"), + New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), + New ClassifiedTextRun(ClassificationTypeNames.ClassName, "MyClass", navigationAction:=Sub() Return, "MyClass"), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "."), + New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod", navigationAction:=Sub() Return, "bool MyClass.MyMethod()"), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "("), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, ")"))), + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "This method returns"))), + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "true", ClassifiedTextRunStyle.UseClassificationFont)), + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "."))) + + AssertEqualAdornments(expected, container) + End Sub + Public Async Sub QuickInfoForParameterReference() @@ -499,13 +567,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense - Dim codeAnalysisQuickInfoItem = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp) - - Dim trackingSpan = New Mock(Of ITrackingSpan) With { - .DefaultValue = DefaultValue.Mock - } - - Dim intellisenseQuickInfo = Await IntellisenseQuickInfoBuilder.BuildItemAsync(trackingSpan.Object, codeAnalysisQuickInfoItem, Nothing, Nothing, CancellationToken.None) + Dim intellisenseQuickInfo = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp) Assert.NotNull(intellisenseQuickInfo) Dim container = Assert.IsType(Of ContainerElement)(intellisenseQuickInfo.Item) @@ -520,13 +582,13 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense New ClassifiedTextElement( New ClassifiedTextRun(ClassificationTypeNames.Keyword, "void"), New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), - New ClassifiedTextRun(ClassificationTypeNames.ClassName, "MyClass"), + New ClassifiedTextRun(ClassificationTypeNames.ClassName, "MyClass", navigationAction:=Sub() Return, "MyClass"), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "."), - New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod"), + New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod", navigationAction:=Sub() Return, "void MyClass.MyMethod(CancellationToken p)"), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "("), - New ClassifiedTextRun(ClassificationTypeNames.StructName, "CancellationToken"), + New ClassifiedTextRun(ClassificationTypeNames.StructName, "CancellationToken", navigationAction:=Sub() Return, "CancellationToken"), New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), - New ClassifiedTextRun(ClassificationTypeNames.ParameterName, "p"), + New ClassifiedTextRun(ClassificationTypeNames.ParameterName, "p", navigationAction:=Sub() Return, "CancellationToken p"), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, ")"))), New ClassifiedTextElement( New ClassifiedTextRun(ClassificationTypeNames.Text, "The parameter is"), @@ -553,13 +615,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense - Dim codeAnalysisQuickInfoItem = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp) - - Dim trackingSpan = New Mock(Of ITrackingSpan) With { - .DefaultValue = DefaultValue.Mock - } - - Dim intellisenseQuickInfo = Await IntellisenseQuickInfoBuilder.BuildItemAsync(trackingSpan.Object, codeAnalysisQuickInfoItem, Nothing, Nothing, CancellationToken.None) + Dim intellisenseQuickInfo = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp) Assert.NotNull(intellisenseQuickInfo) Dim container = Assert.IsType(Of ContainerElement)(intellisenseQuickInfo.Item) @@ -574,9 +630,9 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), New ClassifiedTextRun(ClassificationTypeNames.Keyword, "void"), New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), - New ClassifiedTextRun(ClassificationTypeNames.StructName, "MyStruct"), + New ClassifiedTextRun(ClassificationTypeNames.StructName, "MyStruct", navigationAction:=Sub() Return, "MyStruct"), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "."), - New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod"), + New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod", navigationAction:=Sub() Return, "readonly void MyStruct.MyMethod()"), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "("), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, ")")))) @@ -597,13 +653,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense - Dim codeAnalysisQuickInfoItem = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp) - - Dim trackingSpan = New Mock(Of ITrackingSpan) With { - .DefaultValue = DefaultValue.Mock - } - - Dim intellisenseQuickInfo = Await IntellisenseQuickInfoBuilder.BuildItemAsync(trackingSpan.Object, codeAnalysisQuickInfoItem, Nothing, Nothing, CancellationToken.None) + Dim intellisenseQuickInfo = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp) Assert.NotNull(intellisenseQuickInfo) Dim container = Assert.IsType(Of ContainerElement)(intellisenseQuickInfo.Item) @@ -616,11 +666,11 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense New ClassifiedTextElement( New ClassifiedTextRun(ClassificationTypeNames.Keyword, "readonly"), New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), - New ClassifiedTextRun(ClassificationTypeNames.Keyword, "int"), + New ClassifiedTextRun(ClassificationTypeNames.Keyword, "int", navigationAction:=Sub() Return, "int"), New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), - New ClassifiedTextRun(ClassificationTypeNames.StructName, "MyStruct"), + New ClassifiedTextRun(ClassificationTypeNames.StructName, "MyStruct", navigationAction:=Sub() Return, "MyStruct"), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "."), - New ClassifiedTextRun(ClassificationTypeNames.PropertyName, "MyProperty"), + New ClassifiedTextRun(ClassificationTypeNames.PropertyName, "MyProperty", navigationAction:=Sub() Return, "readonly int MyStruct.MyProperty"), New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "{"), New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), @@ -646,13 +696,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense - Dim codeAnalysisQuickInfoItem = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp) - - Dim trackingSpan = New Mock(Of ITrackingSpan) With { - .DefaultValue = DefaultValue.Mock - } - - Dim intellisenseQuickInfo = Await IntellisenseQuickInfoBuilder.BuildItemAsync(trackingSpan.Object, codeAnalysisQuickInfoItem, Nothing, Nothing, CancellationToken.None) + Dim intellisenseQuickInfo = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp) Assert.NotNull(intellisenseQuickInfo) Dim container = Assert.IsType(Of ContainerElement)(intellisenseQuickInfo.Item) @@ -665,13 +709,13 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense New ClassifiedTextElement( New ClassifiedTextRun(ClassificationTypeNames.Keyword, "readonly"), New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), - New ClassifiedTextRun(ClassificationTypeNames.NamespaceName, "System"), + New ClassifiedTextRun(ClassificationTypeNames.NamespaceName, "System", navigationAction:=Sub() Return, "System"), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "."), - New ClassifiedTextRun(ClassificationTypeNames.DelegateName, "Action"), + New ClassifiedTextRun(ClassificationTypeNames.DelegateName, "Action", navigationAction:=Sub() Return, "Action"), New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), - New ClassifiedTextRun(ClassificationTypeNames.StructName, "MyStruct"), + New ClassifiedTextRun(ClassificationTypeNames.StructName, "MyStruct", navigationAction:=Sub() Return, "MyStruct"), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "."), - New ClassifiedTextRun(ClassificationTypeNames.EventName, "MyEvent")))) + New ClassifiedTextRun(ClassificationTypeNames.EventName, "MyEvent", navigationAction:=Sub() Return, "readonly event Action MyStruct.MyEvent")))) AssertEqualAdornments(expected, container) End Sub @@ -696,13 +740,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense - Dim codeAnalysisQuickInfoItem = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp) - - Dim trackingSpan = New Mock(Of ITrackingSpan) With { - .DefaultValue = DefaultValue.Mock - } - - Dim intellisenseQuickInfo = Await IntellisenseQuickInfoBuilder.BuildItemAsync(trackingSpan.Object, codeAnalysisQuickInfoItem, Nothing, Nothing, CancellationToken.None) + Dim intellisenseQuickInfo = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp) Assert.NotNull(intellisenseQuickInfo) Dim container = Assert.IsType(Of ContainerElement)(intellisenseQuickInfo.Item) @@ -717,11 +755,11 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense New ClassifiedTextElement( New ClassifiedTextRun(ClassificationTypeNames.Keyword, "void"), New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), - New ClassifiedTextRun(ClassificationTypeNames.ClassName, "MyClass"), + New ClassifiedTextRun(ClassificationTypeNames.ClassName, "MyClass", navigationAction:=Sub() Return, "MyClass"), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "."), - New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod"), + New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod", navigationAction:=Sub() Return, "void MyClass.MyMethod()"), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "<"), - New ClassifiedTextRun(ClassificationTypeNames.Keyword, "int"), + New ClassifiedTextRun(ClassificationTypeNames.Keyword, "int", navigationAction:=Sub() Return, "int"), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, ">"), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "("), New ClassifiedTextRun(ClassificationTypeNames.Punctuation, ")"))), @@ -733,203 +771,5 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense AssertEqualAdornments(expected, container) End Sub - - Private Async Function GetQuickInfoItemAsync(workspaceDefinition As XElement, language As String) As Task(Of QuickInfoItem) - Using workspace = TestWorkspace.Create(workspaceDefinition) - Dim solution = workspace.CurrentSolution - Dim cursorDocument = workspace.Documents.First(Function(d) d.CursorPosition.HasValue) - Dim cursorPosition = cursorDocument.CursorPosition.Value - Dim cursorBuffer = cursorDocument.TextBuffer - - Dim document = workspace.CurrentSolution.GetDocument(cursorDocument.Id) - - Dim languageServiceProvider = workspace.Services.GetLanguageServices(language) - Dim quickInfoService = languageServiceProvider.GetRequiredService(Of QuickInfoService) - - Return Await quickInfoService.GetQuickInfoAsync(document, cursorPosition, CancellationToken.None).ConfigureAwait(False) - End Using - End Function - - Private Shared Sub AssertEqualAdornments(expected As Object, actual As Object) - Try - Assert.IsType(expected.GetType, actual) - - Dim containerElement = TryCast(expected, ContainerElement) - If containerElement IsNot Nothing Then - AssertEqualContainerElement(containerElement, DirectCast(actual, ContainerElement)) - Return - End If - - Dim imageElement = TryCast(expected, ImageElement) - If imageElement IsNot Nothing Then - AssertEqualImageElement(imageElement, DirectCast(actual, ImageElement)) - Return - End If - - Dim classifiedTextElement = TryCast(expected, ClassifiedTextElement) - If classifiedTextElement IsNot Nothing Then - AssertEqualClassifiedTextElement(classifiedTextElement, DirectCast(actual, ClassifiedTextElement)) - Return - End If - - Dim classifiedTextRun = TryCast(expected, ClassifiedTextRun) - If classifiedTextRun IsNot Nothing Then - AssertEqualClassifiedTextRun(classifiedTextRun, DirectCast(actual, ClassifiedTextRun)) - Return - End If - - Throw Roslyn.Utilities.ExceptionUtilities.Unreachable - Catch ex As Exception - Dim renderedExpected = ContainerToString(expected) - Dim renderedActual = ContainerToString(actual) - AssertEx.EqualOrDiff(renderedExpected, renderedActual) - - ' This is not expected to be hit, but it will be hit if the difference cannot be detected within the diff - Throw - End Try - End Sub - - Private Shared Sub AssertEqualContainerElement(expected As ContainerElement, actual As ContainerElement) - Assert.Equal(expected.Style, actual.Style) - Assert.Equal(expected.Elements.Count, actual.Elements.Count) - For Each pair In expected.Elements.Zip(actual.Elements, Function(expectedElement, actualElement) (expectedElement, actualElement)) - AssertEqualAdornments(pair.expectedElement, pair.actualElement) - Next - End Sub - - Private Shared Sub AssertEqualImageElement(expected As ImageElement, actual As ImageElement) - Assert.Equal(expected.ImageId.Guid, actual.ImageId.Guid) - Assert.Equal(expected.ImageId.Id, actual.ImageId.Id) - Assert.Equal(expected.AutomationName, actual.AutomationName) - End Sub - - Private Shared Sub AssertEqualClassifiedTextElement(expected As ClassifiedTextElement, actual As ClassifiedTextElement) - Assert.Equal(expected.Runs.Count, actual.Runs.Count) - For Each pair In expected.Runs.Zip(actual.Runs, Function(expectedRun, actualRun) (expectedRun, actualRun)) - AssertEqualClassifiedTextRun(pair.expectedRun, pair.actualRun) - Next - End Sub - - Private Shared Sub AssertEqualClassifiedTextRun(expected As ClassifiedTextRun, actual As ClassifiedTextRun) - Assert.Equal(expected.ClassificationTypeName, actual.ClassificationTypeName) - Assert.Equal(expected.Text, actual.Text) - End Sub - - Private Shared Function ContainerToString(element As Object) As String - Dim result = New StringBuilder - ContainerToString(element, "", result) - Return result.ToString() - End Function - - Private Shared Sub ContainerToString(element As Object, indent As String, result As StringBuilder) - result.Append($"{indent}New {element.GetType().Name}(") - - Dim container = TryCast(element, ContainerElement) - If container IsNot Nothing Then - result.AppendLine() - indent += " " - result.AppendLine($"{indent}{ContainerStyleToString(container.Style)},") - Dim elements = container.Elements.ToArray() - For i = 0 To elements.Length - 1 - ContainerToString(elements(i), indent, result) - - If i < elements.Length - 1 Then - result.AppendLine(",") - Else - result.Append(")") - End If - Next - - Return - End If - - Dim image = TryCast(element, ImageElement) - If image IsNot Nothing Then - Dim guid = GetKnownImageGuid(image.ImageId.Guid) - Dim id = GetKnownImageId(image.ImageId.Id) - result.Append($"New {NameOf(ImageId)}({guid}, {id}))") - Return - End If - - Dim classifiedTextElement = TryCast(element, ClassifiedTextElement) - If classifiedTextElement IsNot Nothing Then - result.AppendLine() - indent += " " - Dim runs = classifiedTextElement.Runs.ToArray() - For i = 0 To runs.Length - 1 - ContainerToString(runs(i), indent, result) - - If i < runs.Length - 1 Then - result.AppendLine(",") - Else - result.Append(")") - End If - Next - - Return - End If - - Dim classifiedTextRun = TryCast(element, ClassifiedTextRun) - If classifiedTextRun IsNot Nothing Then - Dim classification = GetKnownClassification(classifiedTextRun.ClassificationTypeName) - result.Append($"{classification}, ""{classifiedTextRun.Text.Replace("""", """""")}"")") - Return - End If - - Throw Roslyn.Utilities.ExceptionUtilities.Unreachable - End Sub - - Private Shared Function ContainerStyleToString(style As ContainerElementStyle) As String - Dim stringValue = style.ToString() - Return String.Join(" Or ", stringValue.Split({","c, " "c}, StringSplitOptions.RemoveEmptyEntries).Select(Function(value) $"{NameOf(ContainerElementStyle)}.{value}")) - End Function - - Private Shared Function GetKnownClassification(classification As String) As String - For Each field In GetType(ClassificationTypeNames).GetFields() - If Not field.IsStatic Then - Continue For - End If - - Dim rawValue = field.GetValue(Nothing) - Dim value = TryCast(rawValue, String) - If value = classification Then - Return $"{NameOf(ClassificationTypeNames)}.{field.Name}" - End If - Next - - Return $"""{classification}""" - End Function - - Private Shared Function GetKnownImageGuid(guid As Guid) As String - For Each field In GetType(KnownImageIds).GetFields() - If Not field.IsStatic Then - Continue For - End If - - Dim rawValue = field.GetValue(Nothing) - Dim value As Guid? = If(TypeOf rawValue Is Guid, DirectCast(rawValue, Guid), Nothing) - If value = guid Then - Return $"{NameOf(KnownImageIds)}.{field.Name}" - End If - Next - - Return guid.ToString() - End Function - - Private Shared Function GetKnownImageId(id As Integer) As String - For Each field In GetType(KnownImageIds).GetFields() - If Not field.IsStatic Then - Continue For - End If - - Dim rawValue = field.GetValue(Nothing) - Dim value As Integer? = If(TypeOf rawValue Is Integer, CInt(rawValue), Nothing) - If value = id Then - Return $"{NameOf(KnownImageIds)}.{field.Name}" - End If - Next - - Return id.ToString() - End Function End Class End Namespace diff --git a/src/EditorFeatures/Test2/IntelliSense/IntellisenseQuickInfoBuilderTests_Links.vb b/src/EditorFeatures/Test2/IntelliSense/IntellisenseQuickInfoBuilderTests_Links.vb new file mode 100644 index 000000000000..833be90f8ef2 --- /dev/null +++ b/src/EditorFeatures/Test2/IntelliSense/IntellisenseQuickInfoBuilderTests_Links.vb @@ -0,0 +1,106 @@ +' 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 + + + + + Public Async Sub QuickInfoForPlainHyperlink(tag As String) + Dim workspace = + + + + using System.Threading; + class MyClass { + /// <summary> + /// This contains a link to <<%= tag %> href="https://github.com/dotnet/roslyn"/>. + /// </summary> + void MyMethod() { + MyM$$ethod(); + } + } + + + + + 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", navigationAction:=Sub() Return, "MyClass"), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "."), + New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod", navigationAction:=Sub() Return, "void MyClass.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 + + + + + Public Async Sub QuickInfoForHyperlinkWithText(tag As String) + Dim workspace = + + + + using System.Threading; + class MyClass { + /// <summary> + /// This contains a link to <<%= tag %> href="https://github.com/dotnet/roslyn">dotnet/roslyn</<%= tag %>>. + /// </summary> + void MyMethod() { + MyM$$ethod(); + } + } + + + + + 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", navigationAction:=Sub() Return, "MyClass"), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "."), + New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod", navigationAction:=Sub() Return, "void MyClass.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 diff --git a/src/EditorFeatures/Test2/IntelliSense/IntellisenseQuickInfoBuilderTests_Lists.vb b/src/EditorFeatures/Test2/IntelliSense/IntellisenseQuickInfoBuilderTests_Lists.vb new file mode 100644 index 000000000000..65aa554f2417 --- /dev/null +++ b/src/EditorFeatures/Test2/IntelliSense/IntellisenseQuickInfoBuilderTests_Lists.vb @@ -0,0 +1,381 @@ +' 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_Lists + Inherits AbstractIntellisenseQuickInfoBuilderTests + + + + + Public Async Sub QuickInfoForBulletedList(itemTags As String()) + Dim openItemTag = String.Join("", itemTags.Select(Function(tag) $"<{tag}>")) + Dim closeItemTag = String.Join("", itemTags.Reverse().Select(Function(tag) $"")) + Dim workspace = + + + + using System.Threading; + class MyClass { + /// <summary> + /// <list type="bullet"> + /// <%= openItemTag %>Item 1<%= closeItemTag %> + /// <%= openItemTag %>Item 2<%= closeItemTag %> + /// </list> + /// </summary> + void MyMethod() { + MyM$$ethod(); + } + } + + + + + 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", navigationAction:=Sub() Return, "MyClass"), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "."), + New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod", navigationAction:=Sub() Return, "void MyClass.MyMethod()"), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "("), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, ")"))), + New ContainerElement( + ContainerElementStyle.Wrapped, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "• ")), + New ContainerElement( + ContainerElementStyle.Stacked, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "Item 1"))))), + New ContainerElement( + ContainerElementStyle.Wrapped, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "• ")), + New ContainerElement( + ContainerElementStyle.Stacked, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "Item 2"))))) + + AssertEqualAdornments(expected, intellisenseQuickInfo.Item) + End Sub + + + + + Public Async Sub QuickInfoForNumberedList(itemTags As String()) + Dim openItemTag = String.Join("", itemTags.Select(Function(tag) $"<{tag}>")) + Dim closeItemTag = String.Join("", itemTags.Reverse().Select(Function(tag) $"")) + Dim workspace = + + + + using System.Threading; + class MyClass { + /// <summary> + /// <list type="number"> + /// <%= openItemTag %>Item 1<%= closeItemTag %> + /// <%= openItemTag %>Item 2<%= closeItemTag %> + /// </list> + /// </summary> + void MyMethod() { + MyM$$ethod(); + } + } + + + + + 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", navigationAction:=Sub() Return, "MyClass"), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "."), + New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod", navigationAction:=Sub() Return, "void MyClass.MyMethod()"), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "("), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, ")"))), + New ContainerElement( + ContainerElementStyle.Wrapped, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "1. ")), + New ContainerElement( + ContainerElementStyle.Stacked, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "Item 1"))))), + New ContainerElement( + ContainerElementStyle.Wrapped, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "2. ")), + New ContainerElement( + ContainerElementStyle.Stacked, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "Item 2"))))) + + AssertEqualAdornments(expected, intellisenseQuickInfo.Item) + End Sub + + + Public Async Sub QuickInfoForBulletedTermList() + Dim workspace = + + + + using System.Threading; + class MyClass { + /// <summary> + /// <list type="bullet"> + /// <item><term>word1</term><description>Item 1</description></item> + /// <item><term>word2</term><description>Item 2</description></item> + /// </list> + /// </summary> + void MyMethod() { + MyM$$ethod(); + } + } + + + + + 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", navigationAction:=Sub() Return, "MyClass"), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "."), + New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod", navigationAction:=Sub() Return, "void MyClass.MyMethod()"), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "("), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, ")"))), + New ContainerElement( + ContainerElementStyle.Wrapped, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "• ")), + New ContainerElement( + ContainerElementStyle.Stacked, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "word1", ClassifiedTextRunStyle.Bold), + New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), + New ClassifiedTextRun(ClassificationTypeNames.Text, "–"), + New ClassifiedTextRun(ClassificationTypeNames.Text, "Item 1"))))), + New ContainerElement( + ContainerElementStyle.Wrapped, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "• ")), + New ContainerElement( + ContainerElementStyle.Stacked, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "word2", ClassifiedTextRunStyle.Bold), + New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), + New ClassifiedTextRun(ClassificationTypeNames.Text, "–"), + New ClassifiedTextRun(ClassificationTypeNames.Text, "Item 2"))))) + + AssertEqualAdornments(expected, intellisenseQuickInfo.Item) + End Sub + + + Public Async Sub QuickInfoForNumberedTermList() + Dim workspace = + + + + using System.Threading; + class MyClass { + /// <summary> + /// <list type="number"> + /// <item><term>word1</term><description>Item 1</description></item> + /// <item><term>word2</term><description>Item 2</description></item> + /// </list> + /// </summary> + void MyMethod() { + MyM$$ethod(); + } + } + + + + + 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", navigationAction:=Sub() Return, "MyClass"), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "."), + New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod", navigationAction:=Sub() Return, "void MyClass.MyMethod()"), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "("), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, ")"))), + New ContainerElement( + ContainerElementStyle.Wrapped, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "1. ")), + New ContainerElement( + ContainerElementStyle.Stacked, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "word1", ClassifiedTextRunStyle.Bold), + New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), + New ClassifiedTextRun(ClassificationTypeNames.Text, "–"), + New ClassifiedTextRun(ClassificationTypeNames.Text, "Item 1"))))), + New ContainerElement( + ContainerElementStyle.Wrapped, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "2. ")), + New ContainerElement( + ContainerElementStyle.Stacked, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "word2", ClassifiedTextRunStyle.Bold), + New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), + New ClassifiedTextRun(ClassificationTypeNames.Text, "–"), + New ClassifiedTextRun(ClassificationTypeNames.Text, "Item 2"))))) + + AssertEqualAdornments(expected, intellisenseQuickInfo.Item) + End Sub + + + + + Public Async Sub QuickInfoForNestedLists(itemTags As String()) + Dim openItemTag = String.Join("", itemTags.Select(Function(tag) $"<{tag}>")) + Dim closeItemTag = String.Join("", itemTags.Reverse().Select(Function(tag) $"")) + Dim workspace = + + + + using System.Threading; + class MyClass { + /// <summary> + /// <list type="number"> + /// <%= openItemTag %> + /// <list type="bullet"> + /// <%= openItemTag %><para>Line1</para><para>Line2</para><%= closeItemTag %> + /// <%= openItemTag %>Item 1.2<%= closeItemTag %> + /// </list> + /// <%= closeItemTag %> + /// <%= openItemTag %> + /// <list type="number"> + /// <%= openItemTag %>Item 2.1<%= closeItemTag %> + /// <%= openItemTag %><para>Line1</para><para>Line2</para><%= closeItemTag %> + /// </list> + /// <%= closeItemTag %> + /// </list> + /// </summary> + void MyMethod() { + MyM$$ethod(); + } + } + + + + + 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", navigationAction:=Sub() Return, "MyClass"), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "."), + New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod", navigationAction:=Sub() Return, "void MyClass.MyMethod()"), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "("), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, ")"))), + New ContainerElement( + ContainerElementStyle.Wrapped, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "1. ")), + New ContainerElement( + ContainerElementStyle.Stacked, + New ContainerElement( + ContainerElementStyle.Wrapped, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "• ")), + New ContainerElement( + ContainerElementStyle.Stacked, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "Line1")), + New ContainerElement( + ContainerElementStyle.Stacked Or ContainerElementStyle.VerticalPadding, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "Line2"))))), + New ContainerElement( + ContainerElementStyle.Stacked Or ContainerElementStyle.VerticalPadding, + New ContainerElement( + ContainerElementStyle.Wrapped, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "• ")), + New ContainerElement( + ContainerElementStyle.Stacked, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "Item 1.2")))))))), + New ContainerElement( + ContainerElementStyle.Wrapped, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "2. ")), + New ContainerElement( + ContainerElementStyle.Stacked, + New ContainerElement( + ContainerElementStyle.Wrapped, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "1. ")), + New ContainerElement( + ContainerElementStyle.Stacked, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "Item 2.1")))), + New ContainerElement( + ContainerElementStyle.Stacked Or ContainerElementStyle.VerticalPadding, + New ContainerElement( + ContainerElementStyle.Wrapped, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "2. ")), + New ContainerElement( + ContainerElementStyle.Stacked, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "Line1")), + New ContainerElement( + ContainerElementStyle.Stacked Or ContainerElementStyle.VerticalPadding, + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "Line2"))))))))) + + AssertEqualAdornments(expected, intellisenseQuickInfo.Item) + End Sub + End Class +End Namespace diff --git a/src/EditorFeatures/Test2/IntelliSense/IntellisenseQuickInfoBuilderTests_Styles.vb b/src/EditorFeatures/Test2/IntelliSense/IntellisenseQuickInfoBuilderTests_Styles.vb new file mode 100644 index 000000000000..a0e149d0166e --- /dev/null +++ b/src/EditorFeatures/Test2/IntelliSense/IntellisenseQuickInfoBuilderTests_Styles.vb @@ -0,0 +1,69 @@ +' 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_Styles + Inherits AbstractIntellisenseQuickInfoBuilderTests + + + + + + + + + + + + + Public Async Sub QuickInfoForStylizedText(styleTags As String(), style As ClassifiedTextRunStyle) + Dim openStyleTag = String.Join("", styleTags.Select(Function(tag) $"<{tag}>")) + Dim closeStyleTag = String.Join("", styleTags.Reverse().Select(Function(tag) $"")) + Dim workspace = + + + + using System.Threading; + class MyClass { + /// <summary> + /// This is some <%= openStyleTag %>stylized text<%= closeStyleTag %>. + /// </summary> + void MyMethod() { + MyM$$ethod(); + } + } + + + + + 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", navigationAction:=Sub() Return, "MyClass"), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "."), + New ClassifiedTextRun(ClassificationTypeNames.MethodName, "MyMethod", navigationAction:=Sub() Return, "void MyClass.MyMethod()"), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, "("), + New ClassifiedTextRun(ClassificationTypeNames.Punctuation, ")"))), + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Text, "This is some"), + New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), + New ClassifiedTextRun(ClassificationTypeNames.Text, "stylized text", style), + New ClassifiedTextRun(ClassificationTypeNames.Text, ".")))) + + AssertEqualAdornments(expected, intellisenseQuickInfo.Item) + End Sub + End Class +End Namespace diff --git a/src/Features/CSharp/Portable/SignatureHelp/AbstractCSharpSignatureHelpProvider.cs b/src/Features/CSharp/Portable/SignatureHelp/AbstractCSharpSignatureHelpProvider.cs index e53bf4f4752e..bfa0e2506a6d 100644 --- a/src/Features/CSharp/Portable/SignatureHelp/AbstractCSharpSignatureHelpProvider.cs +++ b/src/Features/CSharp/Portable/SignatureHelp/AbstractCSharpSignatureHelpProvider.cs @@ -67,7 +67,7 @@ protected IList GetAwaitableUsage(IMethodSymbol method, SemanticMode if (method.IsAwaitableNonDynamic(semanticModel, position)) { return method.ToAwaitableParts(SyntaxFacts.GetText(SyntaxKind.AwaitKeyword), "x", semanticModel, position) - .ToTaggedText(); + .ToTaggedText(TaggedTextStyle.None); } return SpecializedCollections.EmptyList(); diff --git a/src/Features/Core/Portable/Common/TaggedText.cs b/src/Features/Core/Portable/Common/TaggedText.cs index e70273c6c33a..0948ff100746 100644 --- a/src/Features/Core/Portable/Common/TaggedText.cs +++ b/src/Features/Core/Portable/Common/TaggedText.cs @@ -26,15 +26,48 @@ public readonly struct TaggedText /// public string Text { get; } + /// + /// Gets the style(s) to apply to the text. + /// + internal TaggedTextStyle Style { get; } + + /// + /// Gets the navigation target for the text, or if the text does not have a navigation + /// target. + /// + internal string NavigationTarget { get; } + + /// + /// Gets the navigation hint for the text, or if the text does not have a navigation + /// hint. + /// + internal string NavigationHint { get; } + /// /// Creates a new instance of /// /// A descriptive tag from . /// The actual text to be displayed. public TaggedText(string tag, string text) + : this(tag, text, TaggedTextStyle.None, navigationTarget: null, navigationHint: null) + { + } + + /// + /// Creates a new instance of + /// + /// A descriptive tag from . + /// The actual text to be displayed. + /// The style(s) to apply to the text. + /// The navigation target for the text, or if the text does not have a navigation target. + /// The navigation hint for the text, or if the text does not have a navigation hint. + internal TaggedText(string tag, string text, TaggedTextStyle style, string navigationTarget, string navigationHint) { Tag = tag ?? throw new ArgumentNullException(nameof(tag)); Text = text ?? throw new ArgumentNullException(nameof(text)); + Style = style; + NavigationTarget = navigationTarget; + NavigationHint = navigationHint; } public override string ToString() @@ -46,14 +79,35 @@ public override string ToString() internal static class TaggedTextExtensions { public static ImmutableArray ToTaggedText(this IEnumerable displayParts) + => displayParts.ToTaggedText(TaggedTextStyle.None); + + public static ImmutableArray ToTaggedText(this IEnumerable displayParts, TaggedTextStyle style) { if (displayParts == null) { return ImmutableArray.Empty; } - return displayParts.Select(d => - new TaggedText(SymbolDisplayPartKindTags.GetTag(d.Kind), d.ToString())).ToImmutableArray(); + return displayParts.SelectAsArray(d => + new TaggedText( + SymbolDisplayPartKindTags.GetTag(d.Kind), + d.ToString(), + style, + GetNavigationTarget(d.Symbol), + GetNavigationHint(d.Symbol))); + + static string GetNavigationTarget(ISymbol symbol) + { + if (symbol is null) + { + return null; + } + + return SymbolKey.CreateString(symbol); + } + + static string GetNavigationHint(ISymbol symbol) + => symbol?.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); } public static string JoinText(this ImmutableArray values) diff --git a/src/Features/Core/Portable/Common/TaggedTextStyle.cs b/src/Features/Core/Portable/Common/TaggedTextStyle.cs new file mode 100644 index 000000000000..655a6eb1f51f --- /dev/null +++ b/src/Features/Core/Portable/Common/TaggedTextStyle.cs @@ -0,0 +1,20 @@ +// 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; + +namespace Microsoft.CodeAnalysis +{ + [Flags] + internal enum TaggedTextStyle + { + None = 0, + + Strong = 0x1, + + Emphasis = 0x2, + + Underline = 0x4, + + Code = 0x8, + } +} diff --git a/src/Features/Core/Portable/Common/TextTags.cs b/src/Features/Core/Portable/Common/TextTags.cs index beb5615a34fd..71aadc66a7b4 100644 --- a/src/Features/Core/Portable/Common/TextTags.cs +++ b/src/Features/Core/Portable/Common/TextTags.cs @@ -39,5 +39,19 @@ public static class TextTags public const string EnumMember = nameof(EnumMember); public const string ExtensionMethod = nameof(ExtensionMethod); public const string Constant = nameof(Constant); + + /// + /// Indicates the start of a text container. The elements after through (but not + /// including) the matching are rendered in a rectangular block which is positioned + /// as an inline element relative to surrounding elements. The text of the element + /// itself precedes the content of the container, and is typically a bullet or number header for an item in a + /// list. + /// + internal const string ContainerStart = nameof(ContainerStart); + + /// + /// Indicates the end of a text container. See . + /// + internal const string ContainerEnd = nameof(ContainerEnd); } } diff --git a/src/Features/Core/Portable/DocumentationComments/AbstractDocumentationCommentFormattingService.cs b/src/Features/Core/Portable/DocumentationComments/AbstractDocumentationCommentFormattingService.cs index 8a1cb9c43246..586188366c63 100644 --- a/src/Features/Core/Portable/DocumentationComments/AbstractDocumentationCommentFormattingService.cs +++ b/src/Features/Core/Portable/DocumentationComments/AbstractDocumentationCommentFormattingService.cs @@ -11,6 +11,14 @@ namespace Microsoft.CodeAnalysis.DocumentationComments { internal abstract class AbstractDocumentationCommentFormattingService : IDocumentationCommentFormattingService { + private enum DocumentationCommentListType + { + None, + Bullet, + Number, + Table, + } + private class FormatterState { private bool _anyNonWhitespaceSinceLastPara; @@ -23,6 +31,48 @@ private class FormatterState internal readonly List Builder = new List(); + /// + /// Defines the containing lists for the current formatting state. The last item in the list is the + /// innermost list. + /// + /// + /// + /// type + /// The type of list. + /// + /// + /// index + /// The index of the current item in the list. + /// + /// + /// renderedItem + /// if the label (a bullet or number) for the current list item has already been rendered; otherwise . + /// + /// + /// + private readonly List<(DocumentationCommentListType type, int index, bool renderedItem)> _listStack = new List<(DocumentationCommentListType type, int index, bool renderedItem)>(); + + /// + /// The top item of the stack indicates the hyperlink to apply to text rendered at the current location. It + /// consists of a navigation target (the destination to navigate to when clicked) and a hint + /// (typically shown as a tooltip for the link). This stack is never empty; when no hyperlink applies to the + /// current scope, the top item of the stack will be a default tuple instance. + /// + private readonly Stack<(string target, string hint)> _navigationTargetStack = new Stack<(string target, string hint)>(); + + /// + /// Tracks the style for text. The top item of the stack is the current style to apply (the merged result of + /// all containing styles). This stack is never empty; when no style applies to the current scope, the top + /// item of the stack will be . + /// + private readonly Stack _styleStack = new Stack(); + + public FormatterState() + { + _navigationTargetStack.Push(default); + _styleStack.Push(TaggedTextStyle.None); + } + internal SemanticModel SemanticModel { get; set; } internal int Position { get; set; } @@ -36,6 +86,9 @@ public bool AtBeginning public SymbolDisplayFormat Format { get; internal set; } + internal (string target, string hint) NavigationTarget => _navigationTargetStack.Peek(); + internal TaggedTextStyle Style => _styleStack.Peek(); + public void AppendSingleSpace() { _pendingSingleSpace = true; @@ -45,7 +98,7 @@ public void AppendString(string s) { EmitPendingChars(); - Builder.Add(new TaggedText(TextTags.Text, s)); + Builder.Add(new TaggedText(TextTags.Text, s, Style, NavigationTarget.target, NavigationTarget.hint)); _anyNonWhitespaceSinceLastPara = true; } @@ -59,6 +112,70 @@ public void AppendParts(IEnumerable parts) _anyNonWhitespaceSinceLastPara = true; } + public void PushList(DocumentationCommentListType listType) + { + _listStack.Add((listType, index: 0, renderedItem: false)); + MarkBeginOrEndPara(); + } + + /// + /// Marks the start of an item in a list; called before each item. + /// + public void NextListItem() + { + if (_listStack.Count == 0) + { + return; + } + + var (type, index, renderedItem) = _listStack[_listStack.Count - 1]; + if (renderedItem) + { + // Mark the end of the previous list item + Builder.Add(new TaggedText(TextTags.ContainerEnd, string.Empty)); + } + + // The next list item has an incremented index, and has not yet been rendered to Builder. + _listStack[_listStack.Count - 1] = (type, index + 1, renderedItem: false); + MarkLineBreak(); + } + + public void PopList() + { + if (_listStack.Count == 0) + { + return; + } + + if (_listStack[_listStack.Count - 1].renderedItem) + { + Builder.Add(new TaggedText(TextTags.ContainerEnd, string.Empty)); + } + + _listStack.RemoveAt(_listStack.Count - 1); + MarkBeginOrEndPara(); + } + + public void PushNavigationTarget(string target, string hint) + { + _navigationTargetStack.Push((target, hint)); + } + + public void PopNavigationTarget() + { + _navigationTargetStack.Pop(); + } + + public void PushStyle(TaggedTextStyle style) + { + _styleStack.Push(_styleStack.Peek() | style); + } + + public void PopStyle() + { + _styleStack.Pop(); + } + public void MarkBeginOrEndPara() { // If this is a with nothing before it, then skip it. @@ -120,6 +237,33 @@ private void EmitPendingChars() _pendingParagraphBreak = false; _pendingLineBreak = false; _pendingSingleSpace = false; + + for (var i = 0; i < _listStack.Count; i++) + { + if (_listStack[i].renderedItem) + { + continue; + } + + switch (_listStack[i].type) + { + case DocumentationCommentListType.Bullet: + Builder.Add(new TaggedText(TextTags.ContainerStart, "• ")); + break; + + case DocumentationCommentListType.Number: + Builder.Add(new TaggedText(TextTags.ContainerStart, $"{_listStack[i].index}. ")); + break; + + case DocumentationCommentListType.Table: + case DocumentationCommentListType.None: + default: + Builder.Add(new TaggedText(TextTags.ContainerStart, string.Empty)); + break; + } + + _listStack[i] = (_listStack[i].type, _listStack[i].index, renderedItem: true); + } } } @@ -178,9 +322,12 @@ private static void AppendTextFromNode(FormatterState state, XNode node, Compila var element = (XElement)node; var name = element.Name.LocalName; + var needPopStyle = false; + (string target, string hint)? navigationTarget = null; if (name == DocumentationCommentXmlNames.SeeElementName || - name == DocumentationCommentXmlNames.SeeAlsoElementName) + name == DocumentationCommentXmlNames.SeeAlsoElementName || + name == "a") { if (element.IsEmpty || element.FirstNode == null) { @@ -191,6 +338,14 @@ private static void AppendTextFromNode(FormatterState state, XNode node, Compila return; } + else + { + navigationTarget = GetNavigationTarget(element, state.SemanticModel, state.Position, state.Format); + if (navigationTarget is object) + { + state.PushNavigationTarget(navigationTarget.Value.target, navigationTarget.Value.hint); + } + } } else if (name == DocumentationCommentXmlNames.ParameterReferenceElementName || name == DocumentationCommentXmlNames.TypeParameterReferenceElementName) @@ -203,8 +358,61 @@ private static void AppendTextFromNode(FormatterState state, XNode node, Compila return; } + else if (name == DocumentationCommentXmlNames.CElementName + || name == DocumentationCommentXmlNames.CodeElementName + || name == "tt") + { + needPopStyle = true; + state.PushStyle(TaggedTextStyle.Code); + } + else if (name == "em" || name == "i") + { + needPopStyle = true; + state.PushStyle(TaggedTextStyle.Emphasis); + } + else if (name == "strong" || name == "b" || name == DocumentationCommentXmlNames.TermElementName) + { + needPopStyle = true; + state.PushStyle(TaggedTextStyle.Strong); + } + else if (name == "u") + { + needPopStyle = true; + state.PushStyle(TaggedTextStyle.Underline); + } - if (name == DocumentationCommentXmlNames.ParaElementName) + if (name == DocumentationCommentXmlNames.ListElementName) + { + var rawListType = element.Attribute(DocumentationCommentXmlNames.TypeAttributeName)?.Value; + DocumentationCommentListType listType; + switch (rawListType) + { + case "table": + listType = DocumentationCommentListType.Table; + break; + + case "number": + listType = DocumentationCommentListType.Number; + break; + + case "bullet": + listType = DocumentationCommentListType.Bullet; + break; + + default: + listType = DocumentationCommentListType.None; + break; + } + + state.PushList(listType); + } + else if (name == DocumentationCommentXmlNames.ItemElementName) + { + state.NextListItem(); + } + + if (name == DocumentationCommentXmlNames.ParaElementName + || name == DocumentationCommentXmlNames.CodeElementName) { state.MarkBeginOrEndPara(); } @@ -218,10 +426,56 @@ private static void AppendTextFromNode(FormatterState state, XNode node, Compila AppendTextFromNode(state, childNode, compilation); } - if (name == DocumentationCommentXmlNames.ParaElementName) + if (name == DocumentationCommentXmlNames.ParaElementName + || name == DocumentationCommentXmlNames.CodeElementName) { state.MarkBeginOrEndPara(); } + + if (name == DocumentationCommentXmlNames.ListElementName) + { + state.PopList(); + } + + if (needPopStyle) + { + state.PopStyle(); + } + + if (navigationTarget is object) + { + state.PopNavigationTarget(); + } + + if (name == DocumentationCommentXmlNames.TermElementName) + { + state.AppendSingleSpace(); + state.AppendString("–"); + } + } + + private static (string target, string hint)? GetNavigationTarget(XElement element, SemanticModel semanticModel, int position, SymbolDisplayFormat format) + { + var crefAttribute = element.Attribute(DocumentationCommentXmlNames.CrefAttributeName); + if (crefAttribute is object) + { + if (semanticModel is object) + { + var symbol = DocumentationCommentId.GetFirstSymbolForDeclarationId(crefAttribute.Value, semanticModel.Compilation); + if (symbol is object) + { + return (target: SymbolKey.CreateString(symbol), hint: symbol.ToMinimalDisplayString(semanticModel, position, format ?? SymbolDisplayFormat.MinimallyQualifiedFormat)); + } + } + } + + var hrefAttribute = element.Attribute(DocumentationCommentXmlNames.HrefAttributeName); + if (hrefAttribute is object) + { + return (target: hrefAttribute.Value, hint: hrefAttribute.Value); + } + + return null; } private static void AppendTextFromAttribute(FormatterState state, XElement element, XAttribute attribute, string attributeNameToParse, SymbolDisplayPartKind kind) @@ -230,14 +484,20 @@ private static void AppendTextFromAttribute(FormatterState state, XElement eleme if (attributeNameToParse == attributeName) { state.AppendParts( - CrefToSymbolDisplayParts(attribute.Value, state.Position, state.SemanticModel, state.Format, kind).ToTaggedText()); + CrefToSymbolDisplayParts(attribute.Value, state.Position, state.SemanticModel, state.Format, kind).ToTaggedText(state.Style)); } else { var displayKind = attributeName == DocumentationCommentXmlNames.LangwordAttributeName ? TextTags.Keyword : TextTags.Text; - state.AppendParts(SpecializedCollections.SingletonEnumerable(new TaggedText(displayKind, attribute.Value))); + var text = attribute.Value; + var style = state.Style; + var navigationTarget = attributeName == DocumentationCommentXmlNames.HrefAttributeName + ? attribute.Value + : null; + var navigationHint = navigationTarget; + state.AppendParts(SpecializedCollections.SingletonEnumerable(new TaggedText(displayKind, text, style, navigationTarget, navigationHint))); } } diff --git a/src/VisualStudio/CSharp/Impl/EventHookup/EventHookupSessionManager.cs b/src/VisualStudio/CSharp/Impl/EventHookup/EventHookupSessionManager.cs index ee33a49d88f6..e20ec1c8df4f 100644 --- a/src/VisualStudio/CSharp/Impl/EventHookup/EventHookupSessionManager.cs +++ b/src/VisualStudio/CSharp/Impl/EventHookup/EventHookupSessionManager.cs @@ -54,9 +54,12 @@ internal void EventHookupFoundInSession(EventHookupSession analyzedSession) // tooltips text is: Program_MyEvents; (Press TAB to insert) // GetEventNameTask() gets back the event name, only needs to add a semicolon after it. - var eventText = analyzedSession.GetEventNameTask.Result + ";"; - var texts = new[] { eventText, CSharpEditorResources.Press_TAB_to_insert }; - var textRuns = texts.Select(s => new ClassifiedTextRun(ClassificationTypeNames.Text, s)); + var textRuns = new[] + { + new ClassifiedTextRun(ClassificationTypeNames.MethodName, analyzedSession.GetEventNameTask.Result, ClassifiedTextRunStyle.UseClassificationFont), + new ClassifiedTextRun(ClassificationTypeNames.Punctuation, ";", ClassifiedTextRunStyle.UseClassificationFont), + new ClassifiedTextRun(ClassificationTypeNames.Text, CSharpEditorResources.Press_TAB_to_insert), + }; var content = new[] { new ClassifiedTextElement(textRuns) }; _toolTipPresenter.StartOrUpdate(analyzedSession.TrackingSpan, content); diff --git a/src/VisualStudio/CSharp/Test/EventHookup/EventHookupCommandHandlerTests.cs b/src/VisualStudio/CSharp/Test/EventHookup/EventHookupCommandHandlerTests.cs index ed431722c244..bee31ebc426e 100644 --- a/src/VisualStudio/CSharp/Test/EventHookup/EventHookupCommandHandlerTests.cs +++ b/src/VisualStudio/CSharp/Test/EventHookup/EventHookupCommandHandlerTests.cs @@ -28,7 +28,7 @@ void M() using var testState = EventHookupTestState.CreateTestState(markup); testState.SendTypeChar('='); await testState.WaitForAsynchronousOperationsAsync(); - testState.AssertShowing("C_MyEvent;"); + testState.AssertShowing("C_MyEvent"); } [WpfFact, Trait(Traits.Feature, Traits.Features.EventHookup)] @@ -52,7 +52,7 @@ void M() using var testState = EventHookupTestState.CreateTestState(markup); testState.SendTypeChar('='); await testState.WaitForAsynchronousOperationsAsync(); - testState.AssertShowing("Local_MyEvent;"); + testState.AssertShowing("Local_MyEvent"); } [WpfFact, Trait(Traits.Feature, Traits.Features.EventHookup)] @@ -81,7 +81,7 @@ void Goo() using var testState = EventHookupTestState.CreateTestState(markup); testState.SendTypeChar('='); await testState.WaitForAsynchronousOperationsAsync(); - testState.AssertShowing("Cfield_MyEvent;"); + testState.AssertShowing("Cfield_MyEvent"); } [WpfFact, Trait(Traits.Feature, Traits.Features.EventHookup)] @@ -138,7 +138,7 @@ private void C_MyEvent() using var testState = EventHookupTestState.CreateTestState(markup); testState.SendTypeChar('='); await testState.WaitForAsynchronousOperationsAsync(); - testState.AssertShowing("C_MyEvent1;"); + testState.AssertShowing("C_MyEvent1"); } [WpfFact, Trait(Traits.Feature, Traits.Features.EventHookup)] @@ -162,7 +162,7 @@ private static void C_MyEvent() using var testState = EventHookupTestState.CreateTestState(markup); testState.SendTypeChar('='); await testState.WaitForAsynchronousOperationsAsync(); - testState.AssertShowing("C_MyEvent1;"); + testState.AssertShowing("C_MyEvent1"); } [WpfFact, Trait(Traits.Feature, Traits.Features.EventHookup)] @@ -182,7 +182,7 @@ void M(string[] args) using var testState = EventHookupTestState.CreateTestState(markup); testState.SendTypeChar('='); await testState.WaitForAsynchronousOperationsAsync(); - testState.AssertShowing("C_MyEvent1;"); + testState.AssertShowing("C_MyEvent1"); } [WpfFact, Trait(Traits.Feature, Traits.Features.EventHookup)] @@ -202,7 +202,7 @@ void Goo() using var testState = EventHookupTestState.CreateTestState(markup); testState.SendTypeChar('='); await testState.WaitForAsynchronousOperationsAsync(); - testState.AssertShowing("C_MyEvent;"); + testState.AssertShowing("C_MyEvent"); } [WpfFact, Trait(Traits.Feature, Traits.Features.EventHookup)] @@ -220,11 +220,11 @@ void M() using var testState = EventHookupTestState.CreateTestState(markup); testState.SendTypeChar('='); await testState.WaitForAsynchronousOperationsAsync(); - testState.AssertShowing("C_MyEvent;"); + testState.AssertShowing("C_MyEvent"); testState.SendTypeChar(' '); await testState.WaitForAsynchronousOperationsAsync(); - testState.AssertShowing("C_MyEvent;"); + testState.AssertShowing("C_MyEvent"); } [WpfFact, Trait(Traits.Feature, Traits.Features.EventHookup)] @@ -242,7 +242,7 @@ void M() using var testState = EventHookupTestState.CreateTestState(markup); testState.SendTypeChar('='); await testState.WaitForAsynchronousOperationsAsync(); - testState.AssertShowing("C_MyEvent;"); + testState.AssertShowing("C_MyEvent"); testState.SendTypeChar('d'); await testState.WaitForAsynchronousOperationsAsync(); @@ -264,7 +264,7 @@ void M() using var testState = EventHookupTestState.CreateTestState(markup); testState.SendTypeChar('='); await testState.WaitForAsynchronousOperationsAsync(); - testState.AssertShowing("C_MyEvent;"); + testState.AssertShowing("C_MyEvent"); testState.SendTypeChar('='); await testState.WaitForAsynchronousOperationsAsync(); @@ -286,15 +286,15 @@ void M() using var testState = EventHookupTestState.CreateTestState(markup); testState.SendTypeChar('='); await testState.WaitForAsynchronousOperationsAsync(); - testState.AssertShowing("C_MyEvent;"); + testState.AssertShowing("C_MyEvent"); testState.SendTypeChar(' '); await testState.WaitForAsynchronousOperationsAsync(); - testState.AssertShowing("C_MyEvent;"); + testState.AssertShowing("C_MyEvent"); testState.SendLeftKey(); await testState.WaitForAsynchronousOperationsAsync(); - testState.AssertShowing("C_MyEvent;"); + testState.AssertShowing("C_MyEvent"); testState.SendLeftKey(); await testState.WaitForAsynchronousOperationsAsync(); @@ -317,11 +317,11 @@ void M() testState.SendTypeChar('='); await testState.WaitForAsynchronousOperationsAsync(); - testState.AssertShowing("C_MyEvent;"); + testState.AssertShowing("C_MyEvent"); testState.SendTypeChar(' '); await testState.WaitForAsynchronousOperationsAsync(); - testState.AssertShowing("C_MyEvent;"); + testState.AssertShowing("C_MyEvent"); testState.SendBackspace(); await testState.WaitForAsynchronousOperationsAsync(); @@ -718,7 +718,7 @@ private void C_MyEvent() using var testState = EventHookupTestState.CreateTestState(markup); testState.SendTypeChar('='); await testState.WaitForAsynchronousOperationsAsync(); - testState.AssertShowing("C_MyEvent1;"); + testState.AssertShowing("C_MyEvent1"); } [WpfFact, Trait(Traits.Feature, Traits.Features.EventHookup)] @@ -740,7 +740,7 @@ void Main(string[] args) using var testState = EventHookupTestState.CreateTestState(markup); testState.SendTypeChar('='); await testState.WaitForAsynchronousOperationsAsync(); - testState.AssertShowing("Console_CancelKeyPress1;"); + testState.AssertShowing("Console_CancelKeyPress1"); } [WpfFact, Trait(Traits.Feature, Traits.Features.EventHookup)] @@ -759,7 +759,7 @@ void M(int C_MyEvent) using var testState = EventHookupTestState.CreateTestState(markup); testState.SendTypeChar('='); await testState.WaitForAsynchronousOperationsAsync(); - testState.AssertShowing("C_MyEvent1;"); + testState.AssertShowing("C_MyEvent1"); } [WpfFact, Trait(Traits.Feature, Traits.Features.EventHookup)] diff --git a/src/VisualStudio/CSharp/Test/EventHookup/EventHookupTestState.cs b/src/VisualStudio/CSharp/Test/EventHookup/EventHookupTestState.cs index 5bee8223a1d5..2a0b7efb476d 100644 --- a/src/VisualStudio/CSharp/Test/EventHookup/EventHookupTestState.cs +++ b/src/VisualStudio/CSharp/Test/EventHookup/EventHookupTestState.cs @@ -59,7 +59,7 @@ internal void AssertShowing(string expectedText) Assert.Single(_commandHandler.EventHookupSessionManager.TEST_MostRecentToolTipContent); var textElement = _commandHandler.EventHookupSessionManager.TEST_MostRecentToolTipContent.First(); - Assert.Equal(2, textElement.Runs.Count()); + Assert.Equal(3, textElement.Runs.Count()); Assert.Equal(expectedText, textElement.Runs.First().Text); } diff --git a/src/VisualStudio/Core/Def/Implementation/Utilities/VisualStudioNavigateToLinkService.cs b/src/VisualStudio/Core/Def/Implementation/Utilities/VisualStudioNavigateToLinkService.cs new file mode 100644 index 000000000000..36dd3618c776 --- /dev/null +++ b/src/VisualStudio/Core/Def/Implementation/Utilities/VisualStudioNavigateToLinkService.cs @@ -0,0 +1,39 @@ +// 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.Editor; +using Microsoft.CodeAnalysis.Host.Mef; +using Roslyn.Utilities; + +namespace Microsoft.VisualStudio.LanguageServices.Implementation.Utilities +{ + [ExportWorkspaceService(typeof(INavigateToLinkService), layer: ServiceLayer.Host)] + [Shared] + internal sealed class VisualStudioNavigateToLinkService : INavigateToLinkService + { + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public VisualStudioNavigateToLinkService() + { + } + + public Task TryNavigateToLinkAsync(Uri uri, CancellationToken cancellationToken) + { + if (!uri.IsAbsoluteUri) + { + return SpecializedTasks.False; + } + + if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps) + { + return SpecializedTasks.False; + } + + BrowserHelper.StartBrowser(uri); + return SpecializedTasks.True; + } + } +}