Skip to content

Commit fda3a76

Browse files
committed
Convert CreateCompletionItem to use switch expression
Makes it a thousand times more readable, and therefore less error-prone. Also verified fields against the spec and so finished the TODOs.
1 parent e7fc1b5 commit fda3a76

File tree

8 files changed

+106
-154
lines changed

8 files changed

+106
-154
lines changed

src/PowerShellEditorServices/Services/TextDocument/Handlers/CompletionHandler.cs

+105-138
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ internal class PsesCompletionHandler : ICompletionHandler, ICompletionResolveHan
3232
private readonly WorkspaceService _workspaceService;
3333
private CompletionCapability _capability;
3434
private readonly Guid _id = Guid.NewGuid();
35-
private static readonly Regex _typeRegex = new(@"^(\[.+\])");
3635

3736
Guid ICanBeIdentifiedHandler.Id => _id;
3837

@@ -168,163 +167,131 @@ public async Task<IEnumerable<CompletionItem>> GetCompletionsInFileAsync(
168167
}
169168

170169
internal static CompletionItem CreateCompletionItem(
171-
CompletionResult completion,
170+
CompletionResult result,
172171
BufferRange completionRange,
173172
int sortIndex)
174173
{
175-
Validate.IsNotNull(nameof(completion), completion);
174+
Validate.IsNotNull(nameof(result), result);
176175

177-
// Some tooltips may have newlines or whitespace for unknown reasons.
178-
string toolTipText = completion.ToolTip?.Trim();
176+
TextEdit textEdit = new()
177+
{
178+
NewText = result.CompletionText,
179+
Range = new Range
180+
{
181+
Start = new Position
182+
{
183+
Line = completionRange.Start.Line - 1,
184+
Character = completionRange.Start.Column - 1
185+
},
186+
End = new Position
187+
{
188+
Line = completionRange.End.Line - 1,
189+
Character = completionRange.End.Column - 1
190+
}
191+
}
192+
};
179193

180-
string completionText = completion.CompletionText;
181-
InsertTextFormat insertTextFormat = InsertTextFormat.PlainText;
182-
CompletionItemKind kind;
194+
// Some tooltips may have newlines or whitespace for unknown reasons.
195+
string detail = result.ToolTip?.Trim();
183196

184-
// Force the client to maintain the sort order in which the original completion results
185-
// were returned. We just need to make sure the default order also be the
186-
// lexicographical order which we do by prefixing the ListItemText with a leading 0's
187-
// four digit index.
188-
string sortText = $"{sortIndex:D4}{completion.ListItemText}";
197+
CompletionItem item = new()
198+
{
199+
Label = result.ListItemText,
200+
Detail = result.ListItemText.Equals(detail, StringComparison.CurrentCulture)
201+
? string.Empty : detail, // Don't repeat label.
202+
// Retain PowerShell's sort order with the given index.
203+
SortText = $"{sortIndex:D4}{result.ListItemText}",
204+
FilterText = result.CompletionText,
205+
TextEdit = textEdit // Used instead of InsertText.
206+
};
189207

190-
switch (completion.ResultType)
208+
return result.ResultType switch
191209
{
192-
case CompletionResultType.Command:
193-
kind = CompletionItemKind.Function;
194-
break;
195-
case CompletionResultType.History:
196-
kind = CompletionItemKind.Reference;
197-
break;
198-
case CompletionResultType.Keyword:
199-
case CompletionResultType.DynamicKeyword:
200-
kind = CompletionItemKind.Keyword;
201-
break;
202-
case CompletionResultType.Method:
203-
kind = CompletionItemKind.Method;
204-
break;
205-
case CompletionResultType.Namespace:
206-
kind = CompletionItemKind.Module;
207-
break;
208-
case CompletionResultType.ParameterName:
209-
kind = CompletionItemKind.Variable;
210-
// Look for type encoded in the tooltip for parameters and variables.
211-
// Display PowerShell type names in [] to be consistent with PowerShell syntax
212-
// and how the debugger displays type names.
213-
MatchCollection matches = _typeRegex.Matches(toolTipText);
214-
if ((matches.Count > 0) && (matches[0].Groups.Count > 1))
210+
CompletionResultType.Text => item with { Kind = CompletionItemKind.Text },
211+
CompletionResultType.History => item with { Kind = CompletionItemKind.Reference },
212+
CompletionResultType.Command => item with { Kind = CompletionItemKind.Function },
213+
CompletionResultType.ProviderItem => item with { Kind = CompletionItemKind.File },
214+
CompletionResultType.ProviderContainer => IsSnippet(result.CompletionText, out string snippet)
215+
? item with
215216
{
216-
toolTipText = matches[0].Groups[1].Value;
217+
Kind = CompletionItemKind.Folder,
218+
InsertTextFormat = InsertTextFormat.Snippet,
219+
TextEdit = textEdit with { NewText = snippet }
217220
}
221+
: item with { Kind = CompletionItemKind.Folder },
222+
CompletionResultType.Property => item with { Kind = CompletionItemKind.Property },
223+
CompletionResultType.Method => item with { Kind = CompletionItemKind.Method },
224+
CompletionResultType.ParameterName => ExtractTypeFromToolTip(detail, out string type)
225+
? item with { Kind = CompletionItemKind.Variable, Detail = type }
218226
// The comparison operators (-eq, -not, -gt, etc) unfortunately come across as
219227
// ParameterName types but they don't have a type associated to them, so we can
220-
// deduce its an operator.
221-
else
222-
{
223-
kind = CompletionItemKind.Operator;
224-
}
225-
break;
226-
case CompletionResultType.ParameterValue:
227-
kind = CompletionItemKind.Value;
228-
break;
229-
case CompletionResultType.Property:
230-
kind = CompletionItemKind.Property;
231-
break;
232-
case CompletionResultType.ProviderContainer:
233-
kind = CompletionItemKind.Folder;
234-
// Insert a final "tab stop" as identified by $0 in the snippet provided for
235-
// completion. For folder paths, we take the path returned by PowerShell e.g.
236-
// 'C:\Program Files' and insert the tab stop marker before the closing quote
237-
// char e.g. 'C:\Program Files$0'. This causes the editing cursor to be placed
238-
// *before* the final quote after completion, which makes subsequent path
239-
// completions work. See this part of the LSP spec for details:
240-
// https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
241-
242-
// Since we want to use a "tab stop" we need to escape a few things for Textmate
243-
// to render properly.
244-
if (EndsWithQuote(completionText))
245-
{
246-
StringBuilder sb = new StringBuilder(completionText)
247-
.Replace(@"\", @"\\")
248-
.Replace(@"}", @"\}")
249-
.Replace(@"$", @"\$");
250-
completionText = sb.Insert(sb.Length - 1, "$0").ToString();
251-
insertTextFormat = InsertTextFormat.Snippet;
252-
}
253-
break;
254-
case CompletionResultType.ProviderItem:
255-
kind = CompletionItemKind.File;
256-
break;
257-
case CompletionResultType.Text:
258-
kind = CompletionItemKind.Text;
259-
break;
260-
case CompletionResultType.Type:
261-
kind = CompletionItemKind.TypeParameter;
228+
// deduce it is an operator.
229+
: item with { Kind = CompletionItemKind.Operator },
230+
CompletionResultType.ParameterValue => item with { Kind = CompletionItemKind.Value },
231+
CompletionResultType.Variable => ExtractTypeFromToolTip(detail, out string type)
232+
? item with { Kind = CompletionItemKind.Variable, Detail = type }
233+
: item with { Kind = CompletionItemKind.Variable },
234+
CompletionResultType.Namespace => item with { Kind = CompletionItemKind.Module },
235+
CompletionResultType.Type => detail.StartsWith("Class ", StringComparison.CurrentCulture)
262236
// Custom classes come through as types but the PowerShell completion tooltip
263237
// will start with "Class ", so we can more accurately display its icon.
264-
if (toolTipText.StartsWith("Class ", StringComparison.Ordinal))
265-
{
266-
kind = CompletionItemKind.Class;
267-
}
268-
break;
269-
case CompletionResultType.Variable:
270-
kind = CompletionItemKind.Variable;
271-
// Look for type encoded in the tooltip for parameters and variables.
272-
// Display PowerShell type names in [] to be consistent with PowerShell syntax
273-
// and how the debugger displays type names.
274-
matches = _typeRegex.Matches(toolTipText);
275-
if ((matches.Count > 0) && (matches[0].Groups.Count > 1))
276-
{
277-
toolTipText = matches[0].Groups[1].Value;
278-
}
279-
break;
280-
default:
281-
throw new ArgumentOutOfRangeException(nameof(completion));
282-
}
238+
? item with { Kind = CompletionItemKind.Class }
239+
: item with { Kind = CompletionItemKind.TypeParameter },
240+
CompletionResultType.Keyword or CompletionResultType.DynamicKeyword =>
241+
item with { Kind = CompletionItemKind.Keyword },
242+
_ => throw new ArgumentOutOfRangeException(nameof(result))
243+
};
244+
}
283245

