Skip to content

Commit

Permalink
Merge pull request #54249 from mavasani/LsifHover
Browse files Browse the repository at this point in the history
LSIF support for hover
  • Loading branch information
mavasani authored Jun 23, 2021
2 parents 82654d8 + d241374 commit d3e915d
Show file tree
Hide file tree
Showing 11 changed files with 325 additions and 54 deletions.
5 changes: 3 additions & 2 deletions src/EditorFeatures/Core.Wpf/InlineHints/InlineHintsTag.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -94,8 +95,8 @@ public async Task<IReadOnlyCollection<object>> 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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
17 changes: 8 additions & 9 deletions src/EditorFeatures/Core/Implementation/IntelliSense/Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,16 @@ internal static class Helpers
{
internal static IReadOnlyCollection<object> BuildInteractiveTextElements(
ImmutableArray<TaggedText> taggedTexts,
Document document,
IThreadingContext threadingContext,
Lazy<IStreamingFindUsagesPresenter> streamingPresenter)
IntellisenseQuickInfoBuilderContext? context)
{
var index = 0;
return BuildInteractiveTextElements(taggedTexts, ref index, document, threadingContext, streamingPresenter);
return BuildInteractiveTextElements(taggedTexts, ref index, context);
}

private static IReadOnlyCollection<object> BuildInteractiveTextElements(
ImmutableArray<TaggedText> taggedTexts,
ref int index,
Document document,
IThreadingContext? threadingContext,
Lazy<IStreamingFindUsagesPresenter>? streamingPresenter)
IntellisenseQuickInfoBuilderContext? context)
{
// This method produces a sequence of zero or more paragraphs
var paragraphs = new List<object>();
Expand Down Expand Up @@ -67,7 +63,7 @@ private static IReadOnlyCollection<object> 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(
Expand Down Expand Up @@ -136,8 +132,11 @@ private static IReadOnlyCollection<object> 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<IStreamingFindUsagesPresenter> streamingPresenter)
{
var document = context.Document;
if (Uri.TryCreate(part.NavigationTarget, UriKind.Absolute, out var absoluteUri))
{
var target = new QuickInfoHyperLink(document.Project.Solution.Workspace, absoluteUri);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,9 +25,7 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo
internal static class IntellisenseQuickInfoBuilder
{
private static async Task<ContainerElement> BuildInteractiveContentAsync(CodeAnalysisQuickInfoItem quickInfoItem,
Document document,
IThreadingContext threadingContext,
Lazy<IStreamingFindUsagesPresenter> 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
Expand All @@ -52,7 +48,7 @@ private static async Task<ContainerElement> 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)
{
Expand All @@ -74,7 +70,7 @@ private static async Task<ContainerElement> 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)
{
Expand All @@ -98,10 +94,10 @@ private static async Task<ContainerElement> 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<ClassifiedSpan>();
foreach (var span in quickInfoItem.RelatedSpans)
Expand Down Expand Up @@ -134,7 +130,8 @@ internal static async Task<IntellisenseQuickInfoItem> BuildItemAsync(
Lazy<IStreamingFindUsagesPresenter> 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);
}
Expand All @@ -147,10 +144,10 @@ internal static async Task<IntellisenseQuickInfoItem> BuildItemAsync(
/// </summary>
internal static Task<ContainerElement> BuildContentWithoutNavigationActionsAsync(
CodeAnalysisQuickInfoItem quickInfoItem,
Document document,
IntellisenseQuickInfoBuilderContext? context,
CancellationToken cancellationToken)
{
return BuildInteractiveContentAsync(quickInfoItem, document, threadingContext: null, streamingPresenter: null, cancellationToken);
return BuildInteractiveContentAsync(quickInfoItem, context, cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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
{
/// <summary>
/// Context to build content for quick info item for intellisense.
/// </summary>
internal sealed class IntellisenseQuickInfoBuilderContext
{
public IntellisenseQuickInfoBuilderContext(
Document document,
IThreadingContext? threadingContext,
Lazy<IStreamingFindUsagesPresenter>? streamingPresenter)
{
Document = document;
ThreadingContext = threadingContext;
StreamingPresenter = streamingPresenter;
}

public Document Document { get; }
public IThreadingContext? ThreadingContext { get; }
public Lazy<IStreamingFindUsagesPresenter>? StreamingPresenter { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public static async Task<int> 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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,9 @@ public static async Task<DocumentOptionSet> FormattingOptionsToDocumentOptionsAs
}

public static LSP.MarkupContent GetDocumentationMarkupContent(ImmutableArray<TaggedText> tags, Document document, bool featureSupportsMarkdown)
=> GetDocumentationMarkupContent(tags, document.Project.Language, featureSupportsMarkdown);

public static LSP.MarkupContent GetDocumentationMarkupContent(ImmutableArray<TaggedText> tags, string language, bool featureSupportsMarkdown)
{
if (!featureSupportsMarkdown)
{
Expand All @@ -657,7 +660,7 @@ public static LSP.MarkupContent GetDocumentationMarkupContent(ImmutableArray<Tag
switch (taggedText.Tag)
{
case TextTags.CodeBlockStart:
var codeBlockLanguageName = GetCodeBlockLanguageName(document);
var codeBlockLanguageName = GetCodeBlockLanguageName(language);
builder.Append($"```{codeBlockLanguageName}{Environment.NewLine}");
builder.Append(taggedText.Text);
isInCodeBlock = true;
Expand Down Expand Up @@ -686,13 +689,13 @@ public static LSP.MarkupContent GetDocumentationMarkupContent(ImmutableArray<Tag
Value = builder.ToString(),
};

static string GetCodeBlockLanguageName(Document document)
static string GetCodeBlockLanguageName(string language)
{
return document.Project.Language switch
return language switch
{
(LanguageNames.CSharp) => CSharpMarkdownLanguageName,
(LanguageNames.VisualBasic) => VisualBasicMarkdownLanguageName,
_ => throw new InvalidOperationException($"{document.Project.Language} is not supported"),
_ => throw new InvalidOperationException($"{language} is not supported"),
};
}

Expand Down
82 changes: 57 additions & 25 deletions src/Features/LanguageServer/Protocol/Handler/Hover/HoverHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
using System;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.Linq;
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;
Expand Down Expand Up @@ -51,44 +53,74 @@ 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);
}

internal static async Task<Hover?> GetHoverAsync(
SemanticModel semanticModel,
int position,
HostLanguageServices languageServices,
CancellationToken cancellationToken)
{
Debug.Assert(semanticModel.Language == LanguageNames.CSharp || semanticModel.Language == LanguageNames.VisualBasic);

static async Task<Hover> GetHoverAsync(QuickInfoItem info, Document document, ClientCapabilities clientCapabilities, CancellationToken cancellationToken)
// 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<QuickInfoService>();
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 document.GetTextAsync(cancellationToken).ConfigureAwait(false);
if (supportsVSExtensions)
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<Hover> GetHoverAsync(
QuickInfoItem info,
SourceText text,
string language,
Document? document,
ClientCapabilities? clientCapabilities,
CancellationToken cancellationToken)
{
var supportsVSExtensions = clientCapabilities.HasVisualStudioLspCapability();

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<string, MarkedString>, SumType<string, MarkedString>[], 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<string, MarkedString>, SumType<string, MarkedString>[], 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);
}
}
}
Expand Down
Loading

0 comments on commit d3e915d

Please sign in to comment.