@@ -30,6 +30,7 @@ internal class PsesCompletionHandler : CompletionHandlerBase
3030 private readonly IRunspaceContext _runspaceContext ;
3131 private readonly IInternalPowerShellExecutionService _executionService ;
3232 private readonly WorkspaceService _workspaceService ;
33+ private CompletionCapability _completionCapability ;
3334
3435 public PsesCompletionHandler (
3536 ILoggerFactory factory ,
@@ -43,13 +44,21 @@ public PsesCompletionHandler(
4344 _workspaceService = workspaceService ;
4445 }
4546
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 ( )
4751 {
4852 // TODO: What do we do with the arguments?
4953 DocumentSelector = LspUtils . PowerShellDocumentSelector ,
5054 ResolveProvider = true ,
51- TriggerCharacters = new [ ] { "." , "-" , ":" , "\\ " , "$" , " " }
55+ TriggerCharacters = new [ ] { "." , "-" , ":" , "\\ " , "$" , " " } ,
5256 } ;
57+ }
58+
59+ public bool SupportsSnippets => _completionCapability ? . CompletionItem ? . SnippetSupport is true ;
60+
61+ public bool SupportsCommitCharacters => _completionCapability ? . CompletionItem ? . CommitCharactersSupport is true ;
5362
5463 public override async Task < CompletionList > Handle ( CompletionParams request , CancellationToken cancellationToken )
5564 {
@@ -143,6 +152,14 @@ internal async Task<CompletionResults> GetCompletionsInFileAsync(
143152 result . ReplacementIndex ,
144153 result . ReplacementIndex + result . ReplacementLength ) ;
145154
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+
146163 bool isIncomplete = false ;
147164 // Create OmniSharp CompletionItems from PowerShell CompletionResults. We use a for loop
148165 // because the index is used for sorting.
@@ -159,16 +176,25 @@ internal async Task<CompletionResults> GetCompletionsInFileAsync(
159176 isIncomplete = true ;
160177 }
161178
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+
163186 _logger . LogTrace ( "Created completion item: " + completionItems [ i ] + " with " + completionItems [ i ] . TextEdit ) ;
164187 }
188+
165189 return new CompletionResults ( isIncomplete , completionItems ) ;
166190 }
167191
168- internal static CompletionItem CreateCompletionItem (
192+ internal CompletionItem CreateCompletionItem (
169193 CompletionResult result ,
170194 BufferRange completionRange ,
171- int sortIndex )
195+ int sortIndex ,
196+ string textToBeReplaced ,
197+ ScriptFile scriptFile )
172198 {
173199 Validate . IsNotNull ( nameof ( result ) , result ) ;
174200
@@ -200,7 +226,9 @@ internal static CompletionItem CreateCompletionItem(
200226 ? string . Empty : detail , // Don't repeat label.
201227 // Retain PowerShell's sort order with the given index.
202228 SortText = $ "{ sortIndex : D4} { result . ListItemText } ",
203- FilterText = result . CompletionText ,
229+ FilterText = result . ResultType is CompletionResultType . Type
230+ ? GetTypeFilterText ( textToBeReplaced , result . CompletionText )
231+ : result . CompletionText ,
204232 // Used instead of Label when TextEdit is unsupported
205233 InsertText = result . CompletionText ,
206234 // Used instead of InsertText when possible
@@ -212,8 +240,8 @@ internal static CompletionItem CreateCompletionItem(
212240 CompletionResultType . Text => item with { Kind = CompletionItemKind . Text } ,
213241 CompletionResultType . History => item with { Kind = CompletionItemKind . Reference } ,
214242 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 ) ,
217245 ? item with
218246 {
219247 Kind = CompletionItemKind . Folder ,
@@ -245,6 +273,99 @@ internal static CompletionItem CreateCompletionItem(
245273 } ;
246274 }
247275
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+
248369 private static readonly Regex s_typeRegex = new ( @"^(\[.+\])" , RegexOptions . Compiled ) ;
249370
250371 /// <summary>
0 commit comments