Skip to content

Commit e857863

Browse files
Merge pull request #1809 from SeeminglyScience/update-member-tooltips
Additional IntelliSense fixes and ToolTip overhaul
2 parents 8aa9e11 + f4548aa commit e857863

File tree

5 files changed

+677
-28
lines changed

5 files changed

+677
-28
lines changed

src/PowerShellEditorServices/Services/Symbols/Vistors/AstOperations.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,15 @@ await executionService.ExecuteDelegateAsync(
127127
.ConfigureAwait(false);
128128

129129
stopwatch.Stop();
130-
logger.LogTrace($"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms: {commandCompletion}");
130+
logger.LogTrace(
131+
"IntelliSense completed in {elapsed}ms - WordToComplete: \"{word}\" MatchCount: {count}",
132+
stopwatch.ElapsedMilliseconds,
133+
commandCompletion.ReplacementLength > 0
134+
? scriptAst.Extent.StartScriptPosition.GetFullScript()?.Substring(
135+
commandCompletion.ReplacementIndex,
136+
commandCompletion.ReplacementLength)
137+
: null,
138+
commandCompletion.CompletionMatches.Count);
131139

132140
return commandCompletion;
133141
}

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

Lines changed: 208 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Linq;
67
using System.Management.Automation;
78
using System.Text;
89
using System.Text.RegularExpressions;
@@ -30,6 +31,7 @@ internal class PsesCompletionHandler : CompletionHandlerBase
3031
private readonly IRunspaceContext _runspaceContext;
3132
private readonly IInternalPowerShellExecutionService _executionService;
3233
private readonly WorkspaceService _workspaceService;
34+
private CompletionCapability _completionCapability;
3335

3436
public PsesCompletionHandler(
3537
ILoggerFactory factory,
@@ -43,13 +45,23 @@ public PsesCompletionHandler(
4345
_workspaceService = workspaceService;
4446
}
4547

46-
protected override CompletionRegistrationOptions CreateRegistrationOptions(CompletionCapability capability, ClientCapabilities clientCapabilities) => new()
48+
protected override CompletionRegistrationOptions CreateRegistrationOptions(CompletionCapability capability, ClientCapabilities clientCapabilities)
4749
{
48-
// TODO: What do we do with the arguments?
49-
DocumentSelector = LspUtils.PowerShellDocumentSelector,
50-
ResolveProvider = true,
51-
TriggerCharacters = new[] { ".", "-", ":", "\\", "$", " " }
52-
};
50+
_completionCapability = capability;
51+
return new CompletionRegistrationOptions()
52+
{
53+
// TODO: What do we do with the arguments?
54+
DocumentSelector = LspUtils.PowerShellDocumentSelector,
55+
ResolveProvider = true,
56+
TriggerCharacters = new[] { ".", "-", ":", "\\", "$", " " },
57+
};
58+
}
59+
60+
public bool SupportsSnippets => _completionCapability?.CompletionItem?.SnippetSupport is true;
61+
62+
public bool SupportsCommitCharacters => _completionCapability?.CompletionItem?.CommitCharactersSupport is true;
63+
64+
public bool SupportsMarkdown => _completionCapability?.CompletionItem?.DocumentationFormat?.Contains(MarkupKind.Markdown) is true;
5365

