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

GH1214: Add cake handler for /codestructure endpoint. #1217

Merged
merged 3 commits into from
Jun 30, 2018
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: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
All changes to the project will be documented in this file.

## [1.32.0] - _Not Yet Released_
* Added new `/codestructure` endpoint which serves a replacement for the `/currentfilemembersastree` endpoint. The new endpoint has a cleaner design, properly supports all C# types and members, and supports more information, such as accessibility, static vs. instance, etc. (PR: [#1211](https://github.com/OmniSharp/omnisharp-roslyn/pull/1211))
* Added new `/codestructure` endpoint which serves a replacement for the `/currentfilemembersastree` endpoint. The new endpoint has a cleaner design, properly supports all C# types and members, and supports more information, such as accessibility, static vs. instance, etc. (PRs: [#1211](https://github.com/OmniSharp/omnisharp-roslyn/pull/1211) [#1217](https://github.com/OmniSharp/omnisharp-roslyn/pull/1217))
* Fixed a bug where language services for newly created CSX files were not provided if no CSX files existed at the moment OmniSharp was started ([#1199](https://github.com/OmniSharp/omnisharp-roslyn/issues/1199), PR: [#1210](https://github.com/OmniSharp/omnisharp-roslyn/pull/1210))
* The legacy project.json support is now disabled by default, allowing OmniSharp to start up a bit faster for common scenarios. If you wish to enable project.json support, add the following setting to your `omnisharp.json` file. (PR: [#1194](https://github.com/OmniSharp/omnisharp-roslyn/pull/1194))

Expand Down
86 changes: 84 additions & 2 deletions src/OmniSharp.Cake/Extensions/ResponseExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
using OmniSharp.Models.Navigate;
using OmniSharp.Models.MembersTree;
using OmniSharp.Models.Rename;
using OmniSharp.Models.V2;
using OmniSharp.Models.V2.CodeActions;
using OmniSharp.Models.V2.CodeStructure;
using OmniSharp.Utilities;

namespace OmniSharp.Cake.Extensions
Expand Down Expand Up @@ -121,7 +123,8 @@ public static async Task<RenameResponse> TranslateAsync(this RenameResponse resp
return response;
}

public static async Task<RunCodeActionResponse> TranslateAsync(this RunCodeActionResponse response, OmniSharpWorkspace workspace)
public static async Task<RunCodeActionResponse> TranslateAsync(this RunCodeActionResponse response,
OmniSharpWorkspace workspace)
{
if (response?.Changes == null)
{
Expand All @@ -146,7 +149,7 @@ public static async Task<RunCodeActionResponse> TranslateAsync(this RunCodeActio
{

if (fileOperations.FirstOrDefault(x => x.FileName == change.Key &&
x.ModificationType == FileModificationType.Modified)
x.ModificationType == FileModificationType.Modified)
is ModifiedFileResponse modifiedFile)
{
modifiedFile.Changes = change.Value;
Expand All @@ -157,6 +160,85 @@ public static async Task<RunCodeActionResponse> TranslateAsync(this RunCodeActio
return response;
}

public static async Task<CodeStructureResponse> TranslateAsync(this CodeStructureResponse response, OmniSharpWorkspace workspace, CodeStructureRequest request)
{
var zeroIndex = await LineIndexHelper.TranslateToGenerated(request.FileName, 0, workspace);
var elements = new List<CodeElement>();

foreach (var element in response.Elements)
{
if (element.Ranges.Values.Any(x => x.Start.Line < zeroIndex))
{
continue;
}

var translatedElement = await element.TranslateAsync(workspace, request);

if (translatedElement.Ranges.Values.Any(x => x.Start.Line < 0))
{
continue;
}

elements.Add(translatedElement);
}

response.Elements = elements;
return response;
}

private static async Task<CodeElement> TranslateAsync(this CodeElement element, OmniSharpWorkspace workspace, SimpleFileRequest request)
{
var builder = new CodeElement.Builder
{
Kind = element.Kind,
DisplayName = element.DisplayName,
Name = element.Name
};

foreach (var property in element.Properties ?? Enumerable.Empty<KeyValuePair<string, object>>())
{
builder.AddProperty(property.Key, property.Value);
}

foreach (var range in element.Ranges ?? Enumerable.Empty<KeyValuePair<string, Range>>())
{
builder.AddRange(range.Key, await range.Value.TranslateAsync(workspace, request));
}

foreach (var childElement in element.Children ?? Enumerable.Empty<CodeElement>())
{
var translatedElement = await childElement.TranslateAsync(workspace, request);

// This is plain stupid, but someone might put a #load directive inside a method or class
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this supported in Cake?
In scripting this would generate CS8098 "Cannot use #load after first token in file"
https://github.com/dotnet/roslyn/blob/ca2f6ad9ab68518344c2d3e25c0731c83d136701/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs#L1329

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it is supported since Cake only includes the content of #load:ed files. It doesn't rely on the Roslyn #load directive currently. I'm about to look into changing that behavior however 😄

if (translatedElement.Ranges.Values.Any(x => x.Start.Line < 0))
{
continue;
}

builder.AddChild(childElement);
}

return builder.ToCodeElement();
}

private static async Task<Range> TranslateAsync(this Range range, OmniSharpWorkspace workspace, SimpleFileRequest request)
{
var (line, _) = await LineIndexHelper.TranslateFromGenerated(request.FileName, range.Start.Line, workspace, true);

if (range.Start.Line == range.End.Line)
{
range.Start.Line = line;
range.End.Line = line;
return range;
}

range.Start.Line = line;
(line, _) = await LineIndexHelper.TranslateFromGenerated(request.FileName, range.End.Line, workspace, true);
range.End.Line = line;

return range;
}

private static async Task<FileMemberElement> TranslateAsync(this FileMemberElement element, OmniSharpWorkspace workspace, Request request)
{
element.Location = await element.Location.TranslateAsync(workspace, request);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Composition;
using System.Threading.Tasks;
using OmniSharp.Cake.Extensions;
using OmniSharp.Mef;
using OmniSharp.Models.V2.CodeStructure;

namespace OmniSharp.Cake.Services.RequestHandlers.Structure
{
[OmniSharpHandler(OmniSharpEndpoints.V2.CodeStructure, Constants.LanguageNames.Cake), Shared]
public class CodeStructureHandler : CakeRequestHandler<CodeStructureRequest, CodeStructureResponse>
{
[ImportingConstructor]
public CodeStructureHandler(
OmniSharpWorkspace workspace)
: base(workspace)
{
}

protected override Task<CodeStructureResponse> TranslateResponse(CodeStructureResponse response, CodeStructureRequest request)
{
return response.TranslateAsync(Workspace, request);
}
}
}
213 changes: 213 additions & 0 deletions tests/OmniSharp.Cake.Tests/CodeStructureFacts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using OmniSharp.Cake.Services.RequestHandlers.Diagnostics;
using OmniSharp.Cake.Services.RequestHandlers.Structure;
using OmniSharp.Models;
using OmniSharp.Models.CodeCheck;
using OmniSharp.Models.UpdateBuffer;
using OmniSharp.Models.V2;
using OmniSharp.Models.V2.CodeStructure;
using TestUtility;
using Xunit;
using Xunit.Abstractions;

namespace OmniSharp.Cake.Tests
{
public class CodeStructureFacts : CakeSingleRequestHandlerTestFixture<CodeStructureHandler>
{
private readonly ILogger _logger;

public CodeStructureFacts(ITestOutputHelper testOutput) : base(testOutput)
{
_logger = LoggerFactory.CreateLogger<CodeStructureHandler>();
}

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

[Fact]
public async Task AllTypes()
{
const string source = @"
class C { }
delegate void D(int i, ref string s);
enum E { One, Two, Three }
interface I { }
struct S { }
";

var (response, _) = await GetCodeStructureAsync(source);

Assert.Equal(5, response.Elements.Count);
AssertElement(response.Elements[0], SymbolKinds.Class, "C", "C");
AssertElement(response.Elements[1], SymbolKinds.Delegate, "D", "D");
AssertElement(response.Elements[2], SymbolKinds.Enum, "E", "E");
AssertElement(response.Elements[3], SymbolKinds.Interface, "I", "I");
AssertElement(response.Elements[4], SymbolKinds.Struct, "S", "S");
}

[Fact]
public async Task AllTypesWithLoadedFile()
{
const string source = @"
#load foo.cake
class C { }
delegate void D(int i, ref string s);
enum E { One, Two, Three }
interface I { }
struct S { }
";

var (response, _) = await GetCodeStructureAsync(source);

Assert.Equal(5, response.Elements.Count);
AssertElement(response.Elements[0], SymbolKinds.Class, "C", "C");
AssertElement(response.Elements[1], SymbolKinds.Delegate, "D", "D");
AssertElement(response.Elements[2], SymbolKinds.Enum, "E", "E");
AssertElement(response.Elements[3], SymbolKinds.Interface, "I", "I");
AssertElement(response.Elements[4], SymbolKinds.Struct, "S", "S");
}

[Fact]
public async Task TestClassMembersNameRanges()
{
const string source = @"
class C
{
private int {|name_f:_f|};
private int {|name_f1:_f1|}, {|name_f2:_f2|};
private const int {|name_c:_c|};
public {|nameCtor:C|}() { }
~{|nameDtor:C|}() { }
public void {|nameM1:M1|}() { }
public void {|nameM2:M2|}(int i, ref string s, params object[] array) { }
public static implicit operator {|nameOpC:C|}(int i) { return null; }
public static C operator {|nameOpPlus:+|}(C c1, C c2) { return null; }
public int {|nameP:P|} { get; set; }
public event EventHandler {|nameE:E|};
public event EventHandler {|nameE1:E1|}, {|nameE2:E2|};
public event EventHandler {|nameE3:E3|} { add { } remove { } }
internal int {|nameThis:this|}[int index] => 42;
}
";

var (response, testFile) = await GetCodeStructureAsync(source);

var elementC = Assert.Single(response.Elements);

AssertRange(elementC.Children[0], testFile.Content, "name_f", "name");
AssertRange(elementC.Children[1], testFile.Content, "name_f1", "name");
AssertRange(elementC.Children[2], testFile.Content, "name_f2", "name");
AssertRange(elementC.Children[3], testFile.Content, "name_c", "name");
AssertRange(elementC.Children[4], testFile.Content, "nameCtor", "name");
AssertRange(elementC.Children[5], testFile.Content, "nameDtor", "name");
AssertRange(elementC.Children[6], testFile.Content, "nameM1", "name");
AssertRange(elementC.Children[7], testFile.Content, "nameM2", "name");
AssertRange(elementC.Children[8], testFile.Content, "nameOpC", "name");
AssertRange(elementC.Children[9], testFile.Content, "nameOpPlus", "name");
AssertRange(elementC.Children[10], testFile.Content, "nameP", "name");
AssertRange(elementC.Children[11], testFile.Content, "nameE", "name");
AssertRange(elementC.Children[12], testFile.Content, "nameE1", "name");
AssertRange(elementC.Children[13], testFile.Content, "nameE2", "name");
AssertRange(elementC.Children[14], testFile.Content, "nameE3", "name");
AssertRange(elementC.Children[15], testFile.Content, "nameThis", "name");
}

[Fact]
public async Task TestClassMembersNameRangesWithLoadedFile()
{
const string source = @"
class C
{
private int {|name_f:_f|};
private int {|name_f1:_f1|}, {|name_f2:_f2|};
private const int {|name_c:_c|};
public {|nameCtor:C|}() { }
~{|nameDtor:C|}() { }
public void {|nameM1:M1|}() { }
public void {|nameM2:M2|}(int i, ref string s, params object[] array) { }
public static implicit operator {|nameOpC:C|}(int i) { return null; }
public static C operator {|nameOpPlus:+|}(C c1, C c2) { return null; }
public int {|nameP:P|} { get; set; }
public event EventHandler {|nameE:E|};
public event EventHandler {|nameE1:E1|}, {|nameE2:E2|};
public event EventHandler {|nameE3:E3|} { add { } remove { } }
#load foo.cake
internal int {|nameThis:this|}[int index] => 42;
}
";

var (response, testFile) = await GetCodeStructureAsync(source);

var elementC = Assert.Single(response.Elements);

AssertRange(elementC.Children[0], testFile.Content, "name_f", "name");
AssertRange(elementC.Children[1], testFile.Content, "name_f1", "name");
AssertRange(elementC.Children[2], testFile.Content, "name_f2", "name");
AssertRange(elementC.Children[3], testFile.Content, "name_c", "name");
AssertRange(elementC.Children[4], testFile.Content, "nameCtor", "name");
AssertRange(elementC.Children[5], testFile.Content, "nameDtor", "name");
AssertRange(elementC.Children[6], testFile.Content, "nameM1", "name");
AssertRange(elementC.Children[7], testFile.Content, "nameM2", "name");
AssertRange(elementC.Children[8], testFile.Content, "nameOpC", "name");
AssertRange(elementC.Children[9], testFile.Content, "nameOpPlus", "name");
AssertRange(elementC.Children[10], testFile.Content, "nameP", "name");
AssertRange(elementC.Children[11], testFile.Content, "nameE", "name");
AssertRange(elementC.Children[12], testFile.Content, "nameE1", "name");
AssertRange(elementC.Children[13], testFile.Content, "nameE2", "name");
AssertRange(elementC.Children[14], testFile.Content, "nameE3", "name");
AssertRange(elementC.Children[15], testFile.Content, "nameThis", "name");
}

private static void AssertRange(CodeElement elementC, TestContent content, string contentSpanName, string elementRangeName)
{
var span = Assert.Single(content.GetSpans(contentSpanName));
var range = content.GetRangeFromSpan(span).ToRange();
Assert.Equal(range, elementC.Ranges[elementRangeName]);
}

private static void AssertElement(CodeElement element, string kind, string name, string displayName, string accessibility = null, bool? @static = null)
{
Assert.Equal(kind, element.Kind);
Assert.Equal(name, element.Name);
Assert.Equal(displayName, element.DisplayName);

if (accessibility != null)
{
Assert.Equal(accessibility, element.Properties[SymbolPropertyNames.Accessibility]);
}

if (@static != null)
{
Assert.Equal(@static, element.Properties[SymbolPropertyNames.Static]);
}
}

private async Task<(CodeStructureResponse, TestFile)> GetCodeStructureAsync(string contents)
{
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"), contents);

var request = new CodeStructureRequest
{
FileName = testFile.FileName
};

var updateBufferRequest = new UpdateBufferRequest
{
Buffer = testFile.Content.Code,
FileName = testFile.FileName,
FromDisk = false
};

await GetUpdateBufferHandler(host).Handle(updateBufferRequest);

var requestHandler = GetRequestHandler(host);

return (await requestHandler.Handle(request), testFile);
}
}
}
}