Skip to content

Commit

Permalink
Merge pull request #1944 from mholo65/feature/cake-completion
Browse files Browse the repository at this point in the history
Adds support for /completion and /completion/resolve endpoints for Cake.
  • Loading branch information
JoeRobich authored Sep 15, 2020
2 parents 23d6ced + 5e8a210 commit 8f10a7f
Show file tree
Hide file tree
Showing 4 changed files with 332 additions and 16 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Changelog
All changes to the project will be documented in this file.

## [1.37.2] - Not Yet Released
* Add support for new completion endpoints when working with Cake ([#1939](https://github.com/OmniSharp/omnisharp-roslyn/issues/1939), PR: [#1944](https://github.com/OmniSharp/omnisharp-roslyn/pull/1944))

## [1.37.1] - 2020-09-01
* Ensure that all quickinfo sections have linebreaks between them, and don't add unecessary duplicate linebreaks (PR: [#1900](https://github.com/OmniSharp/omnisharp-roslyn/pull/1900))
* Support completion of unimported types (PR: [#1896](https://github.com/OmniSharp/omnisharp-roslyn/pull/1896))
Expand Down
65 changes: 49 additions & 16 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.v1.Completion;
using OmniSharp.Models.V2;
using OmniSharp.Models.V2.CodeActions;
using OmniSharp.Models.V2.CodeStructure;
Expand All @@ -27,22 +28,6 @@ public static QuickFixResponse OnlyThisFile(this QuickFixResponse response, stri
var quickFixes = response.QuickFixes.Where(x => PathsAreEqual(x.FileName, fileName));
response.QuickFixes = quickFixes;
return response;

bool PathsAreEqual(string x, string y)
{
if (x == null && y == null)
{
return true;
}
if (x == null || y == null)
{
return false;
}

var comparer = PlatformHelper.IsWindows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;

return Path.GetFullPath(x).Equals(Path.GetFullPath(y), comparer);
}
}

public static Task<QuickFixResponse> TranslateAsync(this QuickFixResponse response, OmniSharpWorkspace workspace)
Expand Down Expand Up @@ -211,6 +196,38 @@ public static async Task<BlockStructureResponse> TranslateAsync(this BlockStruct
return response;
}

public static async Task<CompletionResponse> TranslateAsync(this CompletionResponse response, OmniSharpWorkspace workspace, CompletionRequest request)
{
foreach (var item in response.Items)
{
if (item.AdditionalTextEdits is null)
{
continue;
}

List<LinePositionSpanTextChange> additionalTextEdits = null;

foreach (var additionalTextEdit in item.AdditionalTextEdits)
{
var (_, change) = await additionalTextEdit.TranslateAsync(workspace, request.FileName);

// Due to the fact that AdditionalTextEdits return the complete buffer, we can't currently use that in Cake.
// Revisit when we have a solution. At this point it's probably just best to remove AdditionalTextEdits.
if (change.StartLine < 0)
{
continue;
}

additionalTextEdits ??= new List<LinePositionSpanTextChange>();
additionalTextEdits.Add(change);
}

item.AdditionalTextEdits = additionalTextEdits;
}

return response;
}

private static async Task<CodeElement> TranslateAsync(this CodeElement element, OmniSharpWorkspace workspace, SimpleFileRequest request)
{
var builder = new CodeElement.Builder
Expand Down Expand Up @@ -345,5 +362,21 @@ private static async Task PopulateModificationsAsync(

return (newFileName, change);
}

private static bool PathsAreEqual(string x, string y)
{
if (x == null && y == null)
{
return true;
}
if (x == null || y == null)
{
return false;
}

var comparer = PlatformHelper.IsWindows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;

return Path.GetFullPath(x).Equals(Path.GetFullPath(y), comparer);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Composition;
using System.Linq;
using System.Threading.Tasks;
using OmniSharp.Cake.Extensions;
using OmniSharp.Mef;
using OmniSharp.Models.v1.Completion;

namespace OmniSharp.Cake.Services.RequestHandlers.Completion
{
[Shared]
[OmniSharpHandler(OmniSharpEndpoints.Completion, Constants.LanguageNames.Cake)]
public class CompletionHandler : CakeRequestHandler<CompletionRequest, CompletionResponse>
{
[ImportingConstructor]
public CompletionHandler(OmniSharpWorkspace workspace) : base(workspace)
{
}

protected override Task<CompletionResponse> TranslateResponse(CompletionResponse response, CompletionRequest request)
{
return response.TranslateAsync(Workspace, request);
}
}

[Shared]
[OmniSharpHandler(OmniSharpEndpoints.CompletionResolve, Constants.LanguageNames.Cake)]
public class CompletionResolveHandler : CakeRequestHandler<CompletionResolveRequest, CompletionResolveResponse>
{
[ImportingConstructor]
public CompletionResolveHandler(OmniSharpWorkspace workspace) : base(workspace)
{
}

protected override Task<CompletionResolveResponse> TranslateResponse(CompletionResolveResponse response, CompletionResolveRequest request)
{
// Due to the fact that AdditionalTextEdits return the complete buffer, we can't currently use that in Cake.
// Revisit when we have a solution. At this point it's probably just best to remove AdditionalTextEdits.
if (response.Item is object)
{
response.Item.AdditionalTextEdits = null;
}

return Task.FromResult(response);
}
}
}
234 changes: 234 additions & 0 deletions tests/OmniSharp.Cake.Tests/CompletionFacts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using OmniSharp.Cake.Services.RequestHandlers.Completion;
using OmniSharp.Models.UpdateBuffer;
using OmniSharp.Models.v1.Completion;
using TestUtility;
using Xunit;
using Xunit.Abstractions;

namespace OmniSharp.Cake.Tests
{
public class CompletionFacts : CakeSingleRequestHandlerTestFixture<CompletionHandler>
{
private const int ImportCompletionTimeout = 1000;
private readonly ILogger _logger;

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

protected override string EndpointName => OmniSharpEndpoints.Completion;

[Fact]
public async Task ShouldGetCompletionFromHostObject()
{
const string input = @"TaskSe$$";

using (var testProject = await TestAssets.Instance.GetTestProjectAsync("CakeProject", shadowCopy : false))
using (var host = CreateOmniSharpHost(testProject.Directory))
{
var fileName = Path.Combine(testProject.Directory, "build.cake");
var completions = await FindCompletionsAsync(fileName, input, host);

Assert.Contains("TaskSetup", completions.Items.Select(c => c.Label));
Assert.Contains("TaskSetup", completions.Items.Select(c => c.InsertText));
}
}

[Fact]
public async Task ShouldGetCompletionFromDSL()
{
const string input =
@"Task(""Test"")
.Does(() => {
Inform$$
});";

using (var testProject = await TestAssets.Instance.GetTestProjectAsync("CakeProject", shadowCopy : false))
using (var host = CreateOmniSharpHost(testProject.Directory))
{
var fileName = Path.Combine(testProject.Directory, "build.cake");
var completions = await FindCompletionsAsync(fileName, input, host);

Assert.Contains("Information", completions.Items.Select(c => c.Label));
Assert.Contains("Information", completions.Items.Select(c => c.InsertText));
}
}

[Fact]
public async Task ShouldResolveFromDSL()
{
const string input =
@"Task(""Test"")
.Does(() => {
Inform$$
});";

using (var testProject = await TestAssets.Instance.GetTestProjectAsync("CakeProject", shadowCopy : false))
using (var host = CreateOmniSharpHost(testProject.Directory))
{
var fileName = Path.Combine(testProject.Directory, "build.cake");
var completion = (await FindCompletionsAsync(fileName, input, host))
.Items.First(x => x.Preselect && x.InsertText == "Information");

var resolved = await ResolveCompletionAsync(completion, host);

Assert.StartsWith(
"```csharp\nvoid Information(string format, params object[] args)",
resolved.Item?.Documentation);
}
}

[Fact]
public async Task ShouldRemoveAdditionalTextEditsFromResolvedCompletions()
{
const string input = @"var regex = new Rege$$";

using (var testProject = await TestAssets.Instance.GetTestProjectAsync("CakeProject", shadowCopy : false))
using (var host = CreateOmniSharpHost(testProject.Directory,
new[] { new KeyValuePair<string, string>("RoslynExtensionsOptions:EnableImportCompletion", "true") }))
{
var fileName = Path.Combine(testProject.Directory, "build.cake");

// First completion request should kick off the task to update the completion cache.
var completions = await FindCompletionsAsync(fileName, input, host);
Assert.True(completions.IsIncomplete);
Assert.DoesNotContain("Regex", completions.Items.Select(c => c.InsertText));

// Populating the completion cache should take no more than a few ms, don't let it take too
// long
var cts = new CancellationTokenSource(millisecondsDelay: ImportCompletionTimeout);
await Task.Run(async () =>
{
while (completions.IsIncomplete)
{
completions = await FindCompletionsAsync(fileName, input, host);
cts.Token.ThrowIfCancellationRequested();
}
}, cts.Token);

Assert.False(completions.IsIncomplete);
Assert.Contains("Regex", completions.Items.Select(c => c.InsertText));

var completion = completions.Items.First(c => c.InsertText == "Regex");
var resolved = await ResolveCompletionAsync(completion, host);

// Due to the fact that AdditionalTextEdits return the complete buffer, we can't currently use that in Cake.
// Revisit when we have a solution. At this point it's probably just best to remove AdditionalTextEdits.
Assert.Null(resolved.Item.AdditionalTextEdits);
}
}

[Fact]
public async Task ShouldGetAdditionalTextEditsFromOverrideCompletion()
{
const string source = @"
class Foo
{
public virtual void Test(string text) {}
public virtual void Test(string text, string moreText) {}
}
class FooChild : Foo
{
override $$
}
";

using (var testProject = await TestAssets.Instance.GetTestProjectAsync("CakeProject", shadowCopy : false))
using (var host = CreateOmniSharpHost(testProject.Directory))
{
var fileName = Path.Combine(testProject.Directory, "build.cake");
var completions = await FindCompletionsAsync(fileName, source, host);
Assert.Equal(
new[]
{
"Equals(object obj)", "GetHashCode()", "Test(string text)",
"Test(string text, string moreText)", "ToString()"
},
completions.Items.Select(c => c.Label));
Assert.Equal(new[]
{
"Equals(object obj)\n {\n return base.Equals(obj);$0\n \\}",
"GetHashCode()\n {\n return base.GetHashCode();$0\n \\}",
"Test(string text)\n {\n base.Test(text);$0\n \\}",
"Test(string text, string moreText)\n {\n base.Test(text, moreText);$0\n \\}",
"ToString()\n {\n return base.ToString();$0\n \\}"
},
completions.Items.Select<CompletionItem, string>(c => c.InsertText));

Assert.Equal(new[]
{
"public override bool",
"public override int",
"public override void",
"public override void",
"public override string"
},
completions.Items.Select(c => c.AdditionalTextEdits.Single().NewText));

Assert.All(completions.Items.Select(c => c.AdditionalTextEdits.Single()),
r =>
{
Assert.Equal(9, r.StartLine);
Assert.Equal(4, r.StartColumn);
Assert.Equal(9, r.EndLine);
Assert.Equal(12, r.EndColumn);
});

Assert.All(completions.Items, c => Assert.Equal(InsertTextFormat.Snippet, c.InsertTextFormat));
}
}

private async Task<CompletionResponse> FindCompletionsAsync(string filename, string source, OmniSharpTestHost host, char? triggerChar = null, TestFile[] additionalFiles = null)
{
var testFile = new TestFile(filename, source);

var files = new[] { testFile };
if (additionalFiles is object)
{
files = files.Concat(additionalFiles).ToArray();
}

host.AddFilesToWorkspace(files);
var point = testFile.Content.GetPointFromPosition();

var request = new CompletionRequest
{
Line = point.Line,
Column = point.Offset,
FileName = testFile.FileName,
Buffer = testFile.Content.Code,
CompletionTrigger = triggerChar is object ? CompletionTriggerKind.TriggerCharacter : CompletionTriggerKind.Invoked,
TriggerCharacter = triggerChar
};

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

await GetUpdateBufferHandler(host).Handle(updateBufferRequest);

var requestHandler = GetRequestHandler(host);

return await requestHandler.Handle(request);
}

private static async Task<CompletionResolveResponse> ResolveCompletionAsync(CompletionItem completionItem, OmniSharpTestHost testHost)
=> await GetResolveHandler(testHost).Handle(new CompletionResolveRequest { Item = completionItem });

private static CompletionResolveHandler GetResolveHandler(OmniSharpTestHost host)
=> host.GetRequestHandler<CompletionResolveHandler>(OmniSharpEndpoints.CompletionResolve, Constants.LanguageNames.Cake);
}
}

0 comments on commit 8f10a7f

Please sign in to comment.