forked from OmniSharp/omnisharp-roslyn
-
Notifications
You must be signed in to change notification settings - Fork 1
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.