Skip to content

Commit

Permalink
Introduces a new hover provider, under V2 of the protocol, that uses …
Browse files Browse the repository at this point in the history
…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
333fred committed Jul 24, 2020
1 parent af15858 commit 017a909
Show file tree
Hide file tree
Showing 5 changed files with 1,016 additions and 0 deletions.
9 changes: 9 additions & 0 deletions src/OmniSharp.Abstractions/Models/v2/QuickInfoRequest.cs
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
{
}
}
35 changes: 35 additions & 0 deletions src/OmniSharp.Abstractions/Models/v2/QuickInfoResponse.cs
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}"" }}";
}
}
}
2 changes: 2 additions & 0 deletions src/OmniSharp.Abstractions/OmniSharpEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ public static class V2
public const string CodeStructure = "/v2/codestructure";

public const string Highlight = "/v2/highlight";

public const string QuickInfo = "/v2/quickinfo";
}
}
}
197 changes: 197 additions & 0 deletions src/OmniSharp.Roslyn.CSharp/Services/QuickInfoProvider.cs
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;
}
}
}
}
}
Loading

0 comments on commit 017a909

Please sign in to comment.