33
44using System ;
55using System . Collections . Generic ;
6+ using System . Linq ;
67using System . Management . Automation ;
78using System . Text ;
89using 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>
0 commit comments