3
3
4
4
using System ;
5
5
using System . Collections . Generic ;
6
+ using System . Linq ;
6
7
using System . Management . Automation ;
7
8
using System . Text ;
8
9
using System . Text . RegularExpressions ;
@@ -30,6 +31,7 @@ internal class PsesCompletionHandler : CompletionHandlerBase
30
31
private readonly IRunspaceContext _runspaceContext ;
31
32
private readonly IInternalPowerShellExecutionService _executionService ;
32
33
private readonly WorkspaceService _workspaceService ;
34
+ private CompletionCapability _completionCapability ;
33
35
34
36
public PsesCompletionHandler (
35
37
ILoggerFactory factory ,
@@ -43,13 +45,23 @@ public PsesCompletionHandler(
43
45
_workspaceService = workspaceService ;
44
46
}
45
47
46
- protected override CompletionRegistrationOptions CreateRegistrationOptions ( CompletionCapability capability , ClientCapabilities clientCapabilities ) => new ( )
48
+ protected override CompletionRegistrationOptions CreateRegistrationOptions ( CompletionCapability capability , ClientCapabilities clientCapabilities )
47
49
{
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 ;
53
65
54
66
public override async Task < CompletionList > Handle ( CompletionParams request , CancellationToken cancellationToken )
55
67
{
@@ -72,6 +84,61 @@ public override async Task<CompletionList> Handle(CompletionParams request, Canc
72
84
// Handler for "completionItem/resolve". In VSCode this is fired when a completion item is highlighted in the completion list.
73
85
public override async Task < CompletionItem > Handle ( CompletionItem request , CancellationToken cancellationToken )
74
86
{
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
+
75
142
// We currently only support this request for anything that returns a CommandInfo:
76
143
// functions, cmdlets, aliases. No detail means the module hasn't been imported yet and
77
144
// IntelliSense shouldn't import the module to get this info.
@@ -143,6 +210,14 @@ internal async Task<CompletionResults> GetCompletionsInFileAsync(
143
210
result . ReplacementIndex ,
144
211
result . ReplacementIndex + result . ReplacementLength ) ;
145
212
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
+
146
221
bool isIncomplete = false ;
147
222
// Create OmniSharp CompletionItems from PowerShell CompletionResults. We use a for loop
148
223
// because the index is used for sorting.
@@ -159,16 +234,25 @@ internal async Task<CompletionResults> GetCompletionsInFileAsync(
159
234
isIncomplete = true ;
160
235
}
161
236
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
+
163
244
_logger . LogTrace ( "Created completion item: " + completionItems [ i ] + " with " + completionItems [ i ] . TextEdit ) ;
164
245
}
246
+
165
247
return new CompletionResults ( isIncomplete , completionItems ) ;
166
248
}
167
249
168
- internal static CompletionItem CreateCompletionItem (
250
+ internal CompletionItem CreateCompletionItem (
169
251
CompletionResult result ,
170
252
BufferRange completionRange ,
171
- int sortIndex )
253
+ int sortIndex ,
254
+ string textToBeReplaced ,
255
+ ScriptFile scriptFile )
172
256
{
173
257
Validate . IsNotNull ( nameof ( result ) , result ) ;
174
258
@@ -200,7 +284,9 @@ internal static CompletionItem CreateCompletionItem(
200
284
? string . Empty : detail , // Don't repeat label.
201
285
// Retain PowerShell's sort order with the given index.
202
286
SortText = $ "{ sortIndex : D4} { result . ListItemText } ",
203
- FilterText = result . CompletionText ,
287
+ FilterText = result . ResultType is CompletionResultType . Type
288
+ ? GetTypeFilterText ( textToBeReplaced , result . CompletionText )
289
+ : result . CompletionText ,
204
290
// Used instead of Label when TextEdit is unsupported
205
291
InsertText = result . CompletionText ,
206
292
// Used instead of InsertText when possible
@@ -212,17 +298,21 @@ internal static CompletionItem CreateCompletionItem(
212
298
CompletionResultType . Text => item with { Kind = CompletionItemKind . Text } ,
213
299
CompletionResultType . History => item with { Kind = CompletionItemKind . Reference } ,
214
300
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
+ } ,
226
316
CompletionResultType . ParameterName => TryExtractType ( detail , out string type )
227
317
? item with { Kind = CompletionItemKind . Variable , Detail = type }
228
318
// The comparison operators (-eq, -not, -gt, etc) unfortunately come across as
@@ -237,14 +327,109 @@ internal static CompletionItem CreateCompletionItem(
237
327
CompletionResultType . Type => detail . StartsWith ( "Class " , StringComparison . CurrentCulture )
238
328
// Custom classes come through as types but the PowerShell completion tooltip
239
329
// 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 } ,
242
334
CompletionResultType . Keyword or CompletionResultType . DynamicKeyword =>
243
335
item with { Kind = CompletionItemKind . Keyword } ,
244
336
_ => throw new ArgumentOutOfRangeException ( nameof ( result ) )
245
337
} ;
246
338
}
247
339
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
+
248
433
private static readonly Regex s_typeRegex = new ( @"^(\[.+\])" , RegexOptions . Compiled ) ;
249
434
250
435
/// <summary>
0 commit comments