Skip to content

Commit 017a909

Browse files
committed
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.
1 parent af15858 commit 017a909

File tree

5 files changed

+1016
-0
lines changed

5 files changed

+1016
-0
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using OmniSharp.Mef;
2+
3+
namespace OmniSharp.Models.v2
4+
{
5+
[OmniSharpEndpoint(OmniSharpEndpoints.V2.QuickInfo, typeof(QuickInfoRequest), typeof(QuickInfoResponse))]
6+
public class QuickInfoRequest : Request
7+
{
8+
}
9+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#nullable enable
2+
namespace OmniSharp.Models.v2
3+
{
4+
public class QuickInfoResponse
5+
{
6+
/// <summary>
7+
/// Description of the symbol under the cursor. This is expected to be rendered as a C# codeblock
8+
/// </summary>
9+
public string? Description { get; set; }
10+
11+
/// <summary>
12+
/// Documentation of the symbol under the cursor, if present. It is expected to be rendered as markdown.
13+
/// </summary>
14+
public string? Summary { get; set; }
15+
16+
/// <summary>
17+
/// Other relevant information to the symbol under the cursor.
18+
/// </summary>
19+
public QuickInfoResponseSection[]? RemainingSections { get; set; }
20+
}
21+
22+
public struct QuickInfoResponseSection
23+
{
24+
/// <summary>
25+
/// If true, the text should be rendered as C# code. If false, the text should be rendered as markdown.
26+
/// </summary>
27+
public bool IsCSharpCode { get; set; }
28+
public string Text { get; set; }
29+
30+
public override string ToString()
31+
{
32+
return $@"{{ IsCSharpCode = {IsCSharpCode}, Text = ""{Text}"" }}";
33+
}
34+
}
35+
}

src/OmniSharp.Abstractions/OmniSharpEndpoints.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ public static class V2
6565
public const string CodeStructure = "/v2/codestructure";
6666

6767
public const string Highlight = "/v2/highlight";
68+
69+
public const string QuickInfo = "/v2/quickinfo";
6870
}
6971
}
7072
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
using System.Collections.Immutable;
2+
using System.Composition;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.QuickInfo;
8+
using Microsoft.CodeAnalysis.Text;
9+
using OmniSharp.Mef;
10+
using OmniSharp.Models.v2;
11+
using OmniSharp.Options;
12+
13+
#nullable enable
14+
15+
namespace OmniSharp.Roslyn.CSharp.Services
16+
{
17+
[OmniSharpHandler(OmniSharpEndpoints.V2.QuickInfo, LanguageNames.CSharp)]
18+
public class QuickInfoProvider : IRequestHandler<QuickInfoRequest, QuickInfoResponse>
19+
{
20+
// Based on https://github.com/dotnet/roslyn/blob/master/src/Features/LanguageServer/Protocol/Handler/Hover/HoverHandler.cs
21+
22+
// These are internal tag values taken from https://github.com/dotnet/roslyn/blob/master/src/Features/Core/Portable/Common/TextTags.cs
23+
// They're copied here so that we can ensure we render blocks correctly in the markdown
24+
25+
/// <summary>
26+
/// Indicates the start of a text container. The elements after <see cref="ContainerStart"/> through (but not
27+
/// including) the matching <see cref="ContainerEnd"/> are rendered in a rectangular block which is positioned
28+
/// as an inline element relative to surrounding elements. The text of the <see cref="ContainerStart"/> element
29+
/// itself precedes the content of the container, and is typically a bullet or number header for an item in a
30+
/// list.
31+
/// </summary>
32+
private const string ContainerStart = nameof(ContainerStart);
33+
/// <summary>
34+
/// Indicates the end of a text container. See <see cref="ContainerStart"/>.
35+
/// </summary>
36+
private const string ContainerEnd = nameof(ContainerEnd);
37+
38+
private readonly OmniSharpWorkspace _workspace;
39+
private readonly FormattingOptions _formattingOptions;
40+
41+
[ImportingConstructor]
42+
public QuickInfoProvider(OmniSharpWorkspace workspace, FormattingOptions formattingOptions)
43+
{
44+
_workspace = workspace;
45+
_formattingOptions = formattingOptions;
46+
}
47+
48+
public async Task<QuickInfoResponse> Handle(QuickInfoRequest request)
49+
{
50+
var document = _workspace.GetDocument(request.FileName);
51+
var response = new QuickInfoResponse();
52+
53+
if (document is null)
54+
{
55+
return response;
56+
}
57+
58+
var quickInfoService = QuickInfoService.GetService(document);
59+
if (quickInfoService is null)
60+
{
61+
return response;
62+
}
63+
64+
var sourceText = await document.GetTextAsync();
65+
var position = sourceText.Lines.GetPosition(new LinePosition(request.Line, request.Column));
66+
67+
var quickInfo = await quickInfoService.GetQuickInfoAsync(document, position);
68+
if (quickInfo is null)
69+
{
70+
return response;
71+
}
72+
73+
74+
var sb = new StringBuilder();
75+
response.Description = quickInfo.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.Description)?.Text;
76+
77+
var documentation = quickInfo.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.DocumentationComments);
78+
if (documentation is object)
79+
{
80+
response.Summary = getMarkdown(documentation.TaggedParts);
81+
}
82+
83+
response.RemainingSections = quickInfo.Sections
84+
.Where(s => s.Kind != QuickInfoSectionKinds.Description && s.Kind != QuickInfoSectionKinds.DocumentationComments)
85+
.Select(s =>
86+
{
87+
switch (s.Kind)
88+
{
89+
case QuickInfoSectionKinds.AnonymousTypes:
90+
case QuickInfoSectionKinds.TypeParameters:
91+
return new QuickInfoResponseSection { IsCSharpCode = true, Text = s.Text };
92+
93+
default:
94+
return new QuickInfoResponseSection { IsCSharpCode = false, Text = getMarkdown(s.TaggedParts) };
95+
}
96+
})
97+
.ToArray();
98+
99+
return response;
100+
101+
string getMarkdown(ImmutableArray<TaggedText> taggedTexts)
102+
{
103+
bool isInCodeBlock = false;
104+
var sb = new StringBuilder();
105+
for (int i = 0; i < taggedTexts.Length; i++)
106+
{
107+
var current = taggedTexts[i];
108+
109+
switch (current.Tag)
110+
{
111+
case TextTags.Text when !isInCodeBlock:
112+
sb.Append(current.Text);
113+
break;
114+
115+
case TextTags.Text:
116+
endBlock();
117+
sb.Append(current.Text);
118+
break;
119+
120+
case TextTags.Space when isInCodeBlock:
121+
if (nextIsTag(TextTags.Text, i))
122+
{
123+
endBlock();
124+
}
125+
126+
sb.Append(current.Text);
127+
break;
128+
129+
case TextTags.Space:
130+
case TextTags.Punctuation:
131+
sb.Append(current.Text);
132+
break;
133+
134+
case ContainerStart:
135+
// Markdown needs 2 linebreaks to make a new paragraph
136+
addNewline();
137+
addNewline();
138+
sb.Append(current.Text);
139+
break;
140+
141+
case ContainerEnd:
142+
// Markdown needs 2 linebreaks to make a new paragraph
143+
addNewline();
144+
addNewline();
145+
break;
146+
147+
case TextTags.LineBreak:
148+
if (!nextIsTag(ContainerStart, i) && !nextIsTag(ContainerEnd, i))
149+
{
150+
addNewline();
151+
addNewline();
152+
}
153+
break;
154+
155+
default:
156+
if (!isInCodeBlock)
157+
{
158+
isInCodeBlock = true;
159+
sb.Append('`');
160+
}
161+
sb.Append(current.Text);
162+
break;
163+
}
164+
}
165+
166+
if (isInCodeBlock)
167+
{
168+
endBlock();
169+
}
170+
171+
return sb.ToString().Trim();
172+
173+
void addNewline()
174+
{
175+
if (isInCodeBlock)
176+
{
177+
endBlock();
178+
}
179+
180+
sb.Append(_formattingOptions.NewLine);
181+
}
182+
183+
void endBlock()
184+
{
185+
sb.Append('`');
186+
isInCodeBlock = false;
187+
}
188+
189+
bool nextIsTag(string tag, int i)
190+
{
191+
int nextI = i + 1;
192+
return nextI < taggedTexts.Length && taggedTexts[nextI].Tag == tag;
193+
}
194+
}
195+
}
196+
}
197+
}

0 commit comments

Comments
 (0)