Skip to content

Commit 59be311

Browse files
committed
Improve completion logic
This makes the deduced `CompletionItemKind` enum more accurate, which results in better icons for the user when viewing completions in an editor. Also cleaned up the file as there were remnants of dead code.
1 parent cad2aa7 commit 59be311

File tree

2 files changed

+98
-140
lines changed

2 files changed

+98
-140
lines changed

src/PowerShellEditorServices/Services/TextDocument/CompletionResults.cs

+27-40
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,17 @@ internal enum CompletionType
147147
/// <summary>
148148
/// Identifies a completion for a provider path (like a file system path) to a container.
149149
/// </summary>
150-
Folder
150+
Folder,
151+
152+
/// <summary>
153+
/// Identifies a completion for history.
154+
/// </summary>
155+
History,
156+
157+
/// <summary>
158+
/// Identifies a completion for just text.
159+
/// </summary>
160+
Text
151161
}
152162

153163
/// <summary>
@@ -277,46 +287,23 @@ public override int GetHashCode()
277287
private static CompletionType ConvertCompletionResultType(
278288
CompletionResultType completionResultType)
279289
{
280-
switch (completionResultType)
290+
return completionResultType switch
281291
{
282-
case CompletionResultType.Command:
283-
return CompletionType.Command;
284-
285-
case CompletionResultType.Method:
286-
return CompletionType.Method;
287-
288-
case CompletionResultType.ParameterName:
289-
return CompletionType.ParameterName;
290-
291-
case CompletionResultType.ParameterValue:
292-
return CompletionType.ParameterValue;
293-
294-
case CompletionResultType.Property:
295-
return CompletionType.Property;
296-
297-
case CompletionResultType.Variable:
298-
return CompletionType.Variable;
299-
300-
case CompletionResultType.Namespace:
301-
return CompletionType.Namespace;
302-
303-
case CompletionResultType.Type:
304-
return CompletionType.Type;
305-
306-
case CompletionResultType.Keyword:
307-
case CompletionResultType.DynamicKeyword:
308-
return CompletionType.Keyword;
309-
310-
case CompletionResultType.ProviderContainer:
311-
return CompletionType.Folder;
312-
313-
case CompletionResultType.ProviderItem:
314-
return CompletionType.File;
315-
316-
default:
317-
// TODO: Trace the unsupported CompletionResultType
318-
return CompletionType.Unknown;
319-
}
292+
CompletionResultType.Command => CompletionType.Command,
293+
CompletionResultType.Method => CompletionType.Method,
294+
CompletionResultType.ParameterName => CompletionType.ParameterName,
295+
CompletionResultType.ParameterValue => CompletionType.ParameterValue,
296+
CompletionResultType.Property => CompletionType.Property,
297+
CompletionResultType.Variable => CompletionType.Variable,
298+
CompletionResultType.Namespace => CompletionType.Namespace,
299+
CompletionResultType.Type => CompletionType.Type,
300+
CompletionResultType.Keyword or CompletionResultType.DynamicKeyword => CompletionType.Keyword,
301+
CompletionResultType.ProviderContainer => CompletionType.Folder,
302+
CompletionResultType.ProviderItem => CompletionType.File,
303+
CompletionResultType.History => CompletionType.History,
304+
CompletionResultType.Text => CompletionType.Text,
305+
_ => CompletionType.Unknown,
306+
};
320307
}
321308

322309
private static string ExtractSymbolTypeNameFromToolTip(string toolTipText)

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

