Skip to content

Commit d3ad9a6

Browse files
authored
Merge pull request #1860 from 333fred/quickinfo
Introduces a new hover provider, under V2 of the protocol, that uses Roslyn's QuickInfoService
2 parents aff7540 + f51549f commit d3ad9a6

File tree

12 files changed

+1022
-8
lines changed

12 files changed

+1022
-8
lines changed

.pipelines/init.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
parameters:
22
# Configuration: Release
33
Verbosity: Normal
4-
DotNetVersion: "3.1.201"
4+
DotNetVersion: "3.1.302"
55
CakeVersion: "0.32.1"
66
NuGetVersion: "4.9.2"
77
steps:

azure-pipelines.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ resources:
2222

2323
variables:
2424
Verbosity: Diagnostic
25-
DotNetVersion: "3.1.201"
25+
DotNetVersion: "3.1.302"
2626
CakeVersion: "0.32.1"
2727
NuGetVersion: "4.9.2"
2828
GitVersionVersion: "5.0.1"

build.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"DotNetInstallScriptURL": "https://dot.net/v1",
33
"DotNetChannel": "Preview",
44
"DotNetVersions": [
5-
"3.1.201",
5+
"3.1.302",
66
"5.0.100-preview.7.20366.6"
77
],
88
"RequiredMonoVersion": "6.6.0",

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"sdk": {
3-
"version": "3.1.201"
3+
"version": "3.1.302"
44
}
55
}
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
4+
{
5+
[OmniSharpEndpoint(OmniSharpEndpoints.QuickInfo, typeof(QuickInfoRequest), typeof(QuickInfoResponse))]
6+
public class QuickInfoRequest : Request
7+
{
8+
}
9+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#nullable enable
2+
using System.Collections.Immutable;
3+
4+
namespace OmniSharp.Models
5+
{
6+
public class QuickInfoResponse
7+
{
8+
/// <summary>
9+
/// QuickInfo for the given position, rendered as markdown.
10+
/// </summary>
11+
public string Markdown { get; set; } = string.Empty;
12+
}
13+
}

src/OmniSharp.Abstractions/OmniSharpEndpoints.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public static class OmniSharpEndpoints
4444
public const string Diagnostics = "/diagnostics";
4545

4646
public const string ReAnalyze = "/reanalyze";
47+
public const string QuickInfo = "/quickinfo";
4748

4849
public static class V2
4950
{
@@ -65,6 +66,7 @@ public static class V2
6566
public const string CodeStructure = "/v2/codestructure";
6667

6768
public const string Highlight = "/v2/highlight";
69+
6870
}
6971
}
7072
}
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
using System.Composition;
2+
using System.Linq;
3+
using System.Text;
4+
using System.Threading.Tasks;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.QuickInfo;
7+
using Microsoft.CodeAnalysis.Text;
8+
using Microsoft.Extensions.Logging;
9+
using OmniSharp.Mef;
10+
using OmniSharp.Models;
11+
using OmniSharp.Options;
12+
13+
#nullable enable
14+
15+
namespace OmniSharp.Roslyn.CSharp.Services
16+
{
17+
[OmniSharpHandler(OmniSharpEndpoints.QuickInfo, LanguageNames.CSharp)]
18+
public class QuickInfoProvider : IRequestHandler<QuickInfoRequest, QuickInfoResponse>
19+
{
20+
// Based on https://github.com/dotnet/roslyn/blob/7dc32a952e77c96c31cae6a2ba6d253a558fc7ff/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+
// https://github.com/dotnet/roslyn/issues/46254 tracks making these public
25+
26+
/// <summary>
27+
/// Indicates the start of a text container. The elements after <see cref="ContainerStart"/> through (but not
28+
/// including) the matching <see cref="ContainerEnd"/> are rendered in a rectangular block which is positioned
29+
/// as an inline element relative to surrounding elements. The text of the <see cref="ContainerStart"/> element
30+
/// itself precedes the content of the container, and is typically a bullet or number header for an item in a
31+
/// list.
32+
/// </summary>
33+
private const string ContainerStart = nameof(ContainerStart);
34+
/// <summary>
35+
/// Indicates the end of a text container. See <see cref="ContainerStart"/>.
36+
/// </summary>
37+
private const string ContainerEnd = nameof(ContainerEnd);
38+
/// <summary>
39+
/// Section kind for nullability analysis.
40+
/// </summary>
41+
internal const string NullabilityAnalysis = nameof(NullabilityAnalysis);
42+
43+
private readonly OmniSharpWorkspace _workspace;
44+
private readonly FormattingOptions _formattingOptions;
45+
private readonly ILogger<QuickInfoProvider>? _logger;
46+
47+
[ImportingConstructor]
48+
public QuickInfoProvider(OmniSharpWorkspace workspace, FormattingOptions formattingOptions, ILoggerFactory? loggerFactory)
49+
{
50+
_workspace = workspace;
51+
_formattingOptions = formattingOptions;
52+
_logger = loggerFactory?.CreateLogger<QuickInfoProvider>();
53+
}
54+
55+
public async Task<QuickInfoResponse> Handle(QuickInfoRequest request)
56+
{
57+
var document = _workspace.GetDocument(request.FileName);
58+
var response = new QuickInfoResponse();
59+
60+
if (document is null)
61+
{
62+
return response;
63+
}
64+
65+
var quickInfoService = QuickInfoService.GetService(document);
66+
if (quickInfoService is null)
67+
{
68+
_logger?.LogWarning($"QuickInfo service was null for {document.FilePath}");
69+
return response;
70+
}
71+
72+
var sourceText = await document.GetTextAsync();
73+
var position = sourceText.Lines.GetPosition(new LinePosition(request.Line, request.Column));
74+
75+
var quickInfo = await quickInfoService.GetQuickInfoAsync(document, position);
76+
if (quickInfo is null)
77+
{
78+
_logger?.LogTrace($"No QuickInfo found for {document.FilePath}:{request.Line},{request.Column}");
79+
return response;
80+
}
81+
82+
var finalTextBuilder = new StringBuilder();
83+
var sectionTextBuilder = new StringBuilder();
84+
85+
var description = quickInfo.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.Description);
86+
if (description is object)
87+
{
88+
appendSectionAsCsharp(description, finalTextBuilder, _formattingOptions, includeSpaceAtStart: false);
89+
}
90+
91+
var summary = quickInfo.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.DocumentationComments);
92+
if (summary is object)
93+
{
94+
buildSectionAsMarkdown(summary, sectionTextBuilder, _formattingOptions, out _);
95+
appendBuiltSection(finalTextBuilder, sectionTextBuilder, _formattingOptions);
96+
}
97+
98+
foreach (var section in quickInfo.Sections)
99+
{
100+
switch (section.Kind)
101+
{
102+
case QuickInfoSectionKinds.Description:
103+
case QuickInfoSectionKinds.DocumentationComments:
104+
continue;
105+
106+
case QuickInfoSectionKinds.TypeParameters:
107+
appendSectionAsCsharp(section, finalTextBuilder, _formattingOptions);
108+
break;
109+
110+
case QuickInfoSectionKinds.AnonymousTypes:
111+
// The first line is "Anonymous Types:"
112+
buildSectionAsMarkdown(section, sectionTextBuilder, _formattingOptions, out int lastIndex, untilLineBreak: true);
113+
appendBuiltSection(finalTextBuilder, sectionTextBuilder, _formattingOptions);
114+
115+
// Then we want all anonymous types to be C# highlighted
116+
appendSectionAsCsharp(section, finalTextBuilder, _formattingOptions, lastIndex + 1);
117+
break;
118+
119+
case NullabilityAnalysis:
120+
// Italicize the nullable analysis for emphasis.
121+
buildSectionAsMarkdown(section, sectionTextBuilder, _formattingOptions, out _);
122+
appendBuiltSection(finalTextBuilder, sectionTextBuilder, _formattingOptions, italicize: true);
123+
break;
124+
125+
default:
126+
buildSectionAsMarkdown(section, sectionTextBuilder, _formattingOptions, out _);
127+
appendBuiltSection(finalTextBuilder, sectionTextBuilder, _formattingOptions);
128+
break;
129+
}
130+
}
131+
132+
response.Markdown = finalTextBuilder.ToString().Trim();
133+
134+
return response;
135+
136+
static void appendBuiltSection(StringBuilder finalTextBuilder, StringBuilder stringBuilder, FormattingOptions formattingOptions, bool italicize = false)
137+
{
138+
// Two newlines to trigger a markdown new paragraph
139+
finalTextBuilder.Append(formattingOptions.NewLine);
140+
finalTextBuilder.Append(formattingOptions.NewLine);
141+
if (italicize)
142+
{
143+
finalTextBuilder.Append("_");
144+
}
145+
finalTextBuilder.Append(stringBuilder);
146+
if (italicize)
147+
{
148+
finalTextBuilder.Append("_");
149+
}
150+
stringBuilder.Clear();
151+
}
152+
153+
static void appendSectionAsCsharp(QuickInfoSection section, StringBuilder builder, FormattingOptions formattingOptions, int startingIndex = 0, bool includeSpaceAtStart = true)
154+
{
155+
if (includeSpaceAtStart)
156+
{
157+
builder.Append(formattingOptions.NewLine);
158+
}
159+
builder.Append("```csharp");
160+
builder.Append(formattingOptions.NewLine);
161+
for (int i = startingIndex; i < section.TaggedParts.Length; i++)
162+
{
163+
TaggedText part = section.TaggedParts[i];
164+
if (part.Tag == TextTags.LineBreak && i + 1 != section.TaggedParts.Length)
165+
{
166+
builder.Append(formattingOptions.NewLine);
167+
}
168+
else
169+
{
170+
builder.Append(part.Text);
171+
}
172+
}
173+
builder.Append(formattingOptions.NewLine);
174+
builder.Append("```");
175+
}
176+
177+
static void buildSectionAsMarkdown(QuickInfoSection section, StringBuilder stringBuilder, FormattingOptions formattingOptions, out int lastIndex, bool untilLineBreak = false)
178+
{
179+
bool isInCodeBlock = false;
180+
lastIndex = 0;
181+
for (int i = 0; i < section.TaggedParts.Length; i++)
182+
{
183+
var current = section.TaggedParts[i];
184+
lastIndex = i;
185+
186+
switch (current.Tag)
187+
{
188+
case TextTags.Text when !isInCodeBlock:
189+
stringBuilder.Append(current.Text);
190+
break;
191+
192+
case TextTags.Text:
193+
endBlock();
194+
stringBuilder.Append(current.Text);
195+
break;
196+
197+
case TextTags.Space when isInCodeBlock:
198+
if (nextIsTag(i, TextTags.Text))
199+
{
200+
endBlock();
201+
}
202+
203+
stringBuilder.Append(current.Text);
204+
break;
205+
206+
case TextTags.Space:
207+
case TextTags.Punctuation:
208+
stringBuilder.Append(current.Text);
209+
break;
210+
211+
case ContainerStart:
212+
addNewline();
213+
stringBuilder.Append(current.Text);
214+
break;
215+
216+
case ContainerEnd:
217+
addNewline();
218+
break;
219+
220+
case TextTags.LineBreak when untilLineBreak && stringBuilder.Length != 0:
221+
// The section will end and another newline will be appended, no need to add yet another newline.
222+
return;
223+
224+
case TextTags.LineBreak:
225+
if (stringBuilder.Length != 0 && !nextIsTag(i, ContainerStart, ContainerEnd) && i + 1 != section.TaggedParts.Length)
226+
{
227+
addNewline();
228+
}
229+
break;
230+
231+
default:
232+
if (!isInCodeBlock)
233+
{
234+
isInCodeBlock = true;
235+
stringBuilder.Append('`');
236+
}
237+
stringBuilder.Append(current.Text);
238+
break;
239+
}
240+
}
241+
242+
if (isInCodeBlock)
243+
{
244+
endBlock();
245+
}
246+
247+
return;
248+
249+
void addNewline()
250+
{
251+
if (isInCodeBlock)
252+
{
253+
endBlock();
254+
}
255+
256+
// Markdown needs 2 linebreaks to make a new paragraph
257+
stringBuilder.Append(formattingOptions.NewLine);
258+
stringBuilder.Append(formattingOptions.NewLine);
259+
}
260+
261+
void endBlock()
262+
{
263+
stringBuilder.Append('`');
264+
isInCodeBlock = false;
265+
}
266+
267+
bool nextIsTag(int i, params string[] tags)
268+
{
269+
int nextI = i + 1;
270+
return nextI < section.TaggedParts.Length && tags.Contains(section.TaggedParts[nextI].Tag);
271+
}
272+
}
273+
}
274+
}
275+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"sdk": {
3-
"version": "3.1.201"
3+
"version": "3.1.302"
44
}
55
}

tests/OmniSharp.MSBuild.Tests/ProjectLoadListenerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ public async Task The_correct_sdk_version_is_emitted()
224224
using (var host = CreateMSBuildTestHost(testProject.Directory, emitter.AsExportDescriptionProvider(LoggerFactory)))
225225
{
226226
Assert.Single(emitter.ReceivedMessages);
227-
Assert.Equal(GetHashedFileExtension("3.1.201"), emitter.ReceivedMessages[0].SdkVersion);
227+
Assert.Equal(GetHashedFileExtension("3.1.302"), emitter.ReceivedMessages[0].SdkVersion);
228228
}
229229
}
230230

0 commit comments

Comments
 (0)