Skip to content

Commit 69a1d0d

Browse files
authored
Improve path auto-completion (#902) (#916)
* 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 641b018 commit 69a1d0d

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
@@ -1922,6 +1922,8 @@ private static CompletionItem CreateCompletionItem(
19221922
{
19231923
string detailString = null;
19241924
string documentationString = null;
1925+
string completionText = completionDetails.CompletionText;
1926+
InsertTextFormat insertTextFormat = InsertTextFormat.PlainText;
19251927

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

19671982
// Force the client to maintain the sort order in which the
19681983
// original completion results were returned. We just need to
@@ -1973,7 +1988,8 @@ private static CompletionItem CreateCompletionItem(
19731988

19741989
return new CompletionItem
19751990
{
1976-
InsertText = completionDetails.CompletionText,
1991+
InsertText = completionText,
1992+
InsertTextFormat = insertTextFormat,
19771993
Label = completionDetails.ListItemText,
19781994
Kind = MapCompletionKind(completionDetails.CompletionType),
19791995
Detail = detailString,
@@ -1982,7 +1998,7 @@ private static CompletionItem CreateCompletionItem(
19821998
FilterText = completionDetails.CompletionText,
19831999
TextEdit = new TextEdit
19842000
{
1985-
NewText = completionDetails.CompletionText,
2001+
NewText = completionText,
19862002
Range = new Range
19872003
{
19882004
Start = new Position

test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs

+113
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using System;
1616
using System.IO;
1717
using System.Linq;
18+
using System.Runtime.InteropServices;
1819
using System.Threading.Tasks;
1920
using Xunit;
2021

@@ -260,6 +261,97 @@ await this.SendRequest(
260261
Assert.True(updatedCompletionItem.Documentation.Length > 0);
261262
}
262263

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

924+
private string TestRootDir
925+
{
926+
get
927+
{
928+
string assemblyDir = Path.GetDirectoryName(this.GetType().Assembly.Location);
929+
return Path.Combine(assemblyDir, @"..\..\..");
930+
}
931+
}
932+
933+
private async Task SetLocationForServerTest(string path)
934+
{
935+
// Change dir to root of this test project's folder
936+
await this.SendRequest(
937+
EvaluateRequest.Type,
938+
new EvaluateRequestArguments
939+
{
940+
Expression = $"Set-Location {path}",
941+
Context = "repl"
942+
});
943+
}
944+
832945
private async Task SendOpenFileEvent(string filePath, bool waitForDiagnostics = true)
833946
{
834947
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)