diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cf2a07d1b..c7f035d54f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog All changes to the project will be documented in this file. +## [1.37.2] - Not Yet Released +* Add support for new quick info endpoint when working with Cake (PR: [#1945](https://github.com/OmniSharp/omnisharp-roslyn/pull/1945)) +* 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)) diff --git a/README.md b/README.md index 25b39abb46..f38be878d0 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ For more details, see [Build](https://github.com/OmniSharp/omnisharp-roslyn/blob ### VS Code -Add the following setting to your [User Settings or Workspace Settings](https://code.visualstudio.com/Docs/customization/userandworkspace). +Add the following setting to your [User Settings](https://code.visualstudio.com/Docs/customization/userandworkspace). ```JSON { @@ -77,7 +77,7 @@ The above option can also be set to: - "latest" - To consume the latest build from the master branch - A specific version number like `1.29.2-beta.60` -In order to be able to attach a debugger, add the following setting: +In order to be able to attach a debugger, add the following setting to your [User or Workspace settings](https://code.visualstudio.com/Docs/customization/userandworkspace): ```JSON { 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 new file mode 100644 index 0000000000..5de5fe1878 --- /dev/null +++ b/src/OmniSharp.Cake/Services/RequestHandlers/Completion/CompletionHandler.cs @@ -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 + { + [ImportingConstructor] + public CompletionHandler(OmniSharpWorkspace workspace) : base(workspace) + { + } + + protected override Task TranslateResponse(CompletionResponse response, CompletionRequest request) + { + return response.TranslateAsync(Workspace, request); + } + } + + [Shared] + [OmniSharpHandler(OmniSharpEndpoints.CompletionResolve, Constants.LanguageNames.Cake)] + public class CompletionResolveHandler : CakeRequestHandler + { + [ImportingConstructor] + public CompletionResolveHandler(OmniSharpWorkspace workspace) : base(workspace) + { + } + + protected override Task 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/src/OmniSharp.Cake/Services/RequestHandlers/QuickInfoHandler.cs b/src/OmniSharp.Cake/Services/RequestHandlers/QuickInfoHandler.cs new file mode 100644 index 0000000000..b3aa32f193 --- /dev/null +++ b/src/OmniSharp.Cake/Services/RequestHandlers/QuickInfoHandler.cs @@ -0,0 +1,16 @@ +using System.Composition; +using OmniSharp.Mef; +using OmniSharp.Models; + +namespace OmniSharp.Cake.Services.RequestHandlers +{ + [Shared] + [OmniSharpHandler(OmniSharpEndpoints.QuickInfo, Constants.LanguageNames.Cake)] + public class QuickInfoHandler : CakeRequestHandler + { + [ImportingConstructor] + public QuickInfoHandler(OmniSharpWorkspace workspace) : base(workspace) + { + } + } +} diff --git a/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpCompletionHandler.cs b/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpCompletionHandler.cs index 1759499372..dfa8b1b37e 100644 --- a/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpCompletionHandler.cs +++ b/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpCompletionHandler.cs @@ -27,10 +27,10 @@ public static IEnumerable Enumerate(RequestHandlers handlers) private static readonly IDictionary _kind = new Dictionary{ // types { "Class", CompletionItemKind.Class }, - { "Delegate", CompletionItemKind.Class }, // need a better option for this. + { "Delegate", CompletionItemKind.Function }, { "Enum", CompletionItemKind.Enum }, { "Interface", CompletionItemKind.Interface }, - { "Struct", CompletionItemKind.Class }, // TODO: Is struct missing from enum? + { "Struct", CompletionItemKind.Struct }, // variables { "Local", CompletionItemKind.Variable }, @@ -38,15 +38,15 @@ public static IEnumerable Enumerate(RequestHandlers handlers) { "RangeVariable", CompletionItemKind.Variable }, // members - { "Const", CompletionItemKind.Value }, // TODO: Is const missing from enum? + { "Const", CompletionItemKind.Constant }, { "EnumMember", CompletionItemKind.Enum }, - { "Event", CompletionItemKind.Function }, // TODO: Is event missing from enum? + { "Event", CompletionItemKind.Event }, { "Field", CompletionItemKind.Field }, { "Method", CompletionItemKind.Method }, { "Property", CompletionItemKind.Property }, // other stuff - { "Label", CompletionItemKind.Unit }, // need a better option for this. + { "Label", CompletionItemKind.Text }, { "Keyword", CompletionItemKind.Keyword }, { "Namespace", CompletionItemKind.Module } }; diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/RunCodeActionService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/RunCodeActionService.cs index 7b6229b620..9a362910ee 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/RunCodeActionService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/RunCodeActionService.cs @@ -68,6 +68,11 @@ public override async Task Handle(RunCodeActionRequest re changes.AddRange(fileChangesResult.FileChanges); solution = fileChangesResult.Solution; } + else + { + o.Apply(this.Workspace, CancellationToken.None); + solution = this.Workspace.CurrentSolution; + } if (request.WantsAllCodeActionOperations) { diff --git a/tests/OmniSharp.Cake.Tests/CompletionFacts.cs b/tests/OmniSharp.Cake.Tests/CompletionFacts.cs new file mode 100644 index 0000000000..83812f57b3 --- /dev/null +++ b/tests/OmniSharp.Cake.Tests/CompletionFacts.cs @@ -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 + { + private const int ImportCompletionTimeout = 1000; + 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 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) + { + 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); + } +} diff --git a/tests/OmniSharp.Cake.Tests/QuickInfoFacts.cs b/tests/OmniSharp.Cake.Tests/QuickInfoFacts.cs new file mode 100644 index 0000000000..0c5ac6040a --- /dev/null +++ b/tests/OmniSharp.Cake.Tests/QuickInfoFacts.cs @@ -0,0 +1,81 @@ +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; +using OmniSharp.Cake.Services.RequestHandlers.Completion; +using OmniSharp.Models; +using OmniSharp.Models.UpdateBuffer; +using OmniSharp.Models.v1.Completion; +using TestUtility; +using Xunit; +using Xunit.Abstractions; + +namespace OmniSharp.Cake.Tests +{ + public class QuickInfoFacts : CakeSingleRequestHandlerTestFixture + { + private readonly ILogger _logger; + + public QuickInfoFacts(ITestOutputHelper testOutput) : base(testOutput) + { + _logger = LoggerFactory.CreateLogger(); + } + + protected override string EndpointName => OmniSharpEndpoints.QuickInfo; + + [Fact] + public async Task ShouldGetQuickInfo() + { + const string input = "Informa$$tion(\"Hello\");"; + + 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 quickInfo = await GetQuickInfo(fileName, input, host); + + Assert.StartsWith("```csharp\nvoid Information(string value)", quickInfo.Markdown); + } + } + + private async Task GetQuickInfo(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 QuickInfoRequest + { + Line = point.Line, + Column = point.Offset, + FileName = testFile.FileName, + Buffer = testFile.Content.Code + }; + + 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); + } + } +}