Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds V2 Highlight support to Cake #2456

Merged
merged 3 commits into from
Oct 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ All changes to the project will be documented in this file.
## [1.39.2] - not yet released
* Updated ILSpy to 7.2.1.6856 (PR: [#2447](https://github.com/OmniSharp/omnisharp-roslyn/pull/2447))
* Include OmniSharp.Cake in .NET 6 builds (PR: [#2455](https://github.com/OmniSharp/omnisharp-roslyn/pull/2455))
* Implement /v2/highlight for Cake (PR: [#2456](https://github.com/OmniSharp/omnisharp-roslyn/pull/2456))


## [1.39.1] - 2022-07-25
* Update Roslyn to 4.4.0 1.22369.1 (PR: [#2420](https://github.com/OmniSharp/omnisharp-roslyn/pull/2420))
Expand Down
35 changes: 35 additions & 0 deletions src/OmniSharp.Cake/Extensions/ResponseExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using OmniSharp.Models.Navigate;
using OmniSharp.Models.MembersTree;
using OmniSharp.Models.Rename;
using OmniSharp.Models.SemanticHighlight;
using OmniSharp.Models.v1.Completion;
using OmniSharp.Models.V2;
using OmniSharp.Models.V2.CodeActions;
Expand Down Expand Up @@ -232,6 +233,40 @@ public static async Task<CompletionResponse> TranslateAsync(this CompletionRespo
return response;
}

public static async Task<SemanticHighlightResponse> TranslateAsync(this SemanticHighlightResponse response,
OmniSharpWorkspace workspace, SemanticHighlightRequest request)
{
var zeroIndex = await LineIndexHelper.TranslateToGenerated(request.FileName, 0, workspace);
var spans = new List<SemanticHighlightSpan>();

foreach (var span in response.Spans)
{
if (span.StartLine < zeroIndex)
{
continue;
}

var (startLine, _) = await LineIndexHelper.TranslateFromGenerated(request.FileName, span.StartLine, workspace, true);

if (startLine < 0)
{
continue;
}

var (endLine, _) = span.StartLine != span.EndLine
? await LineIndexHelper.TranslateFromGenerated(request.FileName, span.EndLine, workspace, true)
: (startLine, null);

span.StartLine = startLine;
span.EndLine = endLine;

spans.Add(span);
}

response.Spans = spans.ToArray();
return response;
}

private static async Task<CodeElement> TranslateAsync(this CodeElement element, OmniSharpWorkspace workspace, SimpleFileRequest request)
{
var builder = new CodeElement.Builder
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Composition;
using System.Threading.Tasks;
using OmniSharp.Cake.Extensions;
using OmniSharp.Cake.Utilities;
using OmniSharp.Mef;
using OmniSharp.Models.SemanticHighlight;

namespace OmniSharp.Cake.Services.RequestHandlers.SemanticHighlight;

[OmniSharpHandler(OmniSharpEndpoints.V2.Highlight, Constants.LanguageNames.Cake), Shared]
public class SemanticHighlightHandler : CakeRequestHandler<SemanticHighlightRequest, SemanticHighlightResponse>
{
[ImportingConstructor]
public SemanticHighlightHandler(OmniSharpWorkspace workspace) : base(workspace)
{
}

protected override async Task<SemanticHighlightRequest> TranslateRequestAsync(SemanticHighlightRequest request)
{
if (request.Range is not null)
{
var startLine = await LineIndexHelper.TranslateToGenerated(request.FileName, request.Range.Start.Line, Workspace);
var endLine = request.Range.Start.Line != request.Range.End.Line
? await LineIndexHelper.TranslateToGenerated(request.FileName, request.Range.End.Line, Workspace)
: startLine;

request.Range = request.Range with
{
Start = request.Range.Start with { Line = startLine },
End = request.Range.End with { Line = endLine }
};
}

return await base.TranslateRequestAsync(request);
}

protected override Task<SemanticHighlightResponse> TranslateResponse(SemanticHighlightResponse response, SemanticHighlightRequest request)
{
return response.TranslateAsync(Workspace, request);
}
}
183 changes: 183 additions & 0 deletions tests/OmniSharp.Cake.Tests/SemanticHighlightFacts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Text;
using OmniSharp.Cake.Services.RequestHandlers.SemanticHighlight;
using OmniSharp.Models.SemanticHighlight;
using OmniSharp.Models.UpdateBuffer;
using OmniSharp.Models.V2;
using TestUtility;
using Xunit;
using Xunit.Abstractions;
using Range = OmniSharp.Models.V2.Range;

namespace OmniSharp.Cake.Tests
{
public class SemanticHighlightFacts : CakeSingleRequestHandlerTestFixture<SemanticHighlightHandler>
{
public SemanticHighlightFacts(ITestOutputHelper testOutput) : base(testOutput)
{
}

protected override string EndpointName => OmniSharpEndpoints.V2.Highlight;

[Fact]
public async Task SemanticHighlightSingleLine()
{
const string source = @"
class C1
{
class C2 { int n = true; }
}
";

var line = 3;
var highlights = await GetSemanticHighlightsForLineAsync(source, line, versionedText: null);

AssertSyntax(highlights, source, line,
Keyword("class"),
ClassName("C2"),
Punctuation("{"),
Keyword("int"),
Field("n"),
Operator("="),
Keyword("true"),
Punctuation(";"),
Punctuation("}"));
}

[Fact]
public async Task SemanticHighlightEntireFile()
{
const string source = @"
class C1
{
class C2 { int n = true; }
}
";

var highlights = await GetSemanticHighlightsForFileAsync(source);

AssertSyntax(highlights, source, 0,
Keyword("class"),
ClassName("C1"),
Punctuation("{"),
Keyword("class"),
ClassName("C2"),
Punctuation("{"),
Keyword("int"),
Field("n"),
Operator("="),
Keyword("true"),
Punctuation(";"),
Punctuation("}"),
Punctuation("}")
);
}

private Task<SemanticHighlightSpan[]> GetSemanticHighlightsForFileAsync(string source)
{
return GetSemanticHighlightsAsync(source, range: null, versionedText: null);
}

private Task<SemanticHighlightSpan[]> GetSemanticHighlightsForLineAsync(string source, int line, string versionedText)
{
var range = new Range()
{
Start = new Point() { Column = 0, Line = line },
End = new Point() { Column = 0, Line = line + 1 }
};

return GetSemanticHighlightsAsync(source, range, versionedText);
}

private async Task<SemanticHighlightSpan[]> GetSemanticHighlightsAsync(string source, Range range, string versionedText)
{
using (var testProject = await TestAssets.Instance.GetTestProjectAsync("CakeProject", shadowCopy : false))
using (var host = CreateOmniSharpHost(testProject.Directory))
{
var testFile = new TestFile(Path.Combine(testProject.Directory, "build.cake"), source);

var request = new SemanticHighlightRequest
{
FileName = testFile.FileName,
Range = range,
VersionedText = versionedText,
};

var updateBufferRequest = new UpdateBufferRequest
{
Buffer = source,
FileName = request.FileName,
FromDisk = false,
};

await GetUpdateBufferHandler(host).Handle(updateBufferRequest);

var requestHandler = GetRequestHandler(host);

var response = await requestHandler.Handle(request);

return response.Spans;
}
}

private static void AssertSyntax(SemanticHighlightSpan[] highlights, string code, int startLine, params (SemanticHighlightClassification kind, string text, SemanticHighlightModifier[] modifiers)[] expectedTokens)
{
var lineNo = startLine;
var lastIndex = 0;
var lines = SourceText.From(code).Lines;

for (var i = 0; i < highlights.Length; i++)
{
var (type, text, modifiers) = expectedTokens[i];
var highlight = highlights[i];

string line;
int start, end;
do
{
line = lines[lineNo].ToString();
start = line.IndexOf(text, lastIndex);
if (start == -1)
{
if (++lineNo >= lines.Count)
{
throw new Exception($"Could not find token {text} in the code");
}

lastIndex = 0;
}
}
while (start == -1);

end = start + text.Length;
lastIndex = end;

Assert.Equal(type, highlight.Type);
Assert.Equal(modifiers, highlight.Modifiers);
Assert.Equal(lineNo, highlight.StartLine);
Assert.Equal(lineNo, highlight.EndLine);
Assert.Equal(start, highlight.StartColumn);
Assert.Equal(end, highlight.EndColumn);
}

Assert.Equal(expectedTokens.Length, highlights.Length);
}

private static (SemanticHighlightClassification type, string text, SemanticHighlightModifier[] modifiers) Method(string text, params SemanticHighlightModifier[] modifiers) => (SemanticHighlightClassification.MethodName, text, modifiers);
private static (SemanticHighlightClassification type, string text, SemanticHighlightModifier[] modifiers) Local(string text, params SemanticHighlightModifier[] modifiers) => (SemanticHighlightClassification.LocalName, text, modifiers);
private static (SemanticHighlightClassification type, string text, SemanticHighlightModifier[] modifiers) ClassName(string text, params SemanticHighlightModifier[] modifiers) => (SemanticHighlightClassification.ClassName, text, modifiers);
private static (SemanticHighlightClassification type, string text, SemanticHighlightModifier[] modifiers) StructName(string text, params SemanticHighlightModifier[] modifiers) => (SemanticHighlightClassification.StructName, text, modifiers);
private static (SemanticHighlightClassification type, string text, SemanticHighlightModifier[] modifiers) Field(string text, params SemanticHighlightModifier[] modifiers) => (SemanticHighlightClassification.FieldName, text, modifiers);
private static (SemanticHighlightClassification type, string text, SemanticHighlightModifier[] modifiers) Identifier(string text, params SemanticHighlightModifier[] modifiers) => (SemanticHighlightClassification.Identifier, text, modifiers);
private static (SemanticHighlightClassification type, string text, SemanticHighlightModifier[] modifiers) Parameter(string text, params SemanticHighlightModifier[] modifiers) => (SemanticHighlightClassification.ParameterName, text, modifiers);
private static (SemanticHighlightClassification type, string text, SemanticHighlightModifier[] modifiers) NamespaceName(string text, params SemanticHighlightModifier[] modifiers) => (SemanticHighlightClassification.NamespaceName, text, modifiers);
private static (SemanticHighlightClassification type, string text, SemanticHighlightModifier[] modifiers) Keyword(string text, params SemanticHighlightModifier[] modifiers) => (SemanticHighlightClassification.Keyword, text, modifiers);
private static (SemanticHighlightClassification type, string text, SemanticHighlightModifier[] modifiers) ControlKeyword(string text, params SemanticHighlightModifier[] modifiers) => (SemanticHighlightClassification.ControlKeyword, text, modifiers);
private static (SemanticHighlightClassification type, string text, SemanticHighlightModifier[] modifiers) Number(string text, params SemanticHighlightModifier[] modifiers) => (SemanticHighlightClassification.NumericLiteral, text, modifiers);
private static (SemanticHighlightClassification type, string text, SemanticHighlightModifier[] modifiers) Operator(string text, params SemanticHighlightModifier[] modifiers) => (SemanticHighlightClassification.Operator, text, modifiers);
private static (SemanticHighlightClassification type, string text, SemanticHighlightModifier[] modifiers) Punctuation(string text, params SemanticHighlightModifier[] modifiers) => (SemanticHighlightClassification.Punctuation, text, modifiers);
private static (SemanticHighlightClassification type, string text, SemanticHighlightModifier[] modifiers) String(string text, params SemanticHighlightModifier[] modifiers) => (SemanticHighlightClassification.StringLiteral, text, modifiers);
}
}