@@ -30,6 +30,7 @@ internal class PsesCompletionHandler : CompletionHandlerBase
30
30
private readonly IRunspaceContext _runspaceContext ;
31
31
private readonly IInternalPowerShellExecutionService _executionService ;
32
32
private readonly WorkspaceService _workspaceService ;
33
+ private CompletionCapability _completionCapability ;
33
34
34
35
public PsesCompletionHandler (
35
36
ILoggerFactory factory ,
@@ -43,13 +44,21 @@ public PsesCompletionHandler(
43
44
_workspaceService = workspaceService ;
44
45
}
45
46
46
- protected override CompletionRegistrationOptions CreateRegistrationOptions ( CompletionCapability capability , ClientCapabilities clientCapabilities ) => new ( )
47
+ protected override CompletionRegistrationOptions CreateRegistrationOptions ( CompletionCapability capability , ClientCapabilities clientCapabilities )
48
+ {
49
+ _completionCapability = capability ;
50
+ return new CompletionRegistrationOptions ( )
47
51
{
48
52
// TODO: What do we do with the arguments?
49
53
DocumentSelector = LspUtils . PowerShellDocumentSelector ,
50
54
ResolveProvider = true ,
51
- TriggerCharacters = new [ ] { "." , "-" , ":" , "\\ " , "$" , " " }
55
+ TriggerCharacters = new [ ] { "." , "-" , ":" , "\\ " , "$" , " " } ,
52
56
} ;
57
+ }
58
+
59
+ public bool SupportsSnippets => _completionCapability ? . CompletionItem ? . SnippetSupport is true ;
60
+
61
+ public bool SupportsCommitCharacters => _completionCapability ? . CompletionItem ? . CommitCharactersSupport is true ;
53
62
54
63
public override async Task < CompletionList > Handle ( CompletionParams request , CancellationToken cancellationToken )
55
64
{
@@ -143,6 +152,14 @@ internal async Task<CompletionResults> GetCompletionsInFileAsync(
143
152
result . ReplacementIndex ,
144
153
result . ReplacementIndex + result . ReplacementLength ) ;
145
154
155
+ string textToBeReplaced = string . Empty ;
156
+ if ( result . ReplacementLength is not 0 )
157
+ {
158
+ textToBeReplaced = scriptFile . Contents . Substring (
159
+ result . ReplacementIndex ,
160
+ result . ReplacementLength ) ;
161
+ }
162
+
146
163
bool isIncomplete = false ;
147
164
// Create OmniSharp CompletionItems from PowerShell CompletionResults. We use a for loop
148
165
// because the index is used for sorting.
@@ -159,16 +176,25 @@ internal async Task<CompletionResults> GetCompletionsInFileAsync(
159
176
isIncomplete = true ;
160
177
}
161
178
162
- completionItems [ i ] = CreateCompletionItem ( result . CompletionMatches [ i ] , replacedRange , i + 1 ) ;
179
+ completionItems [ i ] = CreateCompletionItem (
180
+ result . CompletionMatches [ i ] ,
181
+ replacedRange ,
182
+ i + 1 ,
183
+ textToBeReplaced ,
184
+ scriptFile ) ;
185
+
163
186
_logger . LogTrace ( "Created completion item: " + completionItems [ i ] + " with " + completionItems [ i ] . TextEdit ) ;
164
187
}
188
+
165
189
return new CompletionResults ( isIncomplete , completionItems ) ;
166
190
}
167
191
168
- internal static CompletionItem CreateCompletionItem (
192
+ internal CompletionItem CreateCompletionItem (
169
193
CompletionResult result ,
170
194
BufferRange completionRange ,
171
- int sortIndex )
195
+ int sortIndex ,
196
+ string textToBeReplaced ,
197
+ ScriptFile scriptFile )
172
198
{
173
199
Validate . IsNotNull ( nameof ( result ) , result ) ;
174
200
@@ -200,7 +226,9 @@ internal static CompletionItem CreateCompletionItem(
200
226
? string . Empty : detail , // Don't repeat label.
201
227
// Retain PowerShell's sort order with the given index.
202
228
SortText = $ "{ sortIndex : D4} { result . ListItemText } ",
203
- FilterText = result . CompletionText ,
229
+ FilterText = result . ResultType is CompletionResultType . Type
230
+ ? GetTypeFilterText ( textToBeReplaced , result . CompletionText )
231
+ : result . CompletionText ,
204
232
// Used instead of Label when TextEdit is unsupported
205
233
InsertText = result . CompletionText ,
206
234
// Used instead of InsertText when possible
@@ -212,8 +240,8 @@ internal static CompletionItem CreateCompletionItem(
212
240
CompletionResultType . Text => item with { Kind = CompletionItemKind . Text } ,
213
241
CompletionResultType . History => item with { Kind = CompletionItemKind . Reference } ,
214
242
CompletionResultType . Command => item with { Kind = CompletionItemKind . Function } ,
215
- CompletionResultType . ProviderItem => item with { Kind = CompletionItemKind . File } ,
216
- CompletionResultType . ProviderContainer => TryBuildSnippet ( result . CompletionText , out string snippet )
243
+ CompletionResultType . ProviderItem or CompletionResultType . ProviderContainer
244
+ => CreateProviderItemCompletion ( item , result , scriptFile , textToBeReplaced ) ,
217
245
? item with
218
246
{
219
247
Kind = CompletionItemKind . Folder ,
@@ -245,6 +273,99 @@ internal static CompletionItem CreateCompletionItem(
245
273
} ;
246
274
}
247
275
276
+ private CompletionItem CreateProviderItemCompletion (
277
+ CompletionItem item ,
278
+ CompletionResult result ,
279
+ ScriptFile scriptFile ,
280
+ string textToBeReplaced )
281
+ {
282
+ // TODO: Work out a way to do this generally instead of special casing PSScriptRoot.
283
+ //
284
+ // This code relies on PowerShell/PowerShell#17376. Until that makes it into a release
285
+ // no matches will be returned anyway.
286
+ const string PSScriptRootVariable = "$PSScriptRoot" ;
287
+ string completionText = result . CompletionText ;
288
+ if ( textToBeReplaced . IndexOf ( PSScriptRootVariable , StringComparison . OrdinalIgnoreCase ) is int variableIndex and not - 1
289
+ && System . IO . Path . GetDirectoryName ( scriptFile . FilePath ) is string scriptFolder and not ""
290
+ && completionText . IndexOf ( scriptFolder , StringComparison . OrdinalIgnoreCase ) is int pathIndex and not - 1
291
+ && ! scriptFile . IsInMemory )
292
+ {
293
+ completionText = completionText
294
+ . Remove ( pathIndex , scriptFolder . Length )
295
+ . Insert ( variableIndex , textToBeReplaced . Substring ( variableIndex , PSScriptRootVariable . Length ) ) ;
296
+ }
297
+
298
+ InsertTextFormat insertFormat ;
299
+ TextEdit edit ;
300
+ CompletionItemKind itemKind ;
301
+ if ( result . ResultType is CompletionResultType . ProviderContainer
302
+ && SupportsSnippets
303
+ && TryBuildSnippet ( completionText , out string snippet ) )
304
+ {
305
+ edit = item . TextEdit . TextEdit with { NewText = snippet } ;
306
+ insertFormat = InsertTextFormat . Snippet ;
307
+ itemKind = CompletionItemKind . Folder ;
308
+ }
309
+ else
310
+ {
311
+ edit = item . TextEdit . TextEdit with { NewText = completionText } ;
312
+ insertFormat = default ;
313
+ itemKind = CompletionItemKind . File ;
314
+ }
315
+
316
+ return item with
317
+ {
318
+ Kind = itemKind ,
319
+ TextEdit = edit ,
320
+ InsertText = completionText ,
321
+ FilterText = completionText ,
322
+ InsertTextFormat = insertFormat ,
323
+ CommitCharacters = MaybeAddCommitCharacters ( "\\ " , "/" , "'" , "\" " ) ,
324
+ } ;
325
+ }
326
+
327
+ private Container < string > MaybeAddCommitCharacters ( params string [ ] characters )
328
+ => SupportsCommitCharacters ? new Container < string > ( characters ) : null ;
329
+
330
+ private static string GetTypeFilterText ( string textToBeReplaced , string completionText )
331
+ {
332
+ // FilterText for a type name with using statements gets a little complicated. Consider
333
+ // this script:
334
+ //
335
+ // using namespace System.Management.Automation
336
+ // [System.Management.Automation.Tracing.]
337
+ //
338
+ // Since we're emitting an edit that replaces `System.Management.Automation.Tracing.` with
339
+ // `Tracing.NullWriter` (for example), we can't use CompletionText as the filter. If we
340
+ // do, we won't find any matches because it's trying to filter `Tracing.NullWriter` with
341
+ // `System.Management.Automation.Tracing.` which is too different. So we prepend each
342
+ // namespace that exists in our original text but does not in our completion text.
343
+ if ( ! textToBeReplaced . Contains ( '.' ) )
344
+ {
345
+ return completionText ;
346
+ }
347
+
348
+ string [ ] oldTypeParts = textToBeReplaced . Split ( '.' ) ;
349
+ string [ ] newTypeParts = completionText . Split ( '.' ) ;
350
+
351
+ StringBuilder newFilterText = new ( completionText ) ;
352
+
353
+ int newPartsIndex = newTypeParts . Length - 2 ;
354
+ for ( int i = oldTypeParts . Length - 2 ; i >= 0 ; i -- )
355
+ {
356
+ if ( newPartsIndex is >= 0
357
+ && newTypeParts [ newPartsIndex ] . Equals ( oldTypeParts [ i ] , StringComparison . OrdinalIgnoreCase ) )
358
+ {
359
+ newPartsIndex -- ;
360
+ continue ;
361
+ }
362
+
363
+ newFilterText . Insert ( 0 , '.' ) . Insert ( 0 , oldTypeParts [ i ] ) ;
364
+ }
365
+
366
+ return newFilterText . ToString ( ) ;
367
+ }
368
+
248
369
private static readonly Regex s_typeRegex = new ( @"^(\[.+\])" , RegexOptions . Compiled ) ;
249
370
250
371
/// <summary>
0 commit comments