From ce46cec092b5521165764bd613bf41e7aff85721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Bj=C3=B6rkstr=C3=B6m?= Date: Tue, 15 Sep 2020 13:42:30 +0300 Subject: [PATCH 1/3] Adds support for /completion and /completion/resolve endpoints for Cake. --- .../Completion/CompletionHandler.cs | 26 +++ tests/OmniSharp.Cake.Tests/CompletionFacts.cs | 150 ++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 src/OmniSharp.Cake/Services/RequestHandlers/Completion/CompletionHandler.cs create mode 100644 tests/OmniSharp.Cake.Tests/CompletionFacts.cs diff --git a/src/OmniSharp.Cake/Services/RequestHandlers/Completion/CompletionHandler.cs b/src/OmniSharp.Cake/Services/RequestHandlers/Completion/CompletionHandler.cs new file mode 100644 index 0000000000..9aa7a94982 --- /dev/null +++ b/src/OmniSharp.Cake/Services/RequestHandlers/Completion/CompletionHandler.cs @@ -0,0 +1,26 @@ +using System.Composition; +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 + { + [ImportingConstructor] + public CompletionHandler(OmniSharpWorkspace workspace) : base(workspace) + { + } + } + + [Shared] + [OmniSharpHandler(OmniSharpEndpoints.CompletionResolve, Constants.LanguageNames.Cake)] + public class CompletionResolveHandler : CakeRequestHandler + { + [ImportingConstructor] + public CompletionResolveHandler(OmniSharpWorkspace workspace) : base(workspace) + { + } + } +} diff --git a/tests/OmniSharp.Cake.Tests/CompletionFacts.cs b/tests/OmniSharp.Cake.Tests/CompletionFacts.cs new file mode 100644 index 0000000000..c474e44dcf --- /dev/null +++ b/tests/OmniSharp.Cake.Tests/CompletionFacts.cs @@ -0,0 +1,150 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using OmniSharp.Cake.Services.RequestHandlers.Completion; +using OmniSharp.Cake.Services.RequestHandlers.Intellisense; +using OmniSharp.Models.AutoComplete; +using OmniSharp.Models.UpdateBuffer; +using OmniSharp.Models.v1.Completion; +using TestUtility; +using Xunit; +using Xunit.Abstractions; + +namespace OmniSharp.Cake.Tests +{ + public class CompletionFacts : CakeSingleRequestHandlerTestFixture + { + private readonly ILogger _logger; + + public CompletionFacts(ITestOutputHelper testOutput) : base(testOutput) + { + _logger = LoggerFactory.CreateLogger(); + } + + 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 ShouldGetCompletionWithAdditionalTextEdits() + // { + // const string input = @"Regex.Repl$$"; + // + // 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("Replace", completions.Items.Select(c => c.Label)); + // Assert.Contains("Replace", completions.Items.Select(c => c.InsertText)); + // } + // } + + private async Task 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 ResolveCompletionAsync(CompletionItem completionItem, OmniSharpTestHost testHost) + => await GetResolveHandler(testHost).Handle(new CompletionResolveRequest { Item = completionItem }); + + private static CompletionResolveHandler GetResolveHandler(OmniSharpTestHost host) + => host.GetRequestHandler(OmniSharpEndpoints.CompletionResolve, Constants.LanguageNames.Cake); + } +} From adc4674cc6f26b4c304ff4343d9e21d091e5f683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Bj=C3=B6rkstr=C3=B6m?= Date: Tue, 15 Sep 2020 22:19:13 +0300 Subject: [PATCH 2/3] Remove those additional texts which contains whole buffer for Cake files. --- .../Extensions/ResponseExtensions.cs | 65 +++++++--- .../Completion/CompletionHandler.cs | 20 +++ tests/OmniSharp.Cake.Tests/CompletionFacts.cs | 120 +++++++++++++++--- 3 files changed, 171 insertions(+), 34 deletions(-) diff --git a/src/OmniSharp.Cake/Extensions/ResponseExtensions.cs b/src/OmniSharp.Cake/Extensions/ResponseExtensions.cs index 2e3bf8772c..ee2fba4ce5 100644 --- a/src/OmniSharp.Cake/Extensions/ResponseExtensions.cs +++ b/src/OmniSharp.Cake/Extensions/ResponseExtensions.cs @@ -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; @@ -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 TranslateAsync(this QuickFixResponse response, OmniSharpWorkspace workspace) @@ -211,6 +196,38 @@ public static async Task TranslateAsync(this BlockStruct return response; } + public static async Task TranslateAsync(this CompletionResponse response, OmniSharpWorkspace workspace, CompletionRequest request) + { + foreach (var item in response.Items) + { + if (item.AdditionalTextEdits is null) + { + continue; + } + + List 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(); + additionalTextEdits.Add(change); + } + + item.AdditionalTextEdits = additionalTextEdits; + } + + return response; + } + private static async Task TranslateAsync(this CodeElement element, OmniSharpWorkspace workspace, SimpleFileRequest request) { var builder = new CodeElement.Builder @@ -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); + } } } diff --git a/src/OmniSharp.Cake/Services/RequestHandlers/Completion/CompletionHandler.cs b/src/OmniSharp.Cake/Services/RequestHandlers/Completion/CompletionHandler.cs index 9aa7a94982..5de5fe1878 100644 --- a/src/OmniSharp.Cake/Services/RequestHandlers/Completion/CompletionHandler.cs +++ b/src/OmniSharp.Cake/Services/RequestHandlers/Completion/CompletionHandler.cs @@ -1,4 +1,7 @@ using System.Composition; +using System.Linq; +using System.Threading.Tasks; +using OmniSharp.Cake.Extensions; using OmniSharp.Mef; using OmniSharp.Models.v1.Completion; @@ -12,6 +15,11 @@ public class CompletionHandler : CakeRequestHandler TranslateResponse(CompletionResponse response, CompletionRequest request) + { + return response.TranslateAsync(Workspace, request); + } } [Shared] @@ -22,5 +30,17 @@ public class CompletionResolveHandler : CakeRequestHandler 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); + } } } diff --git a/tests/OmniSharp.Cake.Tests/CompletionFacts.cs b/tests/OmniSharp.Cake.Tests/CompletionFacts.cs index c474e44dcf..83812f57b3 100644 --- a/tests/OmniSharp.Cake.Tests/CompletionFacts.cs +++ b/tests/OmniSharp.Cake.Tests/CompletionFacts.cs @@ -1,12 +1,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using OmniSharp.Cake.Services.RequestHandlers.Completion; -using OmniSharp.Cake.Services.RequestHandlers.Intellisense; -using OmniSharp.Models.AutoComplete; using OmniSharp.Models.UpdateBuffer; using OmniSharp.Models.v1.Completion; using TestUtility; @@ -17,6 +15,7 @@ namespace OmniSharp.Cake.Tests { public class CompletionFacts : CakeSingleRequestHandlerTestFixture { + private const int ImportCompletionTimeout = 1000; private readonly ILogger _logger; public CompletionFacts(ITestOutputHelper testOutput) : base(testOutput) @@ -86,21 +85,106 @@ public async Task ShouldResolveFromDSL() } } - // [Fact] - // public async Task ShouldGetCompletionWithAdditionalTextEdits() - // { - // const string input = @"Regex.Repl$$"; - // - // 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("Replace", completions.Items.Select(c => c.Label)); - // Assert.Contains("Replace", completions.Items.Select(c => c.InsertText)); - // } - // } + [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("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(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 FindCompletionsAsync(string filename, string source, OmniSharpTestHost host, char? triggerChar = null, TestFile[] additionalFiles = null) { From 5e8a2103985b7a74ad00f570518f494b17a57782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Bj=C3=B6rkstr=C3=B6m?= Date: Tue, 15 Sep 2020 22:43:49 +0300 Subject: [PATCH 3/3] Add release notes --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cf2a07d1b..841a42afac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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))