@@ -32,7 +32,6 @@ internal class PsesCompletionHandler : ICompletionHandler, ICompletionResolveHan
3232 private readonly WorkspaceService _workspaceService ;
3333 private CompletionCapability _capability ;
3434 private readonly Guid _id = Guid . NewGuid ( ) ;
35- private static readonly Regex _typeRegex = new ( @"^(\[.+\])" ) ;
3635
3736 Guid ICanBeIdentifiedHandler . Id => _id ;
3837
@@ -168,163 +167,132 @@ public async Task<IEnumerable<CompletionItem>> GetCompletionsInFileAsync(
168167 }
169168
170169 internal static CompletionItem CreateCompletionItem (
171- CompletionResult completion ,
170+ CompletionResult result ,
172171 BufferRange completionRange ,
173172 int sortIndex )
174173 {
175- Validate . IsNotNull ( nameof ( completion ) , completion ) ;
174+ Validate . IsNotNull ( nameof ( result ) , result ) ;
176175
177- // Some tooltips may have newlines or whitespace for unknown reasons.
178- string toolTipText = completion . ToolTip ? . Trim ( ) ;
176+ TextEdit textEdit = new ( )
177+ {
178+ NewText = result . CompletionText ,
179+ Range = new Range
180+ {
181+ Start = new Position
182+ {
183+ Line = completionRange . Start . Line - 1 ,
184+ Character = completionRange . Start . Column - 1
185+ } ,
186+ End = new Position
187+ {
188+ Line = completionRange . End . Line - 1 ,
189+ Character = completionRange . End . Column - 1
190+ }
191+ }
192+ } ;
179193
180- string completionText = completion . CompletionText ;
181- InsertTextFormat insertTextFormat = InsertTextFormat . PlainText ;
182- CompletionItemKind kind ;
194+ // Some tooltips may have newlines or whitespace for unknown reasons.
195+ string detail = result . ToolTip ? . Trim ( ) ;
183196
184- // Force the client to maintain the sort order in which the original completion results
185- // were returned. We just need to make sure the default order also be the
186- // lexicographical order which we do by prefixing the ListItemText with a leading 0's
187- // four digit index.
188- string sortText = $ "{ sortIndex : D4} { completion . ListItemText } ";
197+ CompletionItem item = new ( )
198+ {
199+ Label = result . ListItemText ,
200+ Detail = result . ListItemText . Equals ( detail , StringComparison . CurrentCulture )
201+ ? string . Empty : detail , // Don't repeat label.
202+ // Retain PowerShell's sort order with the given index.
203+ SortText = $ "{ sortIndex : D4} { result . ListItemText } ",
204+ FilterText = result . CompletionText ,
205+ TextEdit = textEdit // Used instead of InsertText.
206+ } ;
189207
190- switch ( completion . ResultType )
208+ return result . ResultType switch
191209 {
192- case CompletionResultType . Command :
193- kind = CompletionItemKind . Function ;
194- break ;
195- case CompletionResultType . History :
196- kind = CompletionItemKind . Reference ;
197- break ;
198- case CompletionResultType . Keyword :
199- case CompletionResultType . DynamicKeyword :
200- kind = CompletionItemKind . Keyword ;
201- break ;
202- case CompletionResultType . Method :
203- kind = CompletionItemKind . Method ;
204- break ;
205- case CompletionResultType . Namespace :
206- kind = CompletionItemKind . Module ;
207- break ;
208- case CompletionResultType . ParameterName :
209- kind = CompletionItemKind . Variable ;
210- // Look for type encoded in the tooltip for parameters and variables.
211- // Display PowerShell type names in [] to be consistent with PowerShell syntax
212- // and how the debugger displays type names.
213- MatchCollection matches = _typeRegex . Matches ( toolTipText ) ;
214- if ( ( matches . Count > 0 ) && ( matches [ 0 ] . Groups . Count > 1 ) )
210+ CompletionResultType . Text => item with { Kind = CompletionItemKind . Text } ,
211+ CompletionResultType . History => item with { Kind = CompletionItemKind . Reference } ,
212+ CompletionResultType . Command => item with { Kind = CompletionItemKind . Function } ,
213+ CompletionResultType . ProviderItem => item with { Kind = CompletionItemKind . File } ,
214+ CompletionResultType . ProviderContainer => TryBuildSnippet ( result . CompletionText , out string snippet )
215+ ? item with
215216 {
216- toolTipText = matches [ 0 ] . Groups [ 1 ] . Value ;
217+ Kind = CompletionItemKind . Folder ,
218+ InsertTextFormat = InsertTextFormat . Snippet ,
219+ TextEdit = textEdit with { NewText = snippet }
217220 }
221+ : item with { Kind = CompletionItemKind . Folder } ,
222+ CompletionResultType . Property => item with { Kind = CompletionItemKind . Property } ,
223+ CompletionResultType . Method => item with { Kind = CompletionItemKind . Method } ,
224+ CompletionResultType . ParameterName => TryExtractType ( detail , out string type )
225+ ? item with { Kind = CompletionItemKind . Variable , Detail = type }
218226 // The comparison operators (-eq, -not, -gt, etc) unfortunately come across as
219227 // ParameterName types but they don't have a type associated to them, so we can
220- // deduce its an operator.
221- else
222- {
223- kind = CompletionItemKind . Operator ;
224- }
225- break ;
226- case CompletionResultType . ParameterValue :
227- kind = CompletionItemKind . Value ;
228- break ;
229- case CompletionResultType . Property :
230- kind = CompletionItemKind . Property ;
231- break ;
232- case CompletionResultType . ProviderContainer :
233- kind = CompletionItemKind . Folder ;
234- // Insert a final "tab stop" as identified by $0 in the snippet provided for
235- // completion. For folder paths, we take the path returned by PowerShell e.g.
236- // 'C:\Program Files' and insert the tab stop marker before the closing quote
237- // char e.g. 'C:\Program Files$0'. This causes the editing cursor to be placed
238- // *before* the final quote after completion, which makes subsequent path
239- // completions work. See this part of the LSP spec for details:
240- // https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
241-
242- // Since we want to use a "tab stop" we need to escape a few things for Textmate
243- // to render properly.
244- if ( EndsWithQuote ( completionText ) )
245- {
246- StringBuilder sb = new StringBuilder ( completionText )
247- . Replace ( @"\" , @"\\" )
248- . Replace ( @"}" , @"\}" )
249- . Replace ( @"$" , @"\$" ) ;
250- completionText = sb . Insert ( sb . Length - 1 , "$0" ) . ToString ( ) ;
251- insertTextFormat = InsertTextFormat . Snippet ;
252- }
253- break ;
254- case CompletionResultType . ProviderItem :
255- kind = CompletionItemKind . File ;
256- break ;
257- case CompletionResultType . Text :
258- kind = CompletionItemKind . Text ;
259- break ;
260- case CompletionResultType . Type :
261- kind = CompletionItemKind . TypeParameter ;
228+ // deduce it is an operator.
229+ : item with { Kind = CompletionItemKind . Operator } ,
230+ CompletionResultType . ParameterValue => item with { Kind = CompletionItemKind . Value } ,
231+ CompletionResultType . Variable => TryExtractType ( detail , out string type )
232+ ? item with { Kind = CompletionItemKind . Variable , Detail = type }
233+ : item with { Kind = CompletionItemKind . Variable } ,
234+ CompletionResultType . Namespace => item with { Kind = CompletionItemKind . Module } ,
235+ CompletionResultType . Type => detail . StartsWith ( "Class " , StringComparison . CurrentCulture )
262236 // Custom classes come through as types but the PowerShell completion tooltip
263237 // will start with "Class ", so we can more accurately display its icon.
264- if ( toolTipText . StartsWith ( "Class " , StringComparison . Ordinal ) )
265- {
266- kind = CompletionItemKind . Class ;
267- }
268- break ;
269- case CompletionResultType . Variable :
270- kind = CompletionItemKind . Variable ;
271- // Look for type encoded in the tooltip for parameters and variables.
272- // Display PowerShell type names in [] to be consistent with PowerShell syntax
273- // and how the debugger displays type names.
274- matches = _typeRegex . Matches ( toolTipText ) ;
275- if ( ( matches . Count > 0 ) && ( matches [ 0 ] . Groups . Count > 1 ) )
276- {
277- toolTipText = matches [ 0 ] . Groups [ 1 ] . Value ;
278- }
279- break ;
280- default :
281- throw new ArgumentOutOfRangeException ( nameof ( completion ) ) ;
282- }
283-
284- // Don't display tooltip if it is the same as the ListItemText.
285- if ( completion . ListItemText . Equals ( toolTipText , StringComparison . OrdinalIgnoreCase ) )
286- {
287- toolTipText = string . Empty ;
288- }
238+ ? item with { Kind = CompletionItemKind . Class }
239+ : item with { Kind = CompletionItemKind . TypeParameter } ,
240+ CompletionResultType . Keyword or CompletionResultType . DynamicKeyword =>
241+ item with { Kind = CompletionItemKind . Keyword } ,
242+ _ => throw new ArgumentOutOfRangeException ( nameof ( result ) )
243+ } ;
244+ }
289245
290- Validate . IsNotNull ( nameof ( CompletionItemKind ) , kind ) ;
246+ private static readonly Regex s_typeRegex = new ( @"^(\[.+\])" , RegexOptions . Compiled ) ;
291247
292- // TODO: We used to extract the symbol type from the tooltip using a regex, but it
293- // wasn't actually used.
294- return new CompletionItem
248+ /// <summary>
249+ /// Look for type encoded in the tooltip for parameters and variables. Display PowerShell
250+ /// type names in [] to be consistent with PowerShell syntax and how the debugger displays
251+ /// type names.
252+ /// </summary>
253+ /// <param name="toolTipText"></param>
254+ /// <param name="type"></param>
255+ /// <returns>Whether or not the type was found.</returns>
256+ private static bool TryExtractType ( string toolTipText , out string type )
257+ {
258+ MatchCollection matches = s_typeRegex . Matches ( toolTipText ) ;
259+ type = string . Empty ;
260+ if ( ( matches . Count > 0 ) && ( matches [ 0 ] . Groups . Count > 1 ) )
295261 {
296- Kind = kind ,
297- TextEdit = new TextEdit
298- {
299- NewText = completionText ,
300- Range = new Range
301- {
302- Start = new Position
303- {
304- Line = completionRange . Start . Line - 1 ,
305- Character = completionRange . Start . Column - 1
306- } ,
307- End = new Position
308- {
309- Line = completionRange . End . Line - 1 ,
310- Character = completionRange . End . Column - 1
311- }
312- }
313- } ,
314- InsertTextFormat = insertTextFormat ,
315- InsertText = completionText ,
316- FilterText = completion . CompletionText ,
317- SortText = sortText ,
318- // TODO: Documentation
319- Detail = toolTipText ,
320- Label = completion . ListItemText ,
321- // TODO: Command
322- } ;
262+ type = matches [ 0 ] . Groups [ 1 ] . Value ;
263+ return true ;
264+ }
265+ return false ;
323266 }
324267
325- private static bool EndsWithQuote ( string text )
268+ /// <summary>
269+ /// Insert a final "tab stop" as identified by $0 in the snippet provided for completion.
270+ /// For folder paths, we take the path returned by PowerShell e.g. 'C:\Program Files' and
271+ /// insert the tab stop marker before the closing quote char e.g. 'C:\Program Files$0'. This
272+ /// causes the editing cursor to be placed *before* the final quote after completion, which
273+ /// makes subsequent path completions work. See this part of the LSP spec for details:
274+ /// https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
275+ /// </summary>
276+ /// <param name="completionText"></param>
277+ /// <param name="snippet"></param>
278+ /// <returns>
279+ /// Whether or not the completion ended with a quote and so was a snippet.
280+ /// </returns>
281+ private static bool TryBuildSnippet ( string completionText , out string snippet )
326282 {
327- return ! string . IsNullOrEmpty ( text ) && text [ text . Length - 1 ] is '"' or '\' ' ;
283+ snippet = string . Empty ;
284+ if ( ! string . IsNullOrEmpty ( completionText )
285+ && completionText [ completionText . Length - 1 ] is '"' or '\' ' )
286+ {
287+ // Since we want to use a "tab stop" we need to escape a few things.
288+ StringBuilder sb = new StringBuilder ( completionText )
289+ . Replace ( @"\" , @"\\" )
290+ . Replace ( @"}" , @"\}" )
291+ . Replace ( @"$" , @"\$" ) ;
292+ snippet = sb . Insert ( sb . Length - 1 , "$0" ) . ToString ( ) ;
293+ return true ;
294+ }
295+ return false ;
328296 }
329297 }
330298}
0 commit comments