5466
public override async Task<CompletionList> Handle(CompletionParams request, CancellationToken cancellationToken)
5567
{
@@ -72,6 +84,61 @@ public override async Task<CompletionList> Handle(CompletionParams request, Canc
7284
// Handler for "completionItem/resolve". In VSCode this is fired when a completion item is highlighted in the completion list.
7385
public override async Task<CompletionItem> Handle(CompletionItem request, CancellationToken cancellationToken)
7486
{
87+
if (SupportsMarkdown)
88+
{
89+
if (request.Kind is CompletionItemKind.Method)
90+
{
91+
string documentation = FormatUtils.GetMethodDocumentation(
92+
_logger,
93+
request.Data.ToString(),
94+
out MarkupKind kind);
95+
96+
return request with
97+
{
98+
Documentation = new MarkupContent()
99+
{
100+
Kind = kind,
101+
Value = documentation,
102+
},
103+
};
104+
}
105+
106+
if (request.Kind is CompletionItemKind.Class or CompletionItemKind.TypeParameter or CompletionItemKind.Enum)
107+
{
108+
string documentation = FormatUtils.GetTypeDocumentation(
109+
_logger,
110+
request.Detail,
111+
out MarkupKind kind);
112+
113+
return request with
114+
{
115+
Detail = null,
116+
Documentation = new MarkupContent()
117+
{
118+
Kind = kind,
119+
Value = documentation,
120+
},
121+
};
122+
}
123+
124+
if (request.Kind is CompletionItemKind.EnumMember or CompletionItemKind.Property or CompletionItemKind.Field)
125+
{
126+
string documentation = FormatUtils.GetPropertyDocumentation(
127+
_logger,
128+
request.Data.ToString(),
129+
out MarkupKind kind);
130+
131+
return request with
132+
{
133+
Documentation = new MarkupContent()
134+
{
135+
Kind = kind,
136+
Value = documentation,
137+
},
138+
};
139+
}
140+
}
141+
75142
// We currently only support this request for anything that returns a CommandInfo:
76143
// functions, cmdlets, aliases. No detail means the module hasn't been imported yet and
77144
// IntelliSense shouldn't import the module to get this info.
@@ -143,6 +210,14 @@ internal async Task<CompletionResults> GetCompletionsInFileAsync(
143210
result.ReplacementIndex,
144211
result.ReplacementIndex + result.ReplacementLength);
145212

213+
string textToBeReplaced = string.Empty;
214+
if (result.ReplacementLength is not 0)
215+
{
216+
textToBeReplaced = scriptFile.Contents.Substring(
217+
result.ReplacementIndex,
218+
result.ReplacementLength);
219+
}
220+
146221
bool isIncomplete = false;
147222
// Create OmniSharp CompletionItems from PowerShell CompletionResults. We use a for loop
148223
// because the index is used for sorting.
@@ -159,16 +234,25 @@ internal async Task<CompletionResults> GetCompletionsInFileAsync(
159234
isIncomplete = true;
160235
}
161236

162-
completionItems[i] = CreateCompletionItem(result.CompletionMatches[i], replacedRange, i + 1);
237+
completionItems[i] = CreateCompletionItem(
238+
result.CompletionMatches[i],
239+
replacedRange,
240+
i + 1,
241+
textToBeReplaced,
242+
scriptFile);
243+
163244
_logger.LogTrace("Created completion item: " + completionItems[i] + " with " + completionItems[i].TextEdit);
164245
}
246+
165247
return new CompletionResults(isIncomplete, completionItems);
166248
}
167249

168-
internal static CompletionItem CreateCompletionItem(
250+
internal CompletionItem CreateCompletionItem(
169251
CompletionResult result,
170252
BufferRange completionRange,
171-
int sortIndex)
253+
int sortIndex,
254+
string textToBeReplaced,
255+
ScriptFile scriptFile)
172256
{
173257
Validate.IsNotNull(nameof(result), result);
174258

@@ -200,7 +284,9 @@ internal static CompletionItem CreateCompletionItem(
200284
? string.Empty : detail, // Don't repeat label.
201285
// Retain PowerShell's sort order with the given index.
202286
SortText = $"{sortIndex:D4}{result.ListItemText}",
203-
FilterText = result.CompletionText,
287+
FilterText = result.ResultType is CompletionResultType.Type
288+
? GetTypeFilterText(textToBeReplaced, result.CompletionText)
289+
: result.CompletionText,
204290
// Used instead of Label when TextEdit is unsupported
205291
InsertText = result.CompletionText,
206292
// Used instead of InsertText when possible
@@ -212,17 +298,21 @@ internal static CompletionItem CreateCompletionItem(
212298
CompletionResultType.Text => item with { Kind = CompletionItemKind.Text },
213299
CompletionResultType.History => item with { Kind = CompletionItemKind.Reference },
214300
CompletionResultType.Command => item with { Kind = CompletionItemKind.Function },
215-
CompletionResultType.ProviderItem => item with { Kind = CompletionItemKind.File },
216-
CompletionResultType.ProviderContainer => TryBuildSnippet(result.CompletionText, out string snippet)
217-
? item with
218-
{
219-
Kind = CompletionItemKind.Folder,
220-
InsertTextFormat = InsertTextFormat.Snippet,
221-
TextEdit = textEdit with { NewText = snippet }
222-
}
223-
: item with { Kind = CompletionItemKind.Folder },
224-
CompletionResultType.Property => item with { Kind = CompletionItemKind.Property },
225-
CompletionResultType.Method => item with { Kind = CompletionItemKind.Method },
301+
CompletionResultType.ProviderItem or CompletionResultType.ProviderContainer
302+
=> CreateProviderItemCompletion(item, result, scriptFile, textToBeReplaced),
303+
CompletionResultType.Property => item with
304+
{
305+
Kind = CompletionItemKind.Property,
306+
Detail = SupportsMarkdown ? null : detail,
307+
Data = SupportsMarkdown ? detail : null,
308+
CommitCharacters = MaybeAddCommitCharacters("."),
309+
},
310+
CompletionResultType.Method => item with
311+
{
312+
Kind = CompletionItemKind.Method,
313+
Data = item.Detail,
314+
Detail = SupportsMarkdown ? null : item.Detail,
315+
},
226316
CompletionResultType.ParameterName => TryExtractType(detail, out string type)
227317
? item with { Kind = CompletionItemKind.Variable, Detail = type }
228318
// The comparison operators (-eq, -not, -gt, etc) unfortunately come across as
@@ -237,14 +327,109 @@ internal static CompletionItem CreateCompletionItem(
237327
CompletionResultType.Type => detail.StartsWith("Class ", StringComparison.CurrentCulture)
238328
// Custom classes come through as types but the PowerShell completion tooltip
239329
// will start with "Class ", so we can more accurately display its icon.
240-
? item with { Kind = CompletionItemKind.Class }
241-
: item with { Kind = CompletionItemKind.TypeParameter },
330+
? item with { Kind = CompletionItemKind.Class, Detail = detail.Substring("Class ".Length) }
331+
: detail.StartsWith("Enum ", StringComparison.CurrentCulture)
332+
? item with { Kind = CompletionItemKind.Enum, Detail = detail.Substring("Enum ".Length) }
333+
: item with { Kind = CompletionItemKind.TypeParameter },
242334
CompletionResultType.Keyword or CompletionResultType.DynamicKeyword =>
243335
item with { Kind = CompletionItemKind.Keyword },
244336
_ => throw new ArgumentOutOfRangeException(nameof(result))
245337
};
246338
}
247339

340+
private CompletionItem CreateProviderItemCompletion(
341+
CompletionItem item,
342+
CompletionResult result,
343+
ScriptFile scriptFile,
344+
string textToBeReplaced)
345+
{
346+
// TODO: Work out a way to do this generally instead of special casing PSScriptRoot.
347+
//
348+
// This code relies on PowerShell/PowerShell#17376. Until that makes it into a release
349+
// no matches will be returned anyway.
350+
const string PSScriptRootVariable = "$PSScriptRoot";
351+
string completionText = result.CompletionText;
352+
if (textToBeReplaced.IndexOf(PSScriptRootVariable, StringComparison.OrdinalIgnoreCase) is int variableIndex and not -1
353+
&& System.IO.Path.GetDirectoryName(scriptFile.FilePath) is string scriptFolder and not ""
354+
&& completionText.IndexOf(scriptFolder, StringComparison.OrdinalIgnoreCase) is int pathIndex and not -1
355+
&& !scriptFile.IsInMemory)
356+
{
357+
completionText = completionText
358+
.Remove(pathIndex, scriptFolder.Length)
359+
.Insert(variableIndex, textToBeReplaced.Substring(variableIndex, PSScriptRootVariable.Length));
360+
}
361+
362+
InsertTextFormat insertFormat;
363+
TextEdit edit;
364+
CompletionItemKind itemKind;
365+
if (result.ResultType is CompletionResultType.ProviderContainer
366+
&& SupportsSnippets
367+
&& TryBuildSnippet(completionText, out string snippet))
368+
{
369+
edit = item.TextEdit.TextEdit with { NewText = snippet };
370+
insertFormat = InsertTextFormat.Snippet;
371+
itemKind = CompletionItemKind.Folder;
372+
}
373+
else
374+
{
375+
edit = item.TextEdit.TextEdit with { NewText = completionText };
376+
insertFormat = default;
377+
itemKind = CompletionItemKind.File;
378+
}
379+
380+
return item with
381+
{
382+
Kind = itemKind,
383+
TextEdit = edit,
384+
InsertText = completionText,
385+
FilterText = completionText,
386+
InsertTextFormat = insertFormat,
387+
CommitCharacters = MaybeAddCommitCharacters("\\", "/", "'", "\""),
388+
};
389+
}
390+
391+
private Container<string> MaybeAddCommitCharacters(params string[] characters)
392+
=> SupportsCommitCharacters ? new Container<string>(characters) : null;
393+
394+
private static string GetTypeFilterText(string textToBeReplaced, string completionText)
395+
{
396+
// FilterText for a type name with using statements gets a little complicated. Consider
397+
// this script:
398+
//
399+
// using namespace System.Management.Automation
400+
// [System.Management.Automation.Tracing.]
401+
//
402+
// Since we're emitting an edit that replaces `System.Management.Automation.Tracing.` with
403+
// `Tracing.NullWriter` (for example), we can't use CompletionText as the filter. If we
404+
// do, we won't find any matches because it's trying to filter `Tracing.NullWriter` with
405+
// `System.Management.Automation.Tracing.` which is too different. So we prepend each
406+
// namespace that exists in our original text but does not in our completion text.
407+
if (!textToBeReplaced.Contains('.'))
408+
{
409+
return completionText;
410+
}
411+
412+
string[] oldTypeParts = textToBeReplaced.Split('.');
413+
string[] newTypeParts = completionText.Split('.');
414+
415+
StringBuilder newFilterText = new(completionText);
416+
417+
int newPartsIndex = newTypeParts.Length - 2;
418+
for (int i = oldTypeParts.Length - 2; i >= 0; i--)
419+
{
420+
if (newPartsIndex is >= 0
421+
&& newTypeParts[newPartsIndex].Equals(oldTypeParts[i], StringComparison.OrdinalIgnoreCase))
422+
{
423+
newPartsIndex--;
424+
continue;
425+
}
426+
427+
newFilterText.Insert(0, '.').Insert(0, oldTypeParts[i]);
428+
}
429+
430+
return newFilterText.ToString();
431+
}
432+
248433
private static readonly Regex s_typeRegex = new(@"^(\[.+\])", RegexOptions.Compiled);
249434

250435
/// <summary>

src/PowerShellEditorServices/Utility/Extensions.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Linq;
66
using System.Collections.Generic;
77
using System.Management.Automation.Language;
8+
using System.Text;
89

910
namespace Microsoft.PowerShell.EditorServices.Utility
1011
{
@@ -144,5 +145,21 @@ public static bool Contains(this IScriptExtent scriptExtent, int line, int colum
144145

145146
return true;
146147
}
148+
149+
/// <summary>
150+
/// Same as <see cref="StringBuilder.AppendLine()" /> but never CRLF. Use this when building
151+
/// formatting for clients that may not render CRLF correctly.
152+
/// </summary>
153+
/// <param name="self"></param>
154+
public static StringBuilder AppendLineLF(this StringBuilder self) => self.Append('\n');
155+
156+
/// <summary>
157+
/// Same as <see cref="StringBuilder.AppendLine(string)" /> but never CRLF. Use this when building
158+
/// formatting for clients that may not render CRLF correctly.
159+
/// </summary>
160+
/// <param name="self"></param>
161+
/// <param name="value"></param>
162+
public static StringBuilder AppendLineLF(this StringBuilder self, string value)
163+
=> self.Append(value).Append('\n');
147164
}
148165
}

0 commit comments

Comments
 (0)