Skip to content

Commit bfb4e8e

Browse files
authored
Improve path auto-completion (#902)
* WIP: Improve path auto-completion * Add comment to address PR feedback * Address PR feedback * Add tests for path completion, do not do snippet completion on files * Fix incorrect macOS/Linxu path in completion test
1 parent 199cf55 commit bfb4e8e

File tree

5 files changed

+142
-3
lines changed

5 files changed

+142
-3
lines changed

src/PowerShellEditorServices.Protocol/LanguageServer/Completion.cs

+8
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ public enum CompletionItemKind
5858
Folder = 19
5959
}
6060

61+
public enum InsertTextFormat
62+
{
63+
PlainText = 1,
64+
Snippet = 2,
65+
}
66+
6167
[DebuggerDisplay("NewText = {NewText}, Range = {Range.Start.Line}:{Range.Start.Character} - {Range.End.Line}:{Range.End.Character}")]
6268
public class TextEdit
6369
{
@@ -86,6 +92,8 @@ public class CompletionItem
8692

8793
public string InsertText { get; set; }
8894

95+
public InsertTextFormat InsertTextFormat { get; set; } = InsertTextFormat.PlainText;
96+
8997
public Range Range { get; set; }
9098

9199
public string[] CommitCharacters { get; set; }

src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs

+18-2
Original file line numberDiff line numberDiff line change
@@ -1915,6 +1915,8 @@ private static CompletionItem CreateCompletionItem(
19151915
{
19161916
string detailString = null;
19171917
string documentationString = null;
1918+
string completionText = completionDetails.CompletionText;
1919+
InsertTextFormat insertTextFormat = InsertTextFormat.PlainText;
19181920

19191921
if ((completionDetails.CompletionType == CompletionType.Variable) ||
19201922
(completionDetails.CompletionType == CompletionType.ParameterName))
@@ -1956,6 +1958,19 @@ private static CompletionItem CreateCompletionItem(
19561958
}
19571959
}
19581960
}
1961+
else if ((completionDetails.CompletionType == CompletionType.Folder) &&
1962+
(completionText.EndsWith("\"") || completionText.EndsWith("'")))
1963+
{
1964+
// Insert a final "tab stop" as identified by $0 in the snippet provided for completion.
1965+
// For folder paths, we take the path returned by PowerShell e.g. 'C:\Program Files' and insert
1966+
// the tab stop marker before the closing quote char e.g. 'C:\Program Files$0'.
1967+
// This causes the editing cursor to be placed *before* the final quote after completion,
1968+
// which makes subsequent path completions work. See this part of the LSP spec for details:
1969+
// https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
1970+
int len = completionDetails.CompletionText.Length;
1971+
completionText = completionDetails.CompletionText.Insert(len - 1, "$0");
1972+
insertTextFormat = InsertTextFormat.Snippet;
1973+
}
19591974

19601975
// Force the client to maintain the sort order in which the
19611976
// original completion results were returned. We just need to
@@ -1966,7 +1981,8 @@ private static CompletionItem CreateCompletionItem(
19661981

19671982
return new CompletionItem
19681983
{
1969-
InsertText = completionDetails.CompletionText,
1984+
InsertText = completionText,
1985+
InsertTextFormat = insertTextFormat,
19701986
Label = completionDetails.ListItemText,
19711987
Kind = MapCompletionKind(completionDetails.CompletionType),
19721988
Detail = detailString,
@@ -1975,7 +1991,7 @@ private static CompletionItem CreateCompletionItem(
19751991
FilterText = completionDetails.CompletionText,
19761992
TextEdit = new TextEdit
19771993
{
1978-
NewText = completionDetails.CompletionText,
1994+
NewText = completionText,
19791995
Range = new Range
19801996
{
19811997
Start = new Position

test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs

+113
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using System;
1717
using System.IO;
1818
using System.Linq;
19+
using System.Runtime.InteropServices;
1920
using System.Threading.Tasks;
2021
using Xunit;
2122

@@ -257,6 +258,97 @@ await this.SendRequest(
257258
Assert.True(updatedCompletionItem.Documentation.Length > 0);
258259
}
259260

261+
[Fact]
262+
public async Task CompletesDetailOnFilePathSuggestion()
263+
{
264+
string expectedPathSnippet;
265+
266+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
267+
{
268+
expectedPathSnippet = @".\TestFiles\CompleteFunctionName.ps1";
269+
}
270+
else
271+
{
272+
expectedPathSnippet = "./TestFiles/CompleteFunctionName.ps1";
273+
}
274+
275+
// Change dir to root of this test project's folder
276+
await this.SetLocationForServerTest(this.TestRootDir);
277+
278+
await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/CompleteFunctionName.ps1"));
279+
280+
CompletionItem[] completions =
281+
await this.SendRequest(
282+
CompletionRequest.Type,
283+
new TextDocumentPositionParams
284+
{
285+
TextDocument = new TextDocumentIdentifier
286+
{
287+
Uri = TestUtilities.NormalizePath("TestFiles/CompleteFunctionName.ps1")
288+
},
289+
Position = new Position
290+
{
291+
Line = 8,
292+
Character = 35
293+
}
294+
});
295+
296+
CompletionItem completionItem =
297+
completions
298+
.FirstOrDefault(
299+
c => c.InsertText == expectedPathSnippet);
300+
301+
Assert.NotNull(completionItem);
302+
Assert.Equal(InsertTextFormat.PlainText, completionItem.InsertTextFormat);
303+
}
304+
305+
[Fact]
306+
public async Task CompletesDetailOnFolderPathSuggestion()
307+
{
308+
string expectedPathSnippet;
309+
InsertTextFormat insertTextFormat;
310+
311+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
312+
{
313+
expectedPathSnippet = @"'.\TestFiles\Folder With Spaces$0'";
314+
insertTextFormat = InsertTextFormat.Snippet;
315+
}
316+
else
317+
{
318+
expectedPathSnippet = @"'./TestFiles/Folder With Spaces$0'";
319+
insertTextFormat = InsertTextFormat.Snippet;
320+
}
321+
322+
// Change dir to root of this test project's folder
323+
await this.SetLocationForServerTest(this.TestRootDir);
324+
325+
await this.SendOpenFileEvent(TestUtilities.NormalizePath("TestFiles/CompleteFunctionName.ps1"));
326+
327+
CompletionItem[] completions =
328+
await this.SendRequest(
329+
CompletionRequest.Type,
330+
new TextDocumentPositionParams
331+
{
332+
TextDocument = new TextDocumentIdentifier
333+
{
334+
Uri = TestUtilities.NormalizePath("TestFiles/CompleteFunctionName.ps1")
335+
},
336+
Position = new Position
337+
{
338+
Line = 7,
339+
Character = 32
340+
}
341+
});
342+
343+
CompletionItem completionItem =
344+
completions
345+
.FirstOrDefault(
346+
c => c.InsertText == expectedPathSnippet);
347+
348+
Assert.NotNull(completionItem);
349+
Assert.Equal(insertTextFormat, completionItem.InsertTextFormat);
350+
}
351+
260352
[Fact]
261353
public async Task FindsReferencesOfVariable()
262354
{
@@ -826,6 +918,27 @@ await this.SendRequest(
826918
Assert.Equal(expectedArchitecture, versionDetails.Architecture);
827919
}
828920

921+
private string TestRootDir
922+
{
923+
get
924+
{
925+
string assemblyDir = Path.GetDirectoryName(this.GetType().Assembly.Location);
926+
return Path.Combine(assemblyDir, @"..\..\..");
927+
}
928+
}
929+
930+
private async Task SetLocationForServerTest(string path)
931+
{
932+
// Change dir to root of this test project's folder
933+
await this.SendRequest(
934+
EvaluateRequest.Type,
935+
new EvaluateRequestArguments
936+
{
937+
Expression = $"Set-Location {path}",
938+
Context = "repl"
939+
});
940+
}
941+
829942
private async Task SendOpenFileEvent(string filePath, bool waitForDiagnostics = true)
830943
{
831944
string fileContents = string.Join(Environment.NewLine, File.ReadAllLines(filePath));

test/PowerShellEditorServices.Test.Host/TestFiles/CompleteFunctionName.ps1

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ function My-Function
44
$Cons
55
My-
66
Get-Proc
7-
$HKC
7+
$HKC
8+
Get-ChildItem ./TestFiles/Folder
9+
Get-ChildItem ./TestFiles/CompleteF

test/PowerShellEditorServices.Test.Host/TestFiles/Folder With Spaces/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)