@@ -44,13 +44,14 @@ public class GitCommitCompleter : ArgumentCompleter
4444
4545 private void CompleteGitCommitHash ()
4646 {
47- var user = GetFakeBoundParameter <string >(" User" );
48- foreach (var commit in GitExe .Log ())
47+ var user = GetBoundParameterOrDefault <string >(" User" , defaultValue : null );
48+ foreach (var commit in GitExe .Log ())
4949 {
50- if (user is { } u && commit .User != u ){
50+ if (user is { } u && commit .User != u )
51+ {
5152 continue ;
5253 }
53- CompleteMatching (text : commit .Hash , tooltip : $" {commit .User }\r\n {commit .Description }} " : CompletionMatch .AnyContainsWithWordToComplete );
54+ CompleteMatching (text : commit .Hash , toolTip : $" {commit .User }\r\n {commit .Description }" , completionMatch : CompletionMatch .AnyContainsWordToComplete );
5455 }
5556 }
5657}
@@ -59,30 +60,7 @@ public class GitCommitCompleter : ArgumentCompleter
5960
6061## Specification
6162
62- ``` CSharp
63- using System .Collections ;
64- using System .Collections .Generic ;
65- using System .Diagnostics .CodeAnalysis ;
66- using System .Management .Automation .Language ;
67-
68- namespace System .Management .Automation
69- {
70-
71- public enum CompletionMatch
72- {
73- TextStartsWithWordToComplete ,
74- TextContainsWordToComplete ,
75- AnyStartsWithWordToComplete ,
76- AnyContainsWordToComplete ,
77- }
78-
79- public enum CompletionResultSortKind
80- {
81- None ,
82- PreferStartsWithWordToComplete ,
83- Sorted
84- }
85-
63+ ``` csharp
8664 /// <summary >
8765 /// Base class for writing custom Argument Completers
8866 /// </summary >
@@ -94,6 +72,7 @@ namespace System.Management.Automation
9472 private IDictionary ? _fakeBoundParameters ;
9573 private string ? _wordToComplete ;
9674 private CommandAst ? _commandAst ;
75+ private string _quote ;
9776
9877 protected ArgumentCompleter (StringComparison stringComparison = StringComparison .CurrentCultureIgnoreCase ) : this (01 , stringComparison )
9978 {
@@ -106,6 +85,8 @@ namespace System.Management.Automation
10685 {
10786 _results = new List <CompletionResult >(capacity : capacity );
10887 }
88+
89+ _quote = string .Empty ;
10990 }
11091 private List <CompletionResult > Results => _results ??= new List <CompletionResult >();
11192
@@ -115,6 +96,7 @@ namespace System.Management.Automation
11596 string wordToComplete , CommandAst commandAst , IDictionary fakeBoundParameters )
11697 {
11798 _fakeBoundParameters = fakeBoundParameters ;
99+ _quote = CompletionCompleters .HandleDoubleAndSingleQuote (ref wordToComplete );
118100 WordToComplete = wordToComplete ;
119101 CommandAst = commandAst ;
120102 var sortKind = AddCompletionsFor (commandName : commandName , parameterName : parameterName , fakeBoundParameters : fakeBoundParameters );
@@ -149,11 +131,12 @@ namespace System.Management.Automation
149131 /// <param name =" listItemText" >the text to be displayed in a list</param >
150132 /// <param name =" toolTip" >the text for the tooltip with details to be displayed about the object</param >
151133 /// <param name =" resultType" >the type of completion result</param >
152- public void Complete (string text , string ? listItemText = null , string ? toolTip = null , CompletionResultType resultType = CompletionResultType .ParameterValue )
134+ /// <param name =" isGlobbingPath" ><see langword =" true" /> if the parameter to complete is a globbing path. This escapes '[' and ']'.</param >
135+ public void Complete (string text , string ? listItemText = null , string ? toolTip = null , CompletionResultType resultType = CompletionResultType .ParameterValue , bool isGlobbingPath = false )
153136 {
154137 if (text == null ) throw new ArgumentNullException (nameof (text ));
155138
156- var quotedText = QuoteCompletionText (text : text );
139+ var quotedText = QuoteCompletionText (completionText : text , isGlobbingPath );
157140 var completionResult = new CompletionResult (completionText : quotedText , listItemText ?? text ,
158141 resultType : resultType , toolTip ?? text );
159142 Results .Add (item : completionResult );
@@ -255,9 +238,49 @@ namespace System.Management.Automation
255238 /// <summary >
256239 /// If necessary, puts quotation marks around the completion text
257240 /// </summary >
258- /// <param name =" text" >The text to complete</param >
259- /// <returns ></returns >
260- protected virtual string QuoteCompletionText (string text ) => text .Contains (" " ) ? $@" "" {text }"" " : text ;
241+ /// <param name =" completionText" >The text to complete</param >
242+ /// <param name =" isGlobbingPath" ><see langword =" true" /> if the characters [ and ] should be escaped.</param >
243+ /// <returns >A quoted string, if quoting was necessary. Otherwise <see param =" completionText" />.</returns >
244+ protected virtual string QuoteCompletionText (string completionText , bool isGlobbingPath = false )
245+ {
246+ if (CompletionCompleters .CompletionRequiresQuotes (completionText , isGlobbingPath ))
247+ {
248+ var quoteInUse = _quote == string .Empty ? " '" : _quote ;
249+ if (quoteInUse == " '" )
250+ {
251+ completionText = completionText .Replace (" '" , " ''" );
252+ }
253+ else
254+ {
255+ // When double quote is in use, we have to escape the backtip and '$' even when using literal path
256+ // Get-Content -LiteralPath ".\a``g.txt"
257+ completionText = completionText .Replace (" `" , " ``" );
258+ completionText = completionText .Replace (" $" , " `$" );
259+ }
260+
261+ if (isGlobbingPath )
262+ {
263+ if (quoteInUse == " '" )
264+ {
265+ completionText = completionText .Replace (" [" , " `[" );
266+ completionText = completionText .Replace (" ]" , " `]" );
267+ }
268+ else
269+ {
270+ completionText = completionText .Replace (" [" , " ``[" );
271+ completionText = completionText .Replace (" ]" , " ``]" );
272+ }
273+ }
274+
275+ completionText = quoteInUse + completionText + quoteInUse ;
276+ }
277+ else if (_quote != string .Empty )
278+ {
279+ completionText = _quote + completionText + _quote ;
280+ }
281+
282+ return completionText ;
283+ }
261284
262285 /// <summary >
263286 /// Predicate to test if a string starts with <see cref =" WordToComplete" />
@@ -340,7 +363,79 @@ namespace System.Management.Automation
340363 private set => _commandAst = value ;
341364 }
342365 }
343- }
366+
367+ internal class CompletionCompleters
368+ {
369+ /// <summary >
370+ /// Determines what the <see param =" wordToComplete" /> is without quotes, and
371+ /// what quote character, if any, is uses
372+ /// </summary >
373+ /// <returns >The quote character, ' or ", or the empty string.</returns >
374+ internal static string HandleDoubleAndSingleQuote (ref string wordToComplete )
375+ {
376+ string quote = string .Empty ;
377+
378+ if (! string .IsNullOrEmpty (wordToComplete ) && (wordToComplete [0 ].IsSingleQuote () || wordToComplete [0 ].IsDoubleQuote ()))
379+ {
380+ char frontQuote = wordToComplete [0 ];
381+ int length = wordToComplete .Length ;
382+
383+ if (length == 1 )
384+ {
385+ wordToComplete = string .Empty ;
386+ quote = frontQuote .IsSingleQuote () ? " '" : " \" " ;
387+ }
388+ else if (length > 1 )
389+ {
390+ if ((wordToComplete [length - 1 ].IsDoubleQuote () && frontQuote .IsDoubleQuote ()) || (wordToComplete [length - 1 ].IsSingleQuote () && frontQuote .IsSingleQuote ()))
391+ {
392+ wordToComplete = wordToComplete .Substring (1 , length - 2 );
393+ quote = frontQuote .IsSingleQuote () ? " '" : " \" " ;
394+ }
395+ else if (! wordToComplete [length - 1 ].IsDoubleQuote () && ! wordToComplete [length - 1 ].IsSingleQuote ())
396+ {
397+ wordToComplete = wordToComplete .Substring (1 );
398+ quote = frontQuote .IsSingleQuote () ? " '" : " \" " ;
399+ }
400+ }
401+ }
402+
403+ return quote ;
404+ }
405+
406+
407+ /// <summary >
408+ /// Determines if the item to complete requires quotes
409+ /// </summary >
410+ internal static bool CompletionRequiresQuotes (string completion , bool escape )
411+ {
412+ // If the tokenizer sees the completion as more than two tokens, or if there is some error, then
413+ // some form of quoting is necessary (if it's a variable, we'd need ${}, filenames would need [], etc.)
414+
415+ Parser .ParseInput (completion , out Token [] tokens , out ParseError [] errors );
416+
417+ ReadOnlySpan < char > charToCheck = escape ? stackalloc char [] { '$' , '[' , ']' , '`' } : stackalloc char [] { '$' , '`' };
418+
419+ // Expect no errors and 2 tokens (1 is for our completion, the other is eof)
420+ // Or if the completion is a keyword, we ignore the errors
421+ bool requireQuote = ! (errors .Length == 0 && tokens .Length == 2 );
422+ if ((! requireQuote && tokens [0 ] is StringToken ) ||
423+ (tokens .Length == 2 && (tokens [0 ].TokenFlags & TokenFlags .Keyword ) != 0 ))
424+ {
425+ requireQuote = false ;
426+ var value = tokens [0 ].Text .AsSpan ();
427+ if (value .IndexOfAny (charToCheck ) != - 1 )
428+ requireQuote = true ;
429+ }
430+
431+ return requireQuote ;
432+ }
433+ }
434+
435+ internal static class CharExtensions {
436+ public static bool IsSingleQuote (this char c ) => c == '\' ' ;
437+ public static bool IsDoubleQuote (this char c ) => c == '\" ' ;
438+ }
344439
345440```
346441
0 commit comments