284-
// Don't display tooltip if it is the same as the ListItemText.
285-
if (completion.ListItemText.Equals(toolTipText, StringComparison.OrdinalIgnoreCase))
246+
/// <summary>
247+
/// Look for type encoded in the tooltip for parameters and variables. Display PowerShell
248+
/// type names in [] to be consistent with PowerShell syntax and how the debugger displays
249+
/// type names.
250+
/// </summary>
251+
/// <param name="toolTipText"></param>
252+
/// <param name="type"></param>
253+
/// <returns>Whether or not the type was found.</returns>
254+
private static bool ExtractTypeFromToolTip(string toolTipText, out string type)
255+
{
256+
Regex _typeRegex = new(@"^(\[.+\])");
257+
MatchCollection matches = _typeRegex.Matches(toolTipText);
258+
type = string.Empty;
259+
if ((matches.Count > 0) && (matches[0].Groups.Count > 1))
286260
{
287-
toolTipText = string.Empty;
261+
type = matches[0].Groups[1].Value;
262+
return true;
288263
}
289-
290-
Validate.IsNotNull(nameof(CompletionItemKind), kind);
291-
292-
// TODO: We used to extract the symbol type from the tooltip using a regex, but it
293-
// wasn't actually used.
294-
return new CompletionItem
295-
{
296-
Kind = kind,
297-
TextEdit = new TextEdit
298-
{
299-
NewText = completionText,
300-
Range = new Range
301-
{
302-
Start = new Position
303-
{
304-
Line = completionRange.Start.Line - 1,
305-
Character = completionRange.Start.Column - 1
306-
},
307-
End = new Position
308-
{
309-
Line = completionRange.End.Line - 1,
310-
Character = completionRange.End.Column - 1
311-
}
312-
}
313-
},
314-
InsertTextFormat = insertTextFormat,
315-
InsertText = completionText,
316-
FilterText = completion.CompletionText,
317-
SortText = sortText,
318-
// TODO: Documentation
319-
Detail = toolTipText,
320-
Label = completion.ListItemText,
321-
// TODO: Command
322-
};
264+
return false;
323265
}
324266

