-
Notifications
You must be signed in to change notification settings - Fork 418
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduces a new hover provider, under V2 of the protocol, that uses …
…Roslyn's QuickInfoService The existing provider uses a custom handler, which this replaces. Among other benefits, this brings nullability display when available, and ensures that any new additions to roslyn's info get propagated to users of this service. Unfortunately, however, TaggedText in VS is significantly more powerful than vscode's hover renderer: that simply uses markdown. Their implementation does not support any extensions to enable C# formatting of code inline, I created a poor-man's substitute: for the description line, we treat the whole line as C#. It does mean there can be a bit of odd formatting with `(parameter)` or similar, but this exactly mirrors what typescript does so I don't think it's a big deal. For other sections, I picked sections that looked ok when formatted as C# code, and I otherwise did a simple conversion, as best I could, from tagged text to inline markdown.
- Loading branch information
Showing
5 changed files
with
1,016 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
using OmniSharp.Mef; | ||
|
||
namespace OmniSharp.Models.v2 | ||
{ | ||
[OmniSharpEndpoint(OmniSharpEndpoints.V2.QuickInfo, typeof(QuickInfoRequest), typeof(QuickInfoResponse))] | ||
public class QuickInfoRequest : Request | ||
{ | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
#nullable enable | ||
namespace OmniSharp.Models.v2 | ||
{ | ||
public class QuickInfoResponse | ||
{ | ||
/// <summary> | ||
/// Description of the symbol under the cursor. This is expected to be rendered as a C# codeblock | ||
/// </summary> | ||
public string? Description { get; set; } | ||
|
||
/// <summary> | ||
/// Documentation of the symbol under the cursor, if present. It is expected to be rendered as markdown. | ||
/// </summary> | ||
public string? Summary { get; set; } | ||
|
||
/// <summary> | ||
/// Other relevant information to the symbol under the cursor. | ||
/// </summary> | ||
public QuickInfoResponseSection[]? RemainingSections { get; set; } | ||
} | ||
|
||
public struct QuickInfoResponseSection | ||
{ | ||
/// <summary> | ||
/// If true, the text should be rendered as C# code. If false, the text should be rendered as markdown. | ||
/// </summary> | ||
public bool IsCSharpCode { get; set; } | ||
public string Text { get; set; } | ||
|
||
public override string ToString() | ||
{ | ||
return $@"{{ IsCSharpCode = {IsCSharpCode}, Text = ""{Text}"" }}"; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
197 changes: 197 additions & 0 deletions
197
src/OmniSharp.Roslyn.CSharp/Services/QuickInfoProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
using System.Collections.Immutable; | ||
using System.Composition; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.QuickInfo; | ||
using Microsoft.CodeAnalysis.Text; | ||
using OmniSharp.Mef; | ||
using OmniSharp.Models.v2; | ||
using OmniSharp.Options; | ||
|
||
#nullable enable | ||
|
||
namespace OmniSharp.Roslyn.CSharp.Services | ||
{ | ||
[OmniSharpHandler(OmniSharpEndpoints.V2.QuickInfo, LanguageNames.CSharp)] | ||
public class QuickInfoProvider : IRequestHandler<QuickInfoRequest, QuickInfoResponse> | ||
{ | ||
// Based on https://github.com/dotnet/roslyn/blob/master/src/Features/LanguageServer/Protocol/Handler/Hover/HoverHandler.cs | ||
|
||
// These are internal tag values taken from https://github.com/dotnet/roslyn/blob/master/src/Features/Core/Portable/Common/TextTags.cs | ||
// They're copied here so that we can ensure we render blocks correctly in the markdown | ||
|
||
/// <summary> | ||
/// Indicates the start of a text container. The elements after <see cref="ContainerStart"/> through (but not | ||
/// including) the matching <see cref="ContainerEnd"/> are rendered in a rectangular block which is positioned | ||
/// as an inline element relative to surrounding elements. The text of the <see cref="ContainerStart"/> element | ||
/// itself precedes the content of the container, and is typically a bullet or number header for an item in a | ||
/// list. | ||
/// </summary> | ||
private const string ContainerStart = nameof(ContainerStart); | ||
/// <summary> | ||
/// Indicates the end of a text container. See <see cref="ContainerStart"/>. | ||
/// </summary> | ||
private const string ContainerEnd = nameof(ContainerEnd); | ||
|
||
private readonly OmniSharpWorkspace _workspace; | ||
private readonly FormattingOptions _formattingOptions; | ||
|
||
[ImportingConstructor] | ||
public QuickInfoProvider(OmniSharpWorkspace workspace, FormattingOptions formattingOptions) | ||
{ | ||
_workspace = workspace; | ||
_formattingOptions = formattingOptions; | ||
} | ||
|
||
public async Task<QuickInfoResponse> Handle(QuickInfoRequest request) | ||
{ | ||
var document = _workspace.GetDocument(request.FileName); | ||
var response = new QuickInfoResponse(); | ||
|
||
if (document is null) | ||
{ | ||
return response; | ||
} | ||
|
||
var quickInfoService = QuickInfoService.GetService(document); | ||
if (quickInfoService is null) | ||
{ | ||
return response; | ||
} | ||
|
||
var sourceText = await document.GetTextAsync(); | ||
var position = sourceText.Lines.GetPosition(new LinePosition(request.Line, request.Column)); | ||
|
||
var quickInfo = await quickInfoService.GetQuickInfoAsync(document, position); | ||
if (quickInfo is null) | ||
{ | ||
return response; | ||
} | ||
|
||
|
||
var sb = new StringBuilder(); | ||
response.Description = quickInfo.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.Description)?.Text; | ||
|
||
var documentation = quickInfo.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.DocumentationComments); | ||
if (documentation is object) | ||
{ | ||
response.Summary = getMarkdown(documentation.TaggedParts); | ||
} | ||
|
||
response.RemainingSections = quickInfo.Sections | ||
.Where(s => s.Kind != QuickInfoSectionKinds.Description && s.Kind != QuickInfoSectionKinds.DocumentationComments) | ||
.Select(s => | ||
{ | ||
switch (s.Kind) | ||
{ | ||
case QuickInfoSectionKinds.AnonymousTypes: | ||
case QuickInfoSectionKinds.TypeParameters: | ||
return new QuickInfoResponseSection { IsCSharpCode = true, Text = s.Text }; | ||
|
||
default: | ||
return new QuickInfoResponseSection { IsCSharpCode = false, Text = getMarkdown(s.TaggedParts) }; | ||
} | ||
}) | ||
.ToArray(); | ||
|
||
return response; | ||
|
||
string getMarkdown(ImmutableArray<TaggedText> taggedTexts) | ||
{ | ||
bool isInCodeBlock = false; | ||
var sb = new StringBuilder(); | ||
for (int i = 0; i < taggedTexts.Length; i++) | ||
{ | ||
var current = taggedTexts[i]; | ||
|
||
switch (current.Tag) | ||
{ | ||
case TextTags.Text when !isInCodeBlock: | ||
sb.Append(current.Text); | ||
break; | ||
|
||
case TextTags.Text: | ||
endBlock(); | ||
sb.Append(current.Text); | ||
break; | ||
|
||
case TextTags.Space when isInCodeBlock: | ||
if (nextIsTag(TextTags.Text, i)) | ||
{ | ||
endBlock(); | ||
} | ||
|
||
sb.Append(current.Text); | ||
break; | ||
|
||
case TextTags.Space: | ||
case TextTags.Punctuation: | ||
sb.Append(current.Text); | ||
break; | ||
|
||
case ContainerStart: | ||
// Markdown needs 2 linebreaks to make a new paragraph | ||
addNewline(); | ||
addNewline(); | ||
sb.Append(current.Text); | ||
break; | ||
|
||
case ContainerEnd: | ||
// Markdown needs 2 linebreaks to make a new paragraph | ||
addNewline(); | ||
addNewline(); | ||
break; | ||
|
||
case TextTags.LineBreak: | ||
if (!nextIsTag(ContainerStart, i) && !nextIsTag(ContainerEnd, i)) | ||
{ | ||
addNewline(); | ||
addNewline(); | ||
} | ||
break; | ||
|
||
default: | ||
if (!isInCodeBlock) | ||
{ | ||
isInCodeBlock = true; | ||
sb.Append('`'); | ||
} | ||
sb.Append(current.Text); | ||
break; | ||
} | ||
} | ||
|
||
if (isInCodeBlock) | ||
{ | ||
endBlock(); | ||
} | ||
|
||
return sb.ToString().Trim(); | ||
|
||
void addNewline() | ||
{ | ||
if (isInCodeBlock) | ||
{ | ||
endBlock(); | ||
} | ||
|
||
sb.Append(_formattingOptions.NewLine); | ||
} | ||
|
||
void endBlock() | ||
{ | ||
sb.Append('`'); | ||
isInCodeBlock = false; | ||
} | ||
|
||
bool nextIsTag(string tag, int i) | ||
{ | ||
int nextI = i + 1; | ||
return nextI < taggedTexts.Length && taggedTexts[nextI].Tag == tag; | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.
017a909
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@333fred Hi Fred,
I try to reuse
QuickInfoProvider
in my code and it works, however, the return value contains only one section. I.e. when I hover overConsole
in the source code, it returns back the markdownclass System.Console
without documentation section. I've tried with native Roslyn'sQuickInfoService
and had the same result.You added this functionality, could you give me some hint, what might be wrong? Two days in a row I'm trying to understand why it doesn't work and still without success. I really appreciate any help from your side.
My code is fairly simple:
This is how I create workspace (
Consts.App
is just a simple console code block):As said, it returns only title section without documentation.
017a909
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@s-KaiNet I'm not sure what part of the MSBuild toolset finds doc comments, but I'd bet you're not replicating it with those metadata references. I'd suggest looking through the Omnisharp project loader code that interacts with msbuild.
017a909
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you! Will take a look at the MSBuild option.