From a384e724ddf2893a34edbd524983c7fff7aa4d88 Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Sun, 31 Mar 2019 22:27:41 -0600 Subject: [PATCH 1/5] WIP: Improve path auto-completion --- .../LanguageServer/Completion.cs | 13 +++++++++++++ .../Server/LanguageServer.cs | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/Completion.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Completion.cs index 38fb9e431..4eef058a4 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/Completion.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/Completion.cs @@ -58,6 +58,12 @@ public enum CompletionItemKind Folder = 19 } + public enum InsertTextFormat + { + PlainText = 1, + Snippet = 2, + } + [DebuggerDisplay("NewText = {NewText}, Range = {Range.Start.Line}:{Range.Start.Character} - {Range.End.Line}:{Range.End.Character}")] public class TextEdit { @@ -69,6 +75,11 @@ public class TextEdit [DebuggerDisplay("Kind = {Kind.ToString()}, Label = {Label}, Detail = {Detail}")] public class CompletionItem { + public CompletionItem() + { + this.InsertTextFormat = InsertTextFormat.PlainText; + } + public string Label { get; set; } public CompletionItemKind? Kind { get; set; } @@ -86,6 +97,8 @@ public class CompletionItem public string InsertText { get; set; } + public InsertTextFormat InsertTextFormat { get; set; } + public Range Range { get; set; } public string[] CommitCharacters { get; set; } diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index a5d3d3c86..75c79413d 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -1915,6 +1915,8 @@ private static CompletionItem CreateCompletionItem( { string detailString = null; string documentationString = null; + string completionText = completionDetails.CompletionText; + InsertTextFormat insertTextFormat = InsertTextFormat.PlainText; if ((completionDetails.CompletionType == CompletionType.Variable) || (completionDetails.CompletionType == CompletionType.ParameterName)) @@ -1956,6 +1958,14 @@ private static CompletionItem CreateCompletionItem( } } } + else if (((completionDetails.CompletionType == CompletionType.File) || + (completionDetails.CompletionType == CompletionType.Folder)) && + (completionText.EndsWith("\"") || completionText.EndsWith("'"))) + { + int len = completionDetails.CompletionText.Length; + completionText = completionDetails.CompletionText.Insert(len - 1, "$0"); + insertTextFormat = InsertTextFormat.Snippet; + } // Force the client to maintain the sort order in which the // original completion results were returned. We just need to @@ -1966,7 +1976,8 @@ private static CompletionItem CreateCompletionItem( return new CompletionItem { - InsertText = completionDetails.CompletionText, + InsertText = completionText, + InsertTextFormat = insertTextFormat, Label = completionDetails.ListItemText, Kind = MapCompletionKind(completionDetails.CompletionType), Detail = detailString, @@ -1975,7 +1986,7 @@ private static CompletionItem CreateCompletionItem( FilterText = completionDetails.CompletionText, TextEdit = new TextEdit { - NewText = completionDetails.CompletionText, + NewText = completionText, Range = new Range { Start = new Position From 03593cc8b4f212a356a8b7da1ed792df6273cb2f Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Tue, 2 Apr 2019 19:46:00 -0600 Subject: [PATCH 2/5] Add comment to address PR feedback --- .../Server/LanguageServer.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index 75c79413d..21c407a18 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -1962,6 +1962,12 @@ private static CompletionItem CreateCompletionItem( (completionDetails.CompletionType == CompletionType.Folder)) && (completionText.EndsWith("\"") || completionText.EndsWith("'"))) { + // Insert a final "tab stop" as identified by $0 in the snippet provided for completion. + // For paths, we take the path returned by PowerShell e.g. 'C:\Program Files' and insert + // the tab stop marker before the closing quote char e.g. 'C:\Program Files$0'. + // This causes the editing cursor to be placed *before* the final quote after completion, + // which makes subsequent path completions work. See this part of the LSP spec for details: + // https://microsoft.github.io/language-server-protocol/specification#textDocument_completion int len = completionDetails.CompletionText.Length; completionText = completionDetails.CompletionText.Insert(len - 1, "$0"); insertTextFormat = InsertTextFormat.Snippet; From f2fae6e6dac40223e1f3ab3e442861e4d8171187 Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Wed, 3 Apr 2019 18:07:12 -0600 Subject: [PATCH 3/5] Address PR feedback --- .../LanguageServer/Completion.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/Completion.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Completion.cs index 4eef058a4..2e0cbeeca 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/Completion.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/Completion.cs @@ -75,11 +75,6 @@ public class TextEdit [DebuggerDisplay("Kind = {Kind.ToString()}, Label = {Label}, Detail = {Detail}")] public class CompletionItem { - public CompletionItem() - { - this.InsertTextFormat = InsertTextFormat.PlainText; - } - public string Label { get; set; } public CompletionItemKind? Kind { get; set; } @@ -97,7 +92,7 @@ public CompletionItem() public string InsertText { get; set; } - public InsertTextFormat InsertTextFormat { get; set; } + public InsertTextFormat InsertTextFormat { get; set; } = InsertTextFormat.PlainText; public Range Range { get; set; } From 137da73dbb4f48917acb6d24485b8d20a93bae92 Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Wed, 3 Apr 2019 22:26:56 -0600 Subject: [PATCH 4/5] Add tests for path completion, do not do snippet completion on files --- .../Server/LanguageServer.cs | 5 +- .../LanguageServerTests.cs | 113 ++++++++++++++++++ .../TestFiles/CompleteFunctionName.ps1 | 4 +- .../TestFiles/Folder With Spaces/.gitkeep | 0 4 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 test/PowerShellEditorServices.Test.Host/TestFiles/Folder With Spaces/.gitkeep diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index 21c407a18..37d791198 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -1958,12 +1958,11 @@ private static CompletionItem CreateCompletionItem( } } } - else if (((completionDetails.CompletionType == CompletionType.File) || - (completionDetails.CompletionType == CompletionType.Folder)) && + else if ((completionDetails.CompletionType == CompletionType.Folder) && (completionText.EndsWith("\"") || completionText.EndsWith("'"))) { // Insert a final "tab stop" as identified by $0 in the snippet provided for completion. - // For paths, we take the path returned by PowerShell e.g. 'C:\Program Files' and insert + // For folder paths, we take the path returned by PowerShell e.g. 'C:\Program Files' and insert // the tab stop marker before the closing quote char e.g. 'C:\Program Files$0'. // This causes the editing cursor to be placed *before* the final quote after completion, // which makes subsequent path completions work. See this part of the LSP spec for details: diff --git a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs index 95b5dbf65..1c69aa7ab 100644 --- a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs +++ b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs @@ -16,6 +16,7 @@ using System; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Threading.Tasks; using Xunit; @@ -257,6 +258,97 @@ await this.SendRequest( Assert.True(updatedCompletionItem.Documentation.Length > 0); } + [Fact] + public async Task CompletesDetailOnFilePathSuggestion() + { + string expectedPathSnippet; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + expectedPathSnippet = @".\TestFiles\CompleteFunctionName.ps1"; + } + else + { + expectedPathSnippet = "./TestFiles/CompleteFunctionName.ps1"; + } + + // Change dir to root of this test project's folder + await this.SetLocationForServerTest(this.TestRootDir); + + await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/CompleteFunctionName.ps1")); + + CompletionItem[] completions = + await this.SendRequest( + CompletionRequest.Type, + new TextDocumentPositionParams + { + TextDocument = new TextDocumentIdentifier + { + Uri = TestUtilities.NormalizePath("TestFiles/CompleteFunctionName.ps1") + }, + Position = new Position + { + Line = 8, + Character = 35 + } + }); + + CompletionItem completionItem = + completions + .FirstOrDefault( + c => c.InsertText == expectedPathSnippet); + + Assert.NotNull(completionItem); + Assert.Equal(InsertTextFormat.PlainText, completionItem.InsertTextFormat); + } + + [Fact] + public async Task CompletesDetailOnFolderPathSuggestion() + { + string expectedPathSnippet; + InsertTextFormat insertTextFormat; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + expectedPathSnippet = @"'.\TestFiles\Folder With Spaces$0'"; + insertTextFormat = InsertTextFormat.Snippet; + } + else + { + expectedPathSnippet = @"./TestFiles/Folder\ With\ Spaces"; + insertTextFormat = InsertTextFormat.PlainText; + } + + // Change dir to root of this test project's folder + await this.SetLocationForServerTest(this.TestRootDir); + + await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/CompleteFunctionName.ps1")); + + CompletionItem[] completions = + await this.SendRequest( + CompletionRequest.Type, + new TextDocumentPositionParams + { + TextDocument = new TextDocumentIdentifier + { + Uri = TestUtilities.NormalizePath("TestFiles/CompleteFunctionName.ps1") + }, + Position = new Position + { + Line = 7, + Character = 32 + } + }); + + CompletionItem completionItem = + completions + .FirstOrDefault( + c => c.InsertText == expectedPathSnippet); + + Assert.NotNull(completionItem); + Assert.Equal(insertTextFormat, completionItem.InsertTextFormat); + } + [Fact] public async Task FindsReferencesOfVariable() { @@ -826,6 +918,27 @@ await this.SendRequest( Assert.Equal(expectedArchitecture, versionDetails.Architecture); } + private string TestRootDir + { + get + { + string assemblyDir = Path.GetDirectoryName(this.GetType().Assembly.Location); + return Path.Combine(assemblyDir, @"..\..\.."); + } + } + + private async Task SetLocationForServerTest(string path) + { + // Change dir to root of this test project's folder + await this.SendRequest( + EvaluateRequest.Type, + new EvaluateRequestArguments + { + Expression = $"Set-Location {path}", + Context = "repl" + }); + } + private async Task SendOpenFileEvent(string filePath, bool waitForDiagnostics = true) { string fileContents = string.Join(Environment.NewLine, File.ReadAllLines(filePath)); diff --git a/test/PowerShellEditorServices.Test.Host/TestFiles/CompleteFunctionName.ps1 b/test/PowerShellEditorServices.Test.Host/TestFiles/CompleteFunctionName.ps1 index a049b2845..d7f9b9aab 100644 --- a/test/PowerShellEditorServices.Test.Host/TestFiles/CompleteFunctionName.ps1 +++ b/test/PowerShellEditorServices.Test.Host/TestFiles/CompleteFunctionName.ps1 @@ -4,4 +4,6 @@ function My-Function $Cons My- Get-Proc -$HKC \ No newline at end of file +$HKC +Get-ChildItem ./TestFiles/Folder +Get-ChildItem ./TestFiles/CompleteF diff --git a/test/PowerShellEditorServices.Test.Host/TestFiles/Folder With Spaces/.gitkeep b/test/PowerShellEditorServices.Test.Host/TestFiles/Folder With Spaces/.gitkeep new file mode 100644 index 000000000..e69de29bb From 20516b0b43eb6ddc0abe480bdfdfc00e965f5d16 Mon Sep 17 00:00:00 2001 From: Keith Hill Date: Sun, 7 Apr 2019 16:41:23 -0600 Subject: [PATCH 5/5] Fix incorrect macOS/Linxu path in completion test --- .../PowerShellEditorServices.Test.Host/LanguageServerTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs index 1c69aa7ab..296d5fd42 100644 --- a/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs +++ b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs @@ -315,8 +315,8 @@ public async Task CompletesDetailOnFolderPathSuggestion() } else { - expectedPathSnippet = @"./TestFiles/Folder\ With\ Spaces"; - insertTextFormat = InsertTextFormat.PlainText; + expectedPathSnippet = @"'./TestFiles/Folder With Spaces$0'"; + insertTextFormat = InsertTextFormat.Snippet; } // Change dir to root of this test project's folder