325-
private static bool EndsWithQuote(string text)
267+
/// <summary>
268+
/// Insert a final "tab stop" as identified by $0 in the snippet provided for completion.
269+
/// For folder paths, we take the path returned by PowerShell e.g. 'C:\Program Files' and
270+
/// insert the tab stop marker before the closing quote char e.g. 'C:\Program Files$0'. This
271+
/// causes the editing cursor to be placed *before* the final quote after completion, which
272+
/// makes subsequent path completions work. See this part of the LSP spec for details:
273+
/// https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
274+
/// </summary>
275+
/// <param name="completionText"></param>
276+
/// <param name="snippet"></param>
277+
/// <returns>
278+
/// Whether or not the completion ended with a quote and so was a snippet.
279+
/// </returns>
280+
private static bool IsSnippet(string completionText, out string snippet)
326281
{
327-
return !string.IsNullOrEmpty(text) && text[text.Length - 1] is '"' or '\'';
282+
snippet = string.Empty;
283+
if (!string.IsNullOrEmpty(completionText)
284+
&& completionText[completionText.Length - 1] is '"' or '\'')
285+
{
286+
// Since we want to use a "tab stop" we need to escape a few things.
287+
StringBuilder sb = new StringBuilder(completionText)
288+
.Replace(@"\", @"\\")
289+
.Replace(@"}", @"\}")
290+
.Replace(@"$", @"\$");
291+
snippet = sb.Insert(sb.Length - 1, "$0").ToString();
292+
return true;
293+
}
294+
return false;
328295
}
329296
}
330297
}

test/PowerShellEditorServices.Test.Shared/Completion/CompleteAttributeValue.cs

