From cd5c67bbf773c75542b4370cbcf6207aa6ef2901 Mon Sep 17 00:00:00 2001 From: Manish Vasani Date: Sun, 20 Jun 2021 20:34:33 -0700 Subject: [PATCH 1/3] LSIF support for hover This change builds on top of #53520, which refactored the quick info service to work at the compiler layer. This PR adds support to the LSIF generator to invoke into quick info service to compute the hover information and adds the hover result to the LSIF graph. --- .../Core.Wpf/InlineHints/InlineHintsTag.cs | 5 +- .../AsyncCompletion/CompletionSource.cs | 4 +- .../Implementation/IntelliSense/Helpers.cs | 17 ++- .../QuickInfo/IntellisenseQuickInfoBuilder.cs | 21 ++- .../IntellisenseQuickInfoBuilderContext.cs | 27 ++++ .../Protocol/Extensions/Extensions.cs | 2 +- .../Extensions/ProtocolConversions.cs | 11 +- .../Protocol/Handler/Hover/HoverHandler.cs | 77 +++++++---- src/Features/Lsif/Generator/Generator.cs | 15 +++ .../Lsif/Generator/Graph/HoverResult.cs | 24 ++++ src/Features/Lsif/GeneratorTest/HoverTests.vb | 122 ++++++++++++++++++ 11 files changed, 271 insertions(+), 54 deletions(-) create mode 100644 src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilderContext.cs create mode 100644 src/Features/Lsif/Generator/Graph/HoverResult.cs create mode 100644 src/Features/Lsif/GeneratorTest/HoverTests.vb diff --git a/src/EditorFeatures/Core.Wpf/InlineHints/InlineHintsTag.cs b/src/EditorFeatures/Core.Wpf/InlineHints/InlineHintsTag.cs index 48fd5dff6c9a1..ea6d47700ed15 100644 --- a/src/EditorFeatures/Core.Wpf/InlineHints/InlineHintsTag.cs +++ b/src/EditorFeatures/Core.Wpf/InlineHints/InlineHintsTag.cs @@ -14,6 +14,7 @@ using System.Windows.Input; using System.Windows.Media; using Microsoft.CodeAnalysis.Editor.Host; +using Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.InlineHints; using Microsoft.CodeAnalysis.PooledObjects; @@ -94,8 +95,8 @@ public async Task> CreateDescriptionAsync(Cancellati var taggedText = await _hint.GetDescriptionAsync(document, cancellationToken).ConfigureAwait(false); if (!taggedText.IsDefaultOrEmpty) { - return Implementation.IntelliSense.Helpers.BuildInteractiveTextElements( - taggedText, document, _threadingContext, _streamingPresenter); + var context = new IntellisenseQuickInfoBuilderContext(document, _threadingContext, _streamingPresenter); + return Implementation.IntelliSense.Helpers.BuildInteractiveTextElements(taggedText, context); } } diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionSource.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionSource.cs index d8069ec1f6559..bf42006821e29 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionSource.cs +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionSource.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Completion.Providers; using Microsoft.CodeAnalysis.Editor.Host; +using Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Experiments; @@ -373,7 +374,8 @@ private bool TryInvokeSnippetCompletion( var description = await service.GetDescriptionAsync(document, roslynItem, cancellationToken).ConfigureAwait(false); - var elements = IntelliSense.Helpers.BuildInteractiveTextElements(description.TaggedParts, document, ThreadingContext, _streamingPresenter).ToArray(); + var context = new IntellisenseQuickInfoBuilderContext(document, ThreadingContext, _streamingPresenter); + var elements = IntelliSense.Helpers.BuildInteractiveTextElements(description.TaggedParts, context).ToArray(); if (elements.Length == 0) { return new ClassifiedTextElement(); diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/Helpers.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/Helpers.cs index 629e21ae5fa55..6e9ceb2cf3624 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/Helpers.cs +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/Helpers.cs @@ -22,20 +22,16 @@ internal static class Helpers { internal static IReadOnlyCollection BuildInteractiveTextElements( ImmutableArray taggedTexts, - Document document, - IThreadingContext threadingContext, - Lazy streamingPresenter) + IntellisenseQuickInfoBuilderContext? context) { var index = 0; - return BuildInteractiveTextElements(taggedTexts, ref index, document, threadingContext, streamingPresenter); + return BuildInteractiveTextElements(taggedTexts, ref index, context); } private static IReadOnlyCollection BuildInteractiveTextElements( ImmutableArray taggedTexts, ref int index, - Document document, - IThreadingContext? threadingContext, - Lazy? streamingPresenter) + IntellisenseQuickInfoBuilderContext? context) { // This method produces a sequence of zero or more paragraphs var paragraphs = new List(); @@ -67,7 +63,7 @@ private static IReadOnlyCollection BuildInteractiveTextElements( } index++; - var nestedElements = BuildInteractiveTextElements(taggedTexts, ref index, document, threadingContext, streamingPresenter); + var nestedElements = BuildInteractiveTextElements(taggedTexts, ref index, context); if (nestedElements.Count <= 1) { currentParagraph.Add(new ContainerElement( @@ -136,8 +132,11 @@ private static IReadOnlyCollection BuildInteractiveTextElements( { // This is tagged text getting added to the current line we are building. var style = GetClassifiedTextRunStyle(part.Style); - if (part.NavigationTarget is object && streamingPresenter != null && threadingContext != null) + if (part.NavigationTarget is object && + context?.ThreadingContext is ThreadingContext threadingContext && + context.StreamingPresenter is Lazy streamingPresenter) { + var document = context.Document; if (Uri.TryCreate(part.NavigationTarget, UriKind.Absolute, out var absoluteUri)) { var target = new QuickInfoHyperLink(document.Project.Solution.Workspace, absoluteUri); diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilder.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilder.cs index a313b77bd03c8..89a300097727c 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilder.cs +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilder.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -27,9 +25,7 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo internal static class IntellisenseQuickInfoBuilder { private static async Task BuildInteractiveContentAsync(CodeAnalysisQuickInfoItem quickInfoItem, - Document document, - IThreadingContext threadingContext, - Lazy streamingPresenter, + IntellisenseQuickInfoBuilderContext? context, CancellationToken cancellationToken) { // Build the first line of QuickInfo item, the images and the Description section should be on the first line with Wrapped style @@ -52,7 +48,7 @@ private static async Task BuildInteractiveContentAsync(CodeAna if (descSection != null) { var isFirstElement = true; - foreach (var element in Helpers.BuildInteractiveTextElements(descSection.TaggedParts, document, threadingContext, streamingPresenter)) + foreach (var element in Helpers.BuildInteractiveTextElements(descSection.TaggedParts, context)) { if (isFirstElement) { @@ -74,7 +70,7 @@ private static async Task BuildInteractiveContentAsync(CodeAna if (documentationCommentSection != null) { var isFirstElement = true; - foreach (var element in Helpers.BuildInteractiveTextElements(documentationCommentSection.TaggedParts, document, threadingContext, streamingPresenter)) + foreach (var element in Helpers.BuildInteractiveTextElements(documentationCommentSection.TaggedParts, context)) { if (isFirstElement) { @@ -98,10 +94,10 @@ private static async Task BuildInteractiveContentAsync(CodeAna // Add the remaining sections as Stacked style elements.AddRange( quickInfoItem.Sections.Where(s => s.Kind != QuickInfoSectionKinds.Description && s.Kind != QuickInfoSectionKinds.DocumentationComments) - .SelectMany(s => Helpers.BuildInteractiveTextElements(s.TaggedParts, document, threadingContext, streamingPresenter))); + .SelectMany(s => Helpers.BuildInteractiveTextElements(s.TaggedParts, context))); // build text for RelatedSpan - if (quickInfoItem.RelatedSpans.Any()) + if (quickInfoItem.RelatedSpans.Any() && context?.Document is Document document) { var classifiedSpanList = new List(); foreach (var span in quickInfoItem.RelatedSpans) @@ -134,7 +130,8 @@ internal static async Task BuildItemAsync( Lazy streamingPresenter, CancellationToken cancellationToken) { - var content = await BuildInteractiveContentAsync(quickInfoItem, document, threadingContext, streamingPresenter, cancellationToken).ConfigureAwait(false); + var context = new IntellisenseQuickInfoBuilderContext(document, threadingContext, streamingPresenter); + var content = await BuildInteractiveContentAsync(quickInfoItem, context, cancellationToken).ConfigureAwait(false); return new IntellisenseQuickInfoItem(trackingSpan, content); } @@ -147,10 +144,10 @@ internal static async Task BuildItemAsync( /// internal static Task BuildContentWithoutNavigationActionsAsync( CodeAnalysisQuickInfoItem quickInfoItem, - Document document, + IntellisenseQuickInfoBuilderContext? context, CancellationToken cancellationToken) { - return BuildInteractiveContentAsync(quickInfoItem, document, threadingContext: null, streamingPresenter: null, cancellationToken); + return BuildInteractiveContentAsync(quickInfoItem, context, cancellationToken); } } } diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilderContext.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilderContext.cs new file mode 100644 index 0000000000000..2249af643c09d --- /dev/null +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilderContext.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.CodeAnalysis.Editor.Host; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; + +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo +{ + internal sealed class IntellisenseQuickInfoBuilderContext + { + public IntellisenseQuickInfoBuilderContext( + Document document, + IThreadingContext? threadingContext, + Lazy? streamingPresenter) + { + Document = document; + ThreadingContext = threadingContext; + StreamingPresenter = streamingPresenter; + } + + public Document Document { get; } + public IThreadingContext? ThreadingContext { get; } + public Lazy? StreamingPresenter { get; } + } +} diff --git a/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs b/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs index e0092066fe773..02a9fcff145b2 100644 --- a/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs +++ b/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs @@ -120,7 +120,7 @@ public static async Task GetPositionFromLinePositionAsync(this TextDocument return text.Lines.GetPosition(linePosition); } - public static bool HasVisualStudioLspCapability(this ClientCapabilities clientCapabilities) + public static bool HasVisualStudioLspCapability(this ClientCapabilities? clientCapabilities) { if (clientCapabilities is VSClientCapabilities vsClientCapabilities) { diff --git a/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs b/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs index 0b463c186d80b..2564094dbce93 100644 --- a/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs +++ b/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs @@ -640,6 +640,9 @@ public static async Task FormattingOptionsToDocumentOptionsAs } public static LSP.MarkupContent GetDocumentationMarkupContent(ImmutableArray tags, Document document, bool featureSupportsMarkdown) + => GetDocumentationMarkupContent(tags, document.Project.Language, featureSupportsMarkdown); + + public static LSP.MarkupContent GetDocumentationMarkupContent(ImmutableArray tags, string language, bool featureSupportsMarkdown) { if (!featureSupportsMarkdown) { @@ -657,7 +660,7 @@ public static LSP.MarkupContent GetDocumentationMarkupContent(ImmutableArray CSharpMarkdownLanguageName, (LanguageNames.VisualBasic) => VisualBasicMarkdownLanguageName, - _ => throw new InvalidOperationException($"{document.Project.Language} is not supported"), + _ => throw new InvalidOperationException($"{language} is not supported"), }; } diff --git a/src/Features/LanguageServer/Protocol/Handler/Hover/HoverHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Hover/HoverHandler.cs index 260d849f4c042..7af58bff89356 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Hover/HoverHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Hover/HoverHandler.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo; +using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.QuickInfo; @@ -51,44 +52,70 @@ public HoverHandler() return null; } - var hover = await GetHoverAsync(info, document, context.ClientCapabilities, cancellationToken).ConfigureAwait(false); - return hover; + var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + return await GetHoverAsync(info, text, document.Project.Language, document, context.ClientCapabilities, cancellationToken).ConfigureAwait(false); + } - static async Task GetHoverAsync(QuickInfoItem info, Document document, ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + internal static async Task GetHoverAsync( + SemanticModel semanticModel, + int position, + HostLanguageServices languageServices, + CancellationToken cancellationToken) + { + var quickInfoService = (QuickInfoServiceWithProviders)languageServices.GetRequiredService(); + var info = await quickInfoService.GetQuickInfoAsync(languageServices.WorkspaceServices.Workspace, semanticModel, position, cancellationToken).ConfigureAwait(false); + if (info == null) { - var supportsVSExtensions = clientCapabilities.HasVisualStudioLspCapability(); + return null; + } + + var text = await semanticModel.SyntaxTree.GetTextAsync(cancellationToken).ConfigureAwait(false); + return await GetHoverAsync(info, text, semanticModel.Language, document: null, clientCapabilities: null, cancellationToken).ConfigureAwait(false); + } + + private static async Task GetHoverAsync( + QuickInfoItem info, + SourceText text, + string language, + Document? document, + ClientCapabilities? clientCapabilities, + CancellationToken cancellationToken) + { + var supportsVSExtensions = clientCapabilities.HasVisualStudioLspCapability(); - var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); - if (supportsVSExtensions) + if (supportsVSExtensions) + { + var context = document != null + ? new IntellisenseQuickInfoBuilderContext(document, threadingContext: null, streamingPresenter: null) + : null; + return new VSHover { - return new VSHover - { - Range = ProtocolConversions.TextSpanToRange(info.Span, text), - Contents = new SumType, SumType[], MarkupContent>(string.Empty), - // Build the classified text without navigation actions - they are not serializable. - // TODO - Switch to markup content once it supports classifications. - // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/918138 - RawContent = await IntellisenseQuickInfoBuilder.BuildContentWithoutNavigationActionsAsync(info, document, cancellationToken).ConfigureAwait(false) - }; - } - else + Range = ProtocolConversions.TextSpanToRange(info.Span, text), + Contents = new SumType, SumType[], MarkupContent>(string.Empty), + // Build the classified text without navigation actions - they are not serializable. + // TODO - Switch to markup content once it supports classifications. + // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/918138 + RawContent = await IntellisenseQuickInfoBuilder.BuildContentWithoutNavigationActionsAsync(info, context, cancellationToken).ConfigureAwait(false) + }; + } + else + { + return new Hover { - return new Hover - { - Range = ProtocolConversions.TextSpanToRange(info.Span, text), - Contents = GetContents(info, document, clientCapabilities), - }; - } + Range = ProtocolConversions.TextSpanToRange(info.Span, text), + Contents = GetContents(info, language, clientCapabilities), + }; } - static MarkupContent GetContents(QuickInfoItem info, Document document, ClientCapabilities clientCapabilities) + // Local functions. + static MarkupContent GetContents(QuickInfoItem info, string language, ClientCapabilities? clientCapabilities) { var clientSupportsMarkdown = clientCapabilities?.TextDocument?.Hover?.ContentFormat.Contains(MarkupKind.Markdown) == true; // Insert line breaks in between sections to ensure we get double spacing between sections. var tags = info.Sections .SelectMany(section => section.TaggedParts.Add(new TaggedText(TextTags.LineBreak, Environment.NewLine))) .ToImmutableArray(); - return ProtocolConversions.GetDocumentationMarkupContent(tags, document, clientSupportsMarkdown); + return ProtocolConversions.GetDocumentationMarkupContent(tags, language, clientSupportsMarkdown); } } } diff --git a/src/Features/Lsif/Generator/Generator.cs b/src/Features/Lsif/Generator/Generator.cs index 75a61d2c4ffd2..1d27784cb7b7f 100644 --- a/src/Features/Lsif/Generator/Generator.cs +++ b/src/Features/Lsif/Generator/Generator.cs @@ -195,6 +195,21 @@ public void GenerateForCompilation(Compilation compilation, string projectPath, var referenceResultsId = symbolResultsTracker.GetResultIdForSymbol(referencedSymbol.OriginalDefinition, Methods.TextDocumentReferencesName, () => new ReferenceResult(idFactory)); lsifJsonWriter.Write(new Item(referenceResultsId.As(), lazyRangeVertex.Value.GetId(), documentVertex.GetId(), idFactory, property: "references")); } + + // Write hover information for the symbol, if edge has not already been added. + // 'textDocument/hover' edge goes from the symbol ResultSet vertex to the hover result + // See https://github.com/Microsoft/language-server-protocol/blob/main/indexFormat/specification.md#resultset for an example. + if (symbolResultsTracker.ResultSetNeedsInformationalEdgeAdded(symbolForLinkedResultSet, Methods.TextDocumentHoverName)) + { + // TODO: Can we avoid the WaitAndGetResult_CanCallOnBackground call by adding a sync method to compute hover? + var hover = HoverHandler.GetHoverAsync(semanticModel, syntaxToken.SpanStart, languageServices, CancellationToken.None).WaitAndGetResult_CanCallOnBackground(CancellationToken.None); + if (hover != null) + { + var hoverResult = new HoverResult(hover, idFactory); + lsifJsonWriter.Write(hoverResult); + lsifJsonWriter.Write(Edge.Create(Methods.TextDocumentHoverName, symbolForLinkedResultSetId, hoverResult.GetId(), idFactory)); + } + } } } diff --git a/src/Features/Lsif/Generator/Graph/HoverResult.cs b/src/Features/Lsif/Generator/Graph/HoverResult.cs new file mode 100644 index 0000000000000..5731da1bc1b6a --- /dev/null +++ b/src/Features/Lsif/Generator/Graph/HoverResult.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Newtonsoft.Json; + +namespace Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.Graph +{ + /// + /// Represents a foverResult vertex for serialization. See https://github.com/Microsoft/language-server-protocol/blob/main/indexFormat/specification.md#more-about-request-textdocumenthover for further details. + /// + internal sealed class HoverResult : Vertex + { + [JsonProperty("result")] + public Hover Result { get; } + + public HoverResult(Hover result, IdFactory idFactory) + : base(label: "hoverResult", idFactory) + { + Result = result; + } + } +} diff --git a/src/Features/Lsif/GeneratorTest/HoverTests.vb b/src/Features/Lsif/GeneratorTest/HoverTests.vb new file mode 100644 index 0000000000000..50d4e2a71a18b --- /dev/null +++ b/src/Features/Lsif/GeneratorTest/HoverTests.vb @@ -0,0 +1,122 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. +' See the LICENSE file in the project root for more information. + +Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces +Imports Microsoft.CodeAnalysis.Test.Utilities +Imports Microsoft.VisualStudio.LanguageServer.Protocol + +Namespace Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.UnitTests + + Public NotInheritable Class HoverTests + Private Const TestProjectAssemblyName As String = "TestProject" + + + + + + + + Doc Comment + void [|M|]() { } +}", +"void C.M() +Doc Comment")> + Public Async Function TestDefinition(code As String, expectedHoverContents As String) As Task + Dim lsif = Await TestLsifOutput.GenerateForWorkspaceAsync( + TestWorkspace.CreateWorkspace( + + FilePath="Z:\TestProject.csproj" CommonReferences="true"> + + <%= code %> + + + )) + + Dim rangeVertex = Await lsif.GetSelectedRangeAsync() + Dim resultSetVertex = lsif.GetLinkedVertices(Of Graph.ResultSet)(rangeVertex, "next").Single() + Dim hoverVertex = lsif.GetLinkedVertices(Of Graph.HoverResult)(resultSetVertex, Methods.TextDocumentHoverName).SingleOrDefault() + Dim hoverMarkupContent = DirectCast(hoverVertex.Result.Contents.Value, MarkupContent) + + Assert.Equal(MarkupKind.PlainText, hoverMarkupContent.Kind) + Assert.Equal(expectedHoverContents + Environment.NewLine, hoverMarkupContent.Value) + End Function + + + + + + + + ")> + + void M() { } +}", +"void C.M()")> + Public Async Function TestReference(code As String, expectedHoverContents As String) As Task + Dim lsif = Await TestLsifOutput.GenerateForWorkspaceAsync( + TestWorkspace.CreateWorkspace( + + FilePath="Z:\TestProject.csproj" CommonReferences="true"> + + <%= code %> + + + )) + + Dim rangeVertex = Await lsif.GetSelectedRangeAsync() + Dim resultSetVertex = lsif.GetLinkedVertices(Of Graph.ResultSet)(rangeVertex, "next").Single() + Dim hoverVertex = lsif.GetLinkedVertices(Of Graph.HoverResult)(resultSetVertex, Methods.TextDocumentHoverName).SingleOrDefault() + Dim hoverMarkupContent = DirectCast(hoverVertex.Result.Contents.Value, MarkupContent) + + Assert.Equal(MarkupKind.PlainText, hoverMarkupContent.Kind) + Assert.Equal(expectedHoverContents + Environment.NewLine, hoverMarkupContent.Value) + End Function + + + Public Async Function ToplevelResultsInMultipleFiles() As Task + Dim lsif = Await TestLsifOutput.GenerateForWorkspaceAsync( + TestWorkspace.CreateWorkspace( + + FilePath="Z:\TestProject.csproj" CommonReferences="true"> + + class A { [|string|] s; } + + + class B { [|string|] s; } + + + class C { [|string|] s; } + + + class D { [|string|] s; } + + + class E { [|string|] s; } + + + )) + + Dim hoverVertex As Graph.HoverResult = Nothing + For Each rangeVertex In Await lsif.GetSelectedRangesAsync() + Dim resultSetVertex = lsif.GetLinkedVertices(Of Graph.ResultSet)(rangeVertex, "next").Single() + Dim vertex = lsif.GetLinkedVertices(Of Graph.HoverResult)(resultSetVertex, Methods.TextDocumentHoverName).Single() + If hoverVertex Is Nothing Then + hoverVertex = vertex + Else + Assert.Same(hoverVertex, vertex) + End If + Next + + Dim hoverMarkupContent = DirectCast(hoverVertex.Result.Contents.Value, MarkupContent) + Assert.Equal(MarkupKind.PlainText, hoverMarkupContent.Kind) + Assert.Equal("class System.String" + Environment.NewLine, hoverMarkupContent.Value) + End Function + End Class +End Namespace From 5076f473a2b0803963c028a9d1d843f18bf038ab Mon Sep 17 00:00:00 2001 From: Manish Vasani Date: Mon, 21 Jun 2021 21:58:17 -0700 Subject: [PATCH 2/3] Address feedback --- .../QuickInfo/IntellisenseQuickInfoBuilderContext.cs | 3 +++ .../LanguageServer/Protocol/Handler/Hover/HoverHandler.cs | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilderContext.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilderContext.cs index 2249af643c09d..ca453d49c4b66 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilderContext.cs +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/IntellisenseQuickInfoBuilderContext.cs @@ -8,6 +8,9 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo { + /// + /// Context to build content for quick info item for intellisense. + /// internal sealed class IntellisenseQuickInfoBuilderContext { public IntellisenseQuickInfoBuilderContext( diff --git a/src/Features/LanguageServer/Protocol/Handler/Hover/HoverHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Hover/HoverHandler.cs index 7af58bff89356..478d32ab93d02 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Hover/HoverHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Hover/HoverHandler.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Immutable; using System.Composition; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -62,6 +63,10 @@ public HoverHandler() HostLanguageServices languageServices, CancellationToken cancellationToken) { + Debug.Assert(semanticModel.Language == LanguageNames.CSharp || semanticModel.Language == LanguageNames.VisualBasic); + + // Get the quick info service to compute quick info. + // This code path is only invoked for C# and VB, so we can directly cast to QuickInfoServiceWithProviders. var quickInfoService = (QuickInfoServiceWithProviders)languageServices.GetRequiredService(); var info = await quickInfoService.GetQuickInfoAsync(languageServices.WorkspaceServices.Workspace, semanticModel, position, cancellationToken).ConfigureAwait(false); if (info == null) From d241374ba50486b5757a280927528f6cc4c8fc83 Mon Sep 17 00:00:00 2001 From: Manish Vasani Date: Wed, 23 Jun 2021 03:17:36 -0700 Subject: [PATCH 3/3] Fix tests - use localized strings in expected hover contents and use the format suggested by Sam --- src/Features/Lsif/GeneratorTest/HoverTests.vb | 82 +++++++++++++++---- 1 file changed, 64 insertions(+), 18 deletions(-) diff --git a/src/Features/Lsif/GeneratorTest/HoverTests.vb b/src/Features/Lsif/GeneratorTest/HoverTests.vb index 50d4e2a71a18b..f8f4c2488e949 100644 --- a/src/Features/Lsif/GeneratorTest/HoverTests.vb +++ b/src/Features/Lsif/GeneratorTest/HoverTests.vb @@ -12,20 +12,18 @@ Namespace Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.UnitTests Private Const TestProjectAssemblyName As String = "TestProject" - - - - - + + + + + Doc Comment void [|M|]() { } -}", -"void C.M() -Doc Comment")> - Public Async Function TestDefinition(code As String, expectedHoverContents As String) As Task +}")> + Public Async Function TestDefinition(code As String) As Task Dim lsif = Await TestLsifOutput.GenerateForWorkspaceAsync( TestWorkspace.CreateWorkspace( @@ -41,25 +39,48 @@ Doc Comment")> Dim hoverVertex = lsif.GetLinkedVertices(Of Graph.HoverResult)(resultSetVertex, Methods.TextDocumentHoverName).SingleOrDefault() Dim hoverMarkupContent = DirectCast(hoverVertex.Result.Contents.Value, MarkupContent) + Dim expectedHoverContents As String + Select Case code + Case "class [|C|] { string s; }" + expectedHoverContents = "class C" + Case "class C { void [|M|]() { } }" + expectedHoverContents = "void C.M()" + Case "class C { string [|s|]; }" + expectedHoverContents = $"({FeaturesResources.field}) string C.s" + Case "class C { void M(string [|s|]) { M(s); } }" + expectedHoverContents = $"({FeaturesResources.parameter}) string s" + Case "class C { void M(string s) { string [|local|] = """"; } }" + expectedHoverContents = $"({FeaturesResources.local_variable}) string local" + Case " +class C +{ + /// Doc Comment + void [|M|]() { } +}" + expectedHoverContents = "void C.M() +Doc Comment" + Case Else + Throw TestExceptionUtilities.UnexpectedValue(code) + End Select + Assert.Equal(MarkupKind.PlainText, hoverMarkupContent.Kind) Assert.Equal(expectedHoverContents + Environment.NewLine, hoverMarkupContent.Value) End Function - - - - - - ")> + + + + + + void M() { } -}", -"void C.M()")> - Public Async Function TestReference(code As String, expectedHoverContents As String) As Task +}")> + Public Async Function TestReference(code As String) As Task Dim lsif = Await TestLsifOutput.GenerateForWorkspaceAsync( TestWorkspace.CreateWorkspace( @@ -75,6 +96,31 @@ class C Dim hoverVertex = lsif.GetLinkedVertices(Of Graph.HoverResult)(resultSetVertex, Methods.TextDocumentHoverName).SingleOrDefault() Dim hoverMarkupContent = DirectCast(hoverVertex.Result.Contents.Value, MarkupContent) + Dim expectedHoverContents As String + Select Case code + Case "class C { [|string|] s; }" + expectedHoverContents = "class System.String" + Case "class C { void M() { [|M|](); } }" + expectedHoverContents = "void C.M()" + Case "class C { void M(string s) { M([|s|]); } }" + expectedHoverContents = $"({FeaturesResources.parameter}) string s" + Case "class C { void M(string s) { string local = """"; M([|local|]); } }" + expectedHoverContents = $"({FeaturesResources.local_variable}) string local" + Case "using [|S|] = System.String;" + expectedHoverContents = "class System.String" + Case "class C { [|global|]::System.String s; }" + expectedHoverContents = "" + Case " +class C +{ + /// + void M() { } +}" + expectedHoverContents = "void C.M()" + Case Else + Throw TestExceptionUtilities.UnexpectedValue(code) + End Select + Assert.Equal(MarkupKind.PlainText, hoverMarkupContent.Kind) Assert.Equal(expectedHoverContents + Environment.NewLine, hoverMarkupContent.Value) End Function