+71-100
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,15 @@ namespace Microsoft.PowerShell.EditorServices.Handlers
2424
// TODO: Use ABCs.
2525
internal class PsesCompletionHandler : ICompletionHandler, ICompletionResolveHandler
2626
{
27-
const int DefaultWaitTimeoutMilliseconds = 5000;
27+
private const int DefaultWaitTimeoutMilliseconds = 5000;
2828
private readonly ILogger _logger;
2929
private readonly IRunspaceContext _runspaceContext;
3030
private readonly IInternalPowerShellExecutionService _executionService;
3131
private readonly WorkspaceService _workspaceService;
32-
private CompletionResults _mostRecentCompletions;
33-
private int _mostRecentRequestLine;
34-
private int _mostRecentRequestOffest;
35-
private string _mostRecentRequestFile;
3632
private CompletionCapability _capability;
3733
private readonly Guid _id = Guid.NewGuid();
34+
private static readonly Regex _regex = new(@"^(\[.+\])");
35+
3836
Guid ICanBeIdentifiedHandler.Id => _id;
3937

4038
public PsesCompletionHandler(
@@ -49,7 +47,7 @@ public PsesCompletionHandler(
4947
_workspaceService = workspaceService;
5048
}
5149

52-
public CompletionRegistrationOptions GetRegistrationOptions(CompletionCapability capability, ClientCapabilities clientCapabilities) => new CompletionRegistrationOptions
50+
public CompletionRegistrationOptions GetRegistrationOptions(CompletionCapability capability, ClientCapabilities clientCapabilities) => new()
5351
{
5452
DocumentSelector = LspUtils.PowerShellDocumentSelector,
5553
ResolveProvider = true,
@@ -69,13 +67,9 @@ public async Task<CompletionList> Handle(CompletionParams request, CancellationT
6967
return Array.Empty<CompletionItem>();
7068
}
7169

72-
CompletionResults completionResults =
73-
await GetCompletionsInFileAsync(
74-
scriptFile,
75-
cursorLine,
76-
cursorColumn).ConfigureAwait(false);
70+
CompletionResults completionResults = await GetCompletionsInFileAsync(scriptFile, cursorLine, cursorColumn).ConfigureAwait(false);
7771

78-
if (completionResults == null)
72+
if (completionResults is null)
7973
{
8074
return Array.Empty<CompletionItem>();
8175
}
@@ -110,13 +104,9 @@ public async Task<CompletionItem> Handle(CompletionItem request, CancellationTok
110104
}
111105

112106
// Get the documentation for the function
113-
CommandInfo commandInfo =
114-
await CommandHelpers.GetCommandInfoAsync(
115-
request.Label,
116-
_runspaceContext.CurrentRunspace,
117-
_executionService).ConfigureAwait(false);
107+
CommandInfo commandInfo = await CommandHelpers.GetCommandInfoAsync(request.Label, _runspaceContext.CurrentRunspace, _executionService).ConfigureAwait(false);
118108

119-
if (commandInfo != null)
109+
if (commandInfo is not null)
120110
{
121111
request = request with
122112
{
@@ -158,13 +148,10 @@ public async Task<CompletionResults> GetCompletionsInFileAsync(
158148

159149
// Get the offset at the specified position. This method
160150
// will also validate the given position.
161-
int fileOffset =
162-
scriptFile.GetOffsetAtPosition(
163-
lineNumber,
164-
columnNumber);
151+
int fileOffset = scriptFile.GetOffsetAtPosition(lineNumber, columnNumber);
165152

166153
CommandCompletion commandCompletion = null;
167-
using (var cts = new CancellationTokenSource(DefaultWaitTimeoutMilliseconds))
154+
using (CancellationTokenSource cts = new(DefaultWaitTimeoutMilliseconds))
168155
{
169156
commandCompletion =
170157
await AstOperations.GetCompletionsAsync(
@@ -176,33 +163,20 @@ await AstOperations.GetCompletionsAsync(
176163
cts.Token).ConfigureAwait(false);
177164
}
178165

179-
if (commandCompletion == null)
166+
if (commandCompletion is null)
180167
{
181168
return new CompletionResults();
182169
}
183170

184171
try
185172
{
186-
CompletionResults completionResults =
187-
CompletionResults.Create(
188-
scriptFile,
189-
commandCompletion);
190-
191-
// save state of most recent completion
192-
_mostRecentCompletions = completionResults;
193-
_mostRecentRequestFile = scriptFile.Id;
194-
_mostRecentRequestLine = lineNumber;
195-
_mostRecentRequestOffest = columnNumber;
196-
197-
return completionResults;
173+
return CompletionResults.Create(scriptFile, commandCompletion);
198174
}
199175
catch (ArgumentException e)
200176
{
201177
// Bad completion results could return an invalid
202178
// replacement range, catch that here
203-
_logger.LogError(
204-
$"Caught exception while trying to create CompletionResults:\n\n{e.ToString()}");
205-
179+
_logger.LogError($"Caught exception while trying to create CompletionResults:\n\n{e.ToString()}");
206180
return new CompletionResults();
207181
}
208182
}
@@ -212,57 +186,65 @@ private static CompletionItem CreateCompletionItem(
212186
BufferRange completionRange,
213187
int sortIndex)
214188
{
215-
string detailString = null;
216-
string documentationString = null;
189+
string toolTipText = completionDetails.ToolTipText;
217190
string completionText = completionDetails.CompletionText;
191+
CompletionItemKind kind = MapCompletionKind(completionDetails.CompletionType);
218192
InsertTextFormat insertTextFormat = InsertTextFormat.PlainText;
219193

220194
switch (completionDetails.CompletionType)
221195
{
222-
case CompletionType.Type:
223196
case CompletionType.Namespace:
224197
case CompletionType.ParameterValue:
225198
case CompletionType.Method:
226199
case CompletionType.Property:
227-
detailString = completionDetails.ToolTipText;
228-
break;
200+
case CompletionType.Keyword:
201+
case CompletionType.File:
202+
case CompletionType.History:
203+
case CompletionType.Text:
229204
case CompletionType.Variable:
205+
case CompletionType.Unknown:
206+
break;
207+
case CompletionType.Type:
208+
// Custom classes come through as types but the PowerShell completion tooltip
209+
// will start with "Class ", so we can more accurately display its icon.
210+
if (toolTipText.StartsWith("Class ", StringComparison.Ordinal))
211+
{
212+
kind = CompletionItemKind.Class;
213+
}
214+
break;
230215
case CompletionType.ParameterName:
231216
// Look for type encoded in the tooltip for parameters and variables.
232217
// Display PowerShell type names in [] to be consistent with PowerShell syntax
233218
// and how the debugger displays type names.
234-
var matches = Regex.Matches(completionDetails.ToolTipText, @"^(\[.+\])");
219+
MatchCollection matches = _regex.Matches(toolTipText);
235220
if ((matches.Count > 0) && (matches[0].Groups.Count > 1))
236221
{
237-
detailString = matches[0].Groups[1].Value;
222+
toolTipText = matches[0].Groups[1].Value;
238223
}
239-
// The comparison operators (-eq, -not, -gt, etc) are unfortunately fall into ParameterName
240-
// but they don't have a type associated to them. This allows those tooltips to show up.
241-
else if (!string.IsNullOrEmpty(completionDetails.ToolTipText))
224+
// The comparison operators (-eq, -not, -gt, etc) unfortunately come across as
225+
// ParameterName types but they don't have a type associated to them, so we can
226+
// deduce its an operator.
227+
else
242228
{
243-
detailString = completionDetails.ToolTipText;
229+
kind = CompletionItemKind.Operator;
244230
}
245231
break;
246232
case CompletionType.Command:
247-
// For Commands, let's extract the resolved command or the path for an exe
248-
// from the ToolTipText - if there is any ToolTipText.
249-
if (completionDetails.ToolTipText != null)
233+
// For commands, let's extract the resolved command or the path for an
234+
// executable from the tooltip.
235+
if (!string.IsNullOrEmpty(toolTipText))
250236
{
251237
// Fix for #240 - notepad++.exe in tooltip text caused regex parser to throw.
252-
string escapedToolTipText = Regex.Escape(completionDetails.ToolTipText);
253-
254-
// Don't display ToolTipText if it is the same as the ListItemText.
255-
// Reject command syntax ToolTipText - it's too much to display as a detailString.
256-
if (!completionDetails.ListItemText.Equals(
257-
completionDetails.ToolTipText,
258-
StringComparison.OrdinalIgnoreCase) &&
259-
!Regex.IsMatch(completionDetails.ToolTipText,
260-
@"^\s*" + escapedToolTipText + @"\s+\["))
238+
string escapedToolTipText = Regex.Escape(toolTipText);
239+
240+
// Don't display tooltip if it is the same as the ListItemText. Don't
241+
// display command syntax tooltip because it's too much.
242+
if (completionDetails.ListItemText.Equals(toolTipText, StringComparison.OrdinalIgnoreCase)
243+
|| Regex.IsMatch(toolTipText, @"^\s*" + escapedToolTipText + @"\s+\["))
261244
{
262-
detailString = completionDetails.ToolTipText;
245+
toolTipText = string.Empty;
263246
}
264247
}
265-
266248
break;
267249
case CompletionType.Folder:
268250
// Insert a final "tab stop" as identified by $0 in the snippet provided for completion.
@@ -275,14 +257,13 @@ private static CompletionItem CreateCompletionItem(
275257
// Since we want to use a "tab stop" we need to escape a few things for Textmate to render properly.
276258
if (EndsWithQuote(completionText))
277259
{
278-
var sb = new StringBuilder(completionDetails.CompletionText)
260+
StringBuilder sb = new StringBuilder(completionText)
279261
.Replace(@"\", @"\\")
280262
.Replace(@"}", @"\}")
281263
.Replace(@"$", @"\$");
282264
completionText = sb.Insert(sb.Length - 1, "$0").ToString();
283265
insertTextFormat = InsertTextFormat.Snippet;
284266
}
285-
286267
break;
287268
}
288269

@@ -291,16 +272,16 @@ private static CompletionItem CreateCompletionItem(
291272
// make sure the default order also be the lexicographical order
292273
// which we do by prefixing the ListItemText with a leading 0's
293274
// four digit index.
294-
var sortText = $"{sortIndex:D4}{completionDetails.ListItemText}";
275+
string sortText = $"{sortIndex:D4}{completionDetails.ListItemText}";
295276

296277
return new CompletionItem
297278
{
298279
InsertText = completionText,
299280
InsertTextFormat = insertTextFormat,
300281
Label = completionDetails.ListItemText,
301-
Kind = MapCompletionKind(completionDetails.CompletionType),
302-
Detail = detailString,
303-
Documentation = documentationString,
282+
Kind = kind,
283+
Detail = toolTipText,
284+
Documentation = string.Empty, // TODO: Fill this in agin.
304285
SortText = sortText,
305286
FilterText = completionDetails.CompletionText,
306287
TextEdit = new TextEdit
@@ -323,43 +304,33 @@ private static CompletionItem CreateCompletionItem(
323304
};
324305
}
325306

307+
// TODO: Unwrap this, it's confusing as it doesn't cover all cases and we conditionally
308+
// override it when it's inaccurate.
326309
private static CompletionItemKind MapCompletionKind(CompletionType completionType)
327310
{
328-
switch (completionType)
311+
return completionType switch
329312
{
330-
case CompletionType.Command:
331-
return CompletionItemKind.Function;
332-
333-
case CompletionType.Property:
334-
return CompletionItemKind.Property;
335-
336-
case CompletionType.Method:
337-
return CompletionItemKind.Method;
338-
339-
case CompletionType.Variable:
340-
case CompletionType.ParameterName:
341-
return CompletionItemKind.Variable;
342-
343-
case CompletionType.File:
344-
return CompletionItemKind.File;
345-
346-
case CompletionType.Folder:
347-
return CompletionItemKind.Folder;
348-
349-
default:
350-
return CompletionItemKind.Text;
351-
}
313+
CompletionType.Unknown => CompletionItemKind.Text,
314+
CompletionType.Command => CompletionItemKind.Function,
315+
CompletionType.Method => CompletionItemKind.Method,
316+
CompletionType.ParameterName => CompletionItemKind.Variable,
317+
CompletionType.ParameterValue => CompletionItemKind.Value,
318+
CompletionType.Property => CompletionItemKind.Property,
319+
CompletionType.Variable => CompletionItemKind.Variable,
320+
CompletionType.Namespace => CompletionItemKind.Module,
321+
CompletionType.Type => CompletionItemKind.TypeParameter,
322+
CompletionType.Keyword => CompletionItemKind.Keyword,
323+
CompletionType.File => CompletionItemKind.File,
324+
CompletionType.Folder => CompletionItemKind.Folder,
325+
CompletionType.History => CompletionItemKind.Reference,
326+
CompletionType.Text => CompletionItemKind.Text,
327+
_ => throw new ArgumentOutOfRangeException(nameof(completionType)),
328+
};
352329
}
353330

354331
private static bool EndsWithQuote(string text)
355332
{
356-
if (string.IsNullOrEmpty(text))
357-
{
358-
return false;
359-
}
360-
361-
char lastChar = text[text.Length - 1];
362-
return lastChar == '"' || lastChar == '\'';
333+
return !string.IsNullOrEmpty(text) && text[text.Length - 1] is '"' or '\'';
363334
}
364335
}
365336
}

0 commit comments

Comments
 (0)