-6
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ internal static class CompleteAttributeValue
2222
{
2323
Kind = CompletionItemKind.Property,
2424
Detail = "System.Boolean ValueFromPipeline",
25-
InsertTextFormat = InsertTextFormat.PlainText,
26-
InsertText = "ValueFromPipeline",
2725
FilterText = "ValueFromPipeline",
2826
Label = "ValueFromPipeline",
2927
SortText = "0001ValueFromPipeline",
@@ -42,8 +40,6 @@ internal static class CompleteAttributeValue
4240
{
4341
Kind = CompletionItemKind.Property,
4442
Detail = "System.Boolean ValueFromPipelineByPropertyName",
45-
InsertTextFormat = InsertTextFormat.PlainText,
46-
InsertText = "ValueFromPipelineByPropertyName",
4743
FilterText = "ValueFromPipelineByPropertyName",
4844
Label = "ValueFromPipelineByPropertyName",
4945
SortText = "0002ValueFromPipelineByPropertyName",
@@ -62,8 +58,6 @@ internal static class CompleteAttributeValue
6258
{
6359
Kind = CompletionItemKind.Property,
6460
Detail = "System.Boolean ValueFromRemainingArguments",
65-
InsertTextFormat = InsertTextFormat.PlainText,
66-
InsertText = "ValueFromRemainingArguments",
6761
FilterText = "ValueFromRemainingArguments",
6862
Label = "ValueFromRemainingArguments",
6963
SortText = "0003ValueFromRemainingArguments",

test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandFromModule.cs

-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ internal static class CompleteCommandFromModule
2626
{
2727
Kind = CompletionItemKind.Function,
2828
Detail = "", // OS-dependent, checked separately.
29-
InsertTextFormat = InsertTextFormat.PlainText,
30-
InsertText = "Get-Random",
3129
FilterText = "Get-Random",
3230
Label = "Get-Random",
3331
SortText = "0001Get-Random",

test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandInFile.cs

-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ internal static class CompleteCommandInFile
2222
{
2323
Kind = CompletionItemKind.Function,
2424
Detail = "",
25-
InsertTextFormat = InsertTextFormat.PlainText,
26-
InsertText = "Get-Something",
2725
FilterText = "Get-Something",
2826
Label = "Get-Something",
2927
SortText = "0001Get-Something",

test/PowerShellEditorServices.Test.Shared/Completion/CompleteNamespace.cs

-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ internal static class CompleteNamespace
2222
{
2323
Kind = CompletionItemKind.Module,
2424
Detail = "Namespace System.Collections",
25-
InsertTextFormat = InsertTextFormat.PlainText,
26-
InsertText = "System.Collections",
2725
FilterText = "System.Collections",
2826
Label = "Collections",
2927
SortText = "0001Collections",

test/PowerShellEditorServices.Test.Shared/Completion/CompleteTypeName.cs

-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ internal static class CompleteTypeName
2222
{
2323
Kind = CompletionItemKind.TypeParameter,
2424
Detail = "System.Collections.ArrayList",
25-
InsertTextFormat = InsertTextFormat.PlainText,
26-
InsertText = "System.Collections.ArrayList",
2725
FilterText = "System.Collections.ArrayList",
2826
Label = "ArrayList",
2927
SortText = "0001ArrayList",

test/PowerShellEditorServices.Test.Shared/Completion/CompleteVariableInFile.cs

-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ internal static class CompleteVariableInFile
2222
{
2323
Kind = CompletionItemKind.Variable,
2424
Detail = "", // Same as label, so not shown.
25-
InsertTextFormat = InsertTextFormat.PlainText,
26-
InsertText = "$testVar1",
2725
FilterText = "$testVar1",
2826
Label = "testVar1",
2927
SortText = "0001testVar1",

test/PowerShellEditorServices.Test/Language/CompletionHandlerTests.cs

+1
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ public async Task CompletesFilePath()
115115
IEnumerable<CompletionItem> results = await GetCompletionResultsAsync(CompleteFilePath.SourceDetails).ConfigureAwait(true);
116116
Assert.NotEmpty(results);
117117
CompletionItem actual = results.First();
118+
// Paths are system dependent so we ignore the text and just check the type and range.
118119
Assert.Equal(actual.TextEdit.TextEdit with { NewText = "" }, CompleteFilePath.ExpectedEdit);
119120
Assert.All(results, r => Assert.True(r.Kind is CompletionItemKind.File or CompletionItemKind.Folder));
120121
}

0 commit comments

Comments
 (0)