Skip to content

Commit

Permalink
Merge pull request #1217 from mholo65/feature/GH-1214
Browse files Browse the repository at this point in the history
GH1214: Add cake handler for /codestructure endpoint.
  • Loading branch information
david-driscoll authored Jun 30, 2018
2 parents 6836fad + e2cb458 commit 6e1ced1
Show file tree
Hide file tree
Showing 4 changed files with 322 additions and 3 deletions.
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
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);
}
}
}
}

0 comments on commit 6e1ced1

Please sign in to comment.