@optique/core 1.0.15-dev.2824 → 1.0.15-dev.2849
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/completion.cjs +130 -32
- package/dist/completion.js +130 -32
- package/package.json +1 -1
package/dist/completion.cjs
CHANGED
|
@@ -62,6 +62,17 @@ function sanitizeForTransport(text) {
|
|
|
62
62
|
return text.replace(/[\t\n\r\0]/g, " ");
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
|
+
* Escapes literal values that would otherwise be interpreted as transport
|
|
66
|
+
* directives. Escape the escape prefix itself so decoding is unambiguous.
|
|
67
|
+
* @param text The literal suggestion text.
|
|
68
|
+
* @returns The sanitized and, if necessary, escaped literal text.
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
function encodeLiteral(text) {
|
|
72
|
+
const sanitized = sanitizeForTransport(text);
|
|
73
|
+
return sanitized.startsWith("__FILE__:") || sanitized.startsWith("__LITERAL__:") ? `__LITERAL__:${sanitized}` : sanitized;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
65
76
|
* The Bash shell completion generator.
|
|
66
77
|
* @since 0.6.0
|
|
67
78
|
*/
|
|
@@ -308,10 +319,11 @@ function _${programName} () {
|
|
|
308
319
|
__reply_prefix=""
|
|
309
320
|
__file_current="$current"
|
|
310
321
|
__literal=0
|
|
311
|
-
|
|
322
|
+
# Protocol tags are case-sensitive even when nocasematch is enabled.
|
|
323
|
+
if [ "\${line:0:12}" = '__LITERAL__:' ]; then
|
|
312
324
|
line="\${line#__LITERAL__:}"
|
|
313
325
|
__literal=1
|
|
314
|
-
elif [
|
|
326
|
+
elif [ "\${line:0:16}" = '__JOINED_FILE__:' ]; then
|
|
315
327
|
line="\${line#__JOINED_FILE__:}"
|
|
316
328
|
__reply_prefix="\${line%%:*}"
|
|
317
329
|
__reply_prefix="\${__reply_prefix//%3A/:}"
|
|
@@ -332,7 +344,7 @@ function _${programName} () {
|
|
|
332
344
|
__file_quote_offset=$((__file_quote_offset - \${#__reply_prefix}))
|
|
333
345
|
fi
|
|
334
346
|
fi
|
|
335
|
-
if [
|
|
347
|
+
if [ "$__literal" = 0 ] && [ "\${line:0:9}" = '__FILE__:' ]; then
|
|
336
348
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
337
349
|
IFS=':' read -r _ type extensions pattern hidden <<< "$line"
|
|
338
350
|
extensions="\${extensions//,\\./,}"; extensions="\${extensions#.}"
|
|
@@ -700,7 +712,12 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
700
712
|
desc=\${line#*\$'\\0'}
|
|
701
713
|
desc=\${desc%%\$'\\0'*}
|
|
702
714
|
|
|
703
|
-
|
|
715
|
+
local __literal=0
|
|
716
|
+
if [[ "\$value" == __LITERAL__:* ]]; then
|
|
717
|
+
value="\${value#__LITERAL__:}"
|
|
718
|
+
__literal=1
|
|
719
|
+
fi
|
|
720
|
+
if [[ \$__literal == 0 && "\$value" == __FILE__:* ]]; then
|
|
704
721
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
705
722
|
local type extensions pattern hidden
|
|
706
723
|
IFS=':' read -r _ type extensions pattern hidden <<< "\$value"
|
|
@@ -798,10 +815,13 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
798
815
|
local -a matches
|
|
799
816
|
local -i i
|
|
800
817
|
for (( i=1; i <= \${#completions[@]}; i++ )); do
|
|
818
|
+
# _describe treats colons as separators and backslashes as escapes.
|
|
819
|
+
value="\${completions[i]//\\\\/\\\\\\\\}"
|
|
820
|
+
value="\${value//:/\\\\:}"
|
|
801
821
|
if [[ -n "\${descriptions[i]}" ]]; then
|
|
802
|
-
matches+=("\${
|
|
822
|
+
matches+=("\${value}:\${descriptions[i]}")
|
|
803
823
|
else
|
|
804
|
-
matches+=("\${
|
|
824
|
+
matches+=("\${value}")
|
|
805
825
|
fi
|
|
806
826
|
done
|
|
807
827
|
_describe 'commands' matches
|
|
@@ -816,7 +836,7 @@ fi
|
|
|
816
836
|
*encodeSuggestions(suggestions) {
|
|
817
837
|
for (const suggestion of suggestions) if (suggestion.kind === "literal") {
|
|
818
838
|
const description = suggestion.description == null ? "" : sanitizeForTransport(require_message.formatMessage(suggestion.description, { colors: false }));
|
|
819
|
-
yield `${
|
|
839
|
+
yield `${encodeLiteral(suggestion.text)}\0${description}\0`;
|
|
820
840
|
} else {
|
|
821
841
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
822
842
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
@@ -875,7 +895,12 @@ function ${functionName}
|
|
|
875
895
|
${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current 2>/dev/null)\n` : ` set -l output (${programName} $prev $current 2>/dev/null)\n`}
|
|
876
896
|
# Process each line of output
|
|
877
897
|
for line in $output
|
|
878
|
-
|
|
898
|
+
set -l literal 0
|
|
899
|
+
if string match -q '__LITERAL__:*' -- $line
|
|
900
|
+
set line (string sub -s 13 -- $line)
|
|
901
|
+
set literal 1
|
|
902
|
+
end
|
|
903
|
+
if test $literal -eq 0; and string match -q '__FILE__:*' -- $line
|
|
879
904
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
880
905
|
set -l directive (string split \\t -- $line)[1]
|
|
881
906
|
set -l parts (string split ':' -- $directive)
|
|
@@ -1166,7 +1191,7 @@ complete -c ${programName} -f -a '(${functionName})'
|
|
|
1166
1191
|
if (i > 0) yield "\n";
|
|
1167
1192
|
if (suggestion.kind === "literal") {
|
|
1168
1193
|
const description = suggestion.description == null ? "" : sanitizeForTransport(require_message.formatMessage(suggestion.description, { colors: false }));
|
|
1169
|
-
yield `${
|
|
1194
|
+
yield `${encodeLiteral(suggestion.text)}\t${description}`;
|
|
1170
1195
|
} else {
|
|
1171
1196
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
1172
1197
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
@@ -1405,8 +1430,10 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1405
1430
|
}
|
|
1406
1431
|
|
|
1407
1432
|
# Process each line of output
|
|
1408
|
-
$output | lines | each {|
|
|
1409
|
-
|
|
1433
|
+
$output | lines | each {|raw_line|
|
|
1434
|
+
let literal = ($raw_line | str starts-with '__LITERAL__:')
|
|
1435
|
+
let line = if $literal { $raw_line | str substring 12.. } else { $raw_line }
|
|
1436
|
+
if (not $literal) and ($line | str starts-with '__FILE__:') {
|
|
1410
1437
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
1411
1438
|
let directive = ($line | split row "\t" | first)
|
|
1412
1439
|
let parts = ($directive | split row ':')
|
|
@@ -1466,22 +1493,26 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1466
1493
|
# Generate file completions based on type.
|
|
1467
1494
|
# When the glob base contains wildcard characters, use it as-is;
|
|
1468
1495
|
# otherwise append * to treat it as a prefix.
|
|
1469
|
-
# Note: into glob is required so that ls expands wildcards from a variable
|
|
1496
|
+
# Note: into glob is required so that ls expands wildcards from a variable.
|
|
1497
|
+
# A literal prefix has its brackets wrapped as [[] and []] so that path
|
|
1498
|
+
# text like "report[1]" is not parsed as a glob character class; a
|
|
1499
|
+
# wildcard pattern is a glob as a whole, so its brackets stay classes.
|
|
1470
1500
|
let has_glob = ($glob_base =~ '[*?]')
|
|
1471
1501
|
let ls_pattern = if ($glob_base | is-empty) {
|
|
1472
1502
|
"."
|
|
1473
1503
|
} else if $has_glob {
|
|
1474
1504
|
($glob_base | into glob)
|
|
1475
1505
|
} else {
|
|
1476
|
-
($glob_base + "*" | into glob)
|
|
1506
|
+
(($glob_base | str replace -a -r '([\\[\\]])' '[$1]') + "*" | into glob)
|
|
1477
1507
|
}
|
|
1478
1508
|
|
|
1479
1509
|
# When using a glob pattern, also compute a directory listing pattern
|
|
1480
|
-
# so that file/any modes can include directories for navigation
|
|
1510
|
+
# so that file/any modes can include directories for navigation; its
|
|
1511
|
+
# directory part is a literal path
|
|
1481
1512
|
let glob_dir_pattern = if $has_glob {
|
|
1482
1513
|
let dir_part = ($glob_base | path dirname)
|
|
1483
1514
|
let dir = if ($dir_part | is-empty) or ($dir_part == $glob_base) { "." } else { $dir_part }
|
|
1484
|
-
($dir + "/*" | into glob)
|
|
1515
|
+
(($dir | str replace -a -r '([\\[\\]])' '[$1]') + "/*" | into glob)
|
|
1485
1516
|
} else {
|
|
1486
1517
|
null
|
|
1487
1518
|
}
|
|
@@ -1674,7 +1705,7 @@ ${functionName}-external
|
|
|
1674
1705
|
if (i > 0) yield "\n";
|
|
1675
1706
|
if (suggestion.kind === "literal") {
|
|
1676
1707
|
const description = suggestion.description == null ? "" : sanitizeForTransport(require_message.formatMessage(suggestion.description, { colors: false }));
|
|
1677
|
-
yield `${
|
|
1708
|
+
yield `${encodeLiteral(suggestion.text)}\t${description}`;
|
|
1678
1709
|
} else {
|
|
1679
1710
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
1680
1711
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
@@ -1702,12 +1733,33 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
|
1702
1733
|
# Extract arguments from AST (handles quoted strings properly)
|
|
1703
1734
|
\$arguments = @()
|
|
1704
1735
|
\$commandElements = \$commandAst.CommandElements
|
|
1736
|
+
\$valuePrefix = \$wordToComplete
|
|
1737
|
+
\$quoteType = \$null
|
|
1705
1738
|
|
|
1706
1739
|
# Keep only complete arguments before the cursor. The current word
|
|
1707
1740
|
# (including a token ending exactly at the cursor) is appended below.
|
|
1708
1741
|
for (\$i = 1; \$i -lt \$commandElements.Count; \$i++) {
|
|
1709
1742
|
\$element = \$commandElements[\$i]
|
|
1710
|
-
if (\$element.Extent.EndOffset -ge \$cursorPosition) {
|
|
1743
|
+
if (\$element.Extent.EndOffset -ge \$cursorPosition) {
|
|
1744
|
+
# wordToComplete has synthesized delimiters and decoded escapes.
|
|
1745
|
+
# Only the AST retains the literal value and original quote type.
|
|
1746
|
+
if (\$element -is [System.Management.Automation.Language.StringConstantExpressionAst] -and
|
|
1747
|
+
\$element.Extent.EndOffset -eq \$cursorPosition) {
|
|
1748
|
+
if (\$element.StringConstantType -in @('SingleQuoted', 'DoubleQuoted')) {
|
|
1749
|
+
\$valuePrefix = \$element.Value
|
|
1750
|
+
\$quoteType = \$element.StringConstantType
|
|
1751
|
+
} elseif (\$element.Extent.Text -match "^[^=]+=['\\u2018-\\u201b]") {
|
|
1752
|
+
# Joined values are BareWord constants despite their quotes.
|
|
1753
|
+
# Quote the entire replacement to keep it one argument.
|
|
1754
|
+
\$valuePrefix = \$element.Value
|
|
1755
|
+
\$quoteType = 'SingleQuoted'
|
|
1756
|
+
} elseif (\$element.Extent.Text -match '^[^=]+=["\\u201c-\\u201e]') {
|
|
1757
|
+
\$valuePrefix = \$element.Value
|
|
1758
|
+
\$quoteType = 'DoubleQuoted'
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
break
|
|
1762
|
+
}
|
|
1711
1763
|
|
|
1712
1764
|
if (\$element -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
|
|
1713
1765
|
\$arguments += \$element.Value
|
|
@@ -1716,23 +1768,41 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
|
1716
1768
|
}
|
|
1717
1769
|
}
|
|
1718
1770
|
|
|
1771
|
+
# CompletionText replaces the whole token, including its opening quote.
|
|
1772
|
+
# Escape literals without evaluating variables or expressions.
|
|
1773
|
+
\$quoteCompletion = {
|
|
1774
|
+
param(\$text)
|
|
1775
|
+
if (\$quoteType -eq 'SingleQuoted') {
|
|
1776
|
+
"'" + (\$text -replace "['\\u2018-\\u201b]", '\$0\$0') + "'"
|
|
1777
|
+
} elseif (\$quoteType -eq 'DoubleQuoted') {
|
|
1778
|
+
'"' + (\$text -replace '[\\x60\$"\\u201c-\\u201e]', '\`\$0') + '"'
|
|
1779
|
+
} else {
|
|
1780
|
+
\$text
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1719
1784
|
# Build arguments array for completion command
|
|
1720
1785
|
\$completionArgs = @()
|
|
1721
1786
|
${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
1722
1787
|
` : ""} \$completionArgs += \$arguments
|
|
1723
|
-
\$completionArgs += \$wordToComplete
|
|
1724
1788
|
|
|
1725
1789
|
# Call completion command and capture output
|
|
1726
1790
|
try {
|
|
1727
|
-
|
|
1791
|
+
# Quote the prefix separately: array arguments expand a literal tilde
|
|
1792
|
+
# when passed to native programs on Unix.
|
|
1793
|
+
\$output = & ${programName} \$completionArgs "\$valuePrefix" 2>\$null
|
|
1728
1794
|
if (-not \$output) { return }
|
|
1729
1795
|
|
|
1730
1796
|
# Parse tab-separated output and create CompletionResult objects
|
|
1731
1797
|
\$output -split "\`n" | ForEach-Object {
|
|
1732
|
-
\$
|
|
1798
|
+
\$literal = \$_.StartsWith('__LITERAL__:', [System.StringComparison]::Ordinal)
|
|
1799
|
+
# Quoted values preserve whitespace; only remove the CR of CRLF.
|
|
1800
|
+
# Keep the existing trimming for unquoted completions.
|
|
1801
|
+
\$line = if (\$null -ne \$quoteType) { \$_.TrimEnd([char]13) } else { \$_.Trim() }
|
|
1733
1802
|
if (-not \$line) { return }
|
|
1803
|
+
if (\$literal) { \$line = \$line.Substring(12) }
|
|
1734
1804
|
|
|
1735
|
-
if (\$
|
|
1805
|
+
if (-not \$literal -and \$_.StartsWith('__FILE__:', [System.StringComparison]::Ordinal)) {
|
|
1736
1806
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
1737
1807
|
\$directive = (\$line -split "\`t")[0]
|
|
1738
1808
|
\$parts = \$directive -split ':', 5
|
|
@@ -1748,7 +1818,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1748
1818
|
# comparing so that Windows backslashes match forward slashes
|
|
1749
1819
|
# in the transported pattern.
|
|
1750
1820
|
\$normalizedPattern = if (\$pattern) { \$pattern.Replace('\\', '/') -replace '^\\./','' } else { '' }
|
|
1751
|
-
\$normalizedWord = if (\$
|
|
1821
|
+
\$normalizedWord = if (\$valuePrefix) { \$valuePrefix.Replace('\\', '/') -replace '^\\./','' } else { '' }
|
|
1752
1822
|
\$comparison = if (\$IsWindows) { [System.StringComparison]::OrdinalIgnoreCase } else { [System.StringComparison]::Ordinal }
|
|
1753
1823
|
# For wildcard patterns, compare only the directory prefix
|
|
1754
1824
|
\$comparePattern = \$normalizedPattern
|
|
@@ -1760,12 +1830,25 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1760
1830
|
\$comparePattern = ''
|
|
1761
1831
|
}
|
|
1762
1832
|
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1833
|
+
# A quoted home-pattern directory inserts an absolute path.
|
|
1834
|
+
# Compare subsequent input against the same expanded prefix.
|
|
1835
|
+
# Detect home syntax before removing ./, which can target a
|
|
1836
|
+
# literal local tilde directory rather than the home directory.
|
|
1837
|
+
\$homePattern = \$pattern -and (\$pattern -eq '~' -or \$pattern.Replace('\\', '/').StartsWith('~/'))
|
|
1838
|
+
if (\$homePattern) {
|
|
1839
|
+
# Preserve the home root when the pattern is exactly ~.
|
|
1840
|
+
\$expandedPattern = if (\$comparePattern -eq '~') { \$HOME } else { \$HOME.Replace('\\', '/').TrimEnd('/') + \$comparePattern.Substring(1) }
|
|
1841
|
+
\$expandedPattern = [IO.Path]::GetFullPath(\$expandedPattern).Replace('\\', '/')
|
|
1842
|
+
if (\$normalizedWord.StartsWith(\$expandedPattern, \$comparison)) {
|
|
1843
|
+
\$comparePattern = \$expandedPattern
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
\$prefix = if ((\$comparePattern -or \$normalizedWord) -and \$normalizedWord -and \$normalizedWord.StartsWith(\$comparePattern, \$comparison) -and \$normalizedWord.Length -ge \$comparePattern.Length -and \$valuePrefix -ne \$pattern) {
|
|
1847
|
+
\$valuePrefix
|
|
1765
1848
|
} elseif (\$pattern) {
|
|
1766
1849
|
\$pattern
|
|
1767
|
-
} elseif (\$
|
|
1768
|
-
\$
|
|
1850
|
+
} elseif (\$valuePrefix) {
|
|
1851
|
+
\$valuePrefix
|
|
1769
1852
|
} else { '' }
|
|
1770
1853
|
|
|
1771
1854
|
# Use -Force to include hidden files when requested, or when
|
|
@@ -1773,15 +1856,22 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1773
1856
|
\$prefixBasename = if (\$prefix) { Split-Path -Leaf \$prefix } else { '' }
|
|
1774
1857
|
\$forceParam = if (\$hidden -or (\$prefixBasename -and \$prefixBasename.StartsWith('.'))) { @{Force = \$true} } else { @{} }
|
|
1775
1858
|
|
|
1859
|
+
# A quoted tilde is literal when passed to a native command.
|
|
1860
|
+
# Prevent PowerShell's file provider from expanding it to home.
|
|
1861
|
+
# Parser-provided home patterns still need home expansion.
|
|
1862
|
+
\$prefixFromInput = \$valuePrefix -and \$prefix -ceq \$valuePrefix
|
|
1863
|
+
\$lookupPrefix = if (\$prefixFromInput -and \$null -ne \$quoteType -and \$prefix.StartsWith('~')) { './' + \$prefix } else { \$prefix }
|
|
1864
|
+
|
|
1776
1865
|
# If prefix is a directory without trailing slash, append
|
|
1777
1866
|
# one so Get-ChildItem lists its contents
|
|
1778
|
-
if (\$prefix -and (Test-Path -Path \$
|
|
1867
|
+
if (\$prefix -and (Test-Path -Path \$lookupPrefix -PathType Container) -and -not \$prefix.EndsWith('/') -and -not \$prefix.EndsWith('\\')) {
|
|
1779
1868
|
\$prefix = \$prefix + '/'
|
|
1869
|
+
\$lookupPrefix = \$lookupPrefix + '/'
|
|
1780
1870
|
}
|
|
1781
1871
|
|
|
1782
1872
|
# Build the glob path — when the prefix contains wildcard
|
|
1783
1873
|
# characters, use it as-is; otherwise append *
|
|
1784
|
-
\$globPath = if (\$
|
|
1874
|
+
\$globPath = if (\$lookupPrefix -match '[\*\?]') { \$lookupPrefix } else { "\${lookupPrefix}*" }
|
|
1785
1875
|
|
|
1786
1876
|
# Get file system items based on type
|
|
1787
1877
|
\$items = @()
|
|
@@ -1818,7 +1908,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1818
1908
|
# For file/any modes with glob patterns, also add directories
|
|
1819
1909
|
# from the base directory for navigation
|
|
1820
1910
|
if (\$prefix -match '[\*\?]' -and \$type -ne 'directory') {
|
|
1821
|
-
\$globDir = Split-Path -Parent \$
|
|
1911
|
+
\$globDir = Split-Path -Parent \$lookupPrefix
|
|
1822
1912
|
if (-not \$globDir) { \$globDir = '.' }
|
|
1823
1913
|
\$extraDirs = Get-ChildItem @forceParam -Directory -Path "\$globDir/*" -ErrorAction SilentlyContinue
|
|
1824
1914
|
if (\$extraDirs) { \$items = @(\$items) + @(\$extraDirs) | Select-Object -Unique }
|
|
@@ -1839,8 +1929,13 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1839
1929
|
\$items | ForEach-Object {
|
|
1840
1930
|
\$completionText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)/" } else { "\$dirPrefix\$(\$_.Name)" }
|
|
1841
1931
|
\$itemType = if (\$_.PSIsContainer) { 'Directory' } else { 'File' }
|
|
1932
|
+
# Quoting disables native tilde expansion. A home pattern
|
|
1933
|
+
# supplied by the parser must insert the discovered path.
|
|
1934
|
+
\$replacementText = if (\$null -ne \$quoteType -and -not \$prefixFromInput -and \$homePattern) {
|
|
1935
|
+
if (\$_.PSIsContainer) { \$_.FullName + '/' } else { \$_.FullName }
|
|
1936
|
+
} else { \$completionText }
|
|
1842
1937
|
[System.Management.Automation.CompletionResult]::new(
|
|
1843
|
-
\$
|
|
1938
|
+
(& \$quoteCompletion \$replacementText),
|
|
1844
1939
|
\$completionText,
|
|
1845
1940
|
'ParameterValue',
|
|
1846
1941
|
\$itemType
|
|
@@ -1850,11 +1945,14 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1850
1945
|
# Parse literal completion: text\\tlistItemText\\tdescription
|
|
1851
1946
|
\$parts = \$line -split "\`t", 3
|
|
1852
1947
|
\$completionText = \$parts[0]
|
|
1948
|
+
# Empty display text is invalid for CompletionResult. Keep
|
|
1949
|
+
# ignoring empty suggestions without dropping later records.
|
|
1950
|
+
if (-not \$completionText) { return }
|
|
1853
1951
|
\$listItemText = if (\$parts.Length -gt 1 -and \$parts[1]) { \$parts[1] } else { \$completionText }
|
|
1854
1952
|
\$toolTip = if (\$parts.Length -gt 2 -and \$parts[2]) { \$parts[2] } else { \$completionText }
|
|
1855
1953
|
|
|
1856
1954
|
[System.Management.Automation.CompletionResult]::new(
|
|
1857
|
-
\$completionText,
|
|
1955
|
+
(& \$quoteCompletion \$completionText),
|
|
1858
1956
|
\$listItemText,
|
|
1859
1957
|
'ParameterValue',
|
|
1860
1958
|
\$toolTip
|
|
@@ -1874,7 +1972,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1874
1972
|
if (suggestion.kind === "literal") {
|
|
1875
1973
|
const description = suggestion.description == null ? "" : sanitizeForTransport(require_message.formatMessage(suggestion.description, { colors: false }));
|
|
1876
1974
|
const text = sanitizeForTransport(suggestion.text);
|
|
1877
|
-
yield `${text}\t${text}\t${description}`;
|
|
1975
|
+
yield `${encodeLiteral(suggestion.text)}\t${text}\t${description}`;
|
|
1878
1976
|
} else {
|
|
1879
1977
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
1880
1978
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
package/dist/completion.js
CHANGED
|
@@ -62,6 +62,17 @@ function sanitizeForTransport(text) {
|
|
|
62
62
|
return text.replace(/[\t\n\r\0]/g, " ");
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
|
+
* Escapes literal values that would otherwise be interpreted as transport
|
|
66
|
+
* directives. Escape the escape prefix itself so decoding is unambiguous.
|
|
67
|
+
* @param text The literal suggestion text.
|
|
68
|
+
* @returns The sanitized and, if necessary, escaped literal text.
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
function encodeLiteral(text) {
|
|
72
|
+
const sanitized = sanitizeForTransport(text);
|
|
73
|
+
return sanitized.startsWith("__FILE__:") || sanitized.startsWith("__LITERAL__:") ? `__LITERAL__:${sanitized}` : sanitized;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
65
76
|
* The Bash shell completion generator.
|
|
66
77
|
* @since 0.6.0
|
|
67
78
|
*/
|
|
@@ -308,10 +319,11 @@ function _${programName} () {
|
|
|
308
319
|
__reply_prefix=""
|
|
309
320
|
__file_current="$current"
|
|
310
321
|
__literal=0
|
|
311
|
-
|
|
322
|
+
# Protocol tags are case-sensitive even when nocasematch is enabled.
|
|
323
|
+
if [ "\${line:0:12}" = '__LITERAL__:' ]; then
|
|
312
324
|
line="\${line#__LITERAL__:}"
|
|
313
325
|
__literal=1
|
|
314
|
-
elif [
|
|
326
|
+
elif [ "\${line:0:16}" = '__JOINED_FILE__:' ]; then
|
|
315
327
|
line="\${line#__JOINED_FILE__:}"
|
|
316
328
|
__reply_prefix="\${line%%:*}"
|
|
317
329
|
__reply_prefix="\${__reply_prefix//%3A/:}"
|
|
@@ -332,7 +344,7 @@ function _${programName} () {
|
|
|
332
344
|
__file_quote_offset=$((__file_quote_offset - \${#__reply_prefix}))
|
|
333
345
|
fi
|
|
334
346
|
fi
|
|
335
|
-
if [
|
|
347
|
+
if [ "$__literal" = 0 ] && [ "\${line:0:9}" = '__FILE__:' ]; then
|
|
336
348
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
337
349
|
IFS=':' read -r _ type extensions pattern hidden <<< "$line"
|
|
338
350
|
extensions="\${extensions//,\\./,}"; extensions="\${extensions#.}"
|
|
@@ -700,7 +712,12 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
700
712
|
desc=\${line#*\$'\\0'}
|
|
701
713
|
desc=\${desc%%\$'\\0'*}
|
|
702
714
|
|
|
703
|
-
|
|
715
|
+
local __literal=0
|
|
716
|
+
if [[ "\$value" == __LITERAL__:* ]]; then
|
|
717
|
+
value="\${value#__LITERAL__:}"
|
|
718
|
+
__literal=1
|
|
719
|
+
fi
|
|
720
|
+
if [[ \$__literal == 0 && "\$value" == __FILE__:* ]]; then
|
|
704
721
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
705
722
|
local type extensions pattern hidden
|
|
706
723
|
IFS=':' read -r _ type extensions pattern hidden <<< "\$value"
|
|
@@ -798,10 +815,13 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
798
815
|
local -a matches
|
|
799
816
|
local -i i
|
|
800
817
|
for (( i=1; i <= \${#completions[@]}; i++ )); do
|
|
818
|
+
# _describe treats colons as separators and backslashes as escapes.
|
|
819
|
+
value="\${completions[i]//\\\\/\\\\\\\\}"
|
|
820
|
+
value="\${value//:/\\\\:}"
|
|
801
821
|
if [[ -n "\${descriptions[i]}" ]]; then
|
|
802
|
-
matches+=("\${
|
|
822
|
+
matches+=("\${value}:\${descriptions[i]}")
|
|
803
823
|
else
|
|
804
|
-
matches+=("\${
|
|
824
|
+
matches+=("\${value}")
|
|
805
825
|
fi
|
|
806
826
|
done
|
|
807
827
|
_describe 'commands' matches
|
|
@@ -816,7 +836,7 @@ fi
|
|
|
816
836
|
*encodeSuggestions(suggestions) {
|
|
817
837
|
for (const suggestion of suggestions) if (suggestion.kind === "literal") {
|
|
818
838
|
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
819
|
-
yield `${
|
|
839
|
+
yield `${encodeLiteral(suggestion.text)}\0${description}\0`;
|
|
820
840
|
} else {
|
|
821
841
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
822
842
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
@@ -875,7 +895,12 @@ function ${functionName}
|
|
|
875
895
|
${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current 2>/dev/null)\n` : ` set -l output (${programName} $prev $current 2>/dev/null)\n`}
|
|
876
896
|
# Process each line of output
|
|
877
897
|
for line in $output
|
|
878
|
-
|
|
898
|
+
set -l literal 0
|
|
899
|
+
if string match -q '__LITERAL__:*' -- $line
|
|
900
|
+
set line (string sub -s 13 -- $line)
|
|
901
|
+
set literal 1
|
|
902
|
+
end
|
|
903
|
+
if test $literal -eq 0; and string match -q '__FILE__:*' -- $line
|
|
879
904
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
880
905
|
set -l directive (string split \\t -- $line)[1]
|
|
881
906
|
set -l parts (string split ':' -- $directive)
|
|
@@ -1166,7 +1191,7 @@ complete -c ${programName} -f -a '(${functionName})'
|
|
|
1166
1191
|
if (i > 0) yield "\n";
|
|
1167
1192
|
if (suggestion.kind === "literal") {
|
|
1168
1193
|
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
1169
|
-
yield `${
|
|
1194
|
+
yield `${encodeLiteral(suggestion.text)}\t${description}`;
|
|
1170
1195
|
} else {
|
|
1171
1196
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
1172
1197
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
@@ -1405,8 +1430,10 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1405
1430
|
}
|
|
1406
1431
|
|
|
1407
1432
|
# Process each line of output
|
|
1408
|
-
$output | lines | each {|
|
|
1409
|
-
|
|
1433
|
+
$output | lines | each {|raw_line|
|
|
1434
|
+
let literal = ($raw_line | str starts-with '__LITERAL__:')
|
|
1435
|
+
let line = if $literal { $raw_line | str substring 12.. } else { $raw_line }
|
|
1436
|
+
if (not $literal) and ($line | str starts-with '__FILE__:') {
|
|
1410
1437
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
1411
1438
|
let directive = ($line | split row "\t" | first)
|
|
1412
1439
|
let parts = ($directive | split row ':')
|
|
@@ -1466,22 +1493,26 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1466
1493
|
# Generate file completions based on type.
|
|
1467
1494
|
# When the glob base contains wildcard characters, use it as-is;
|
|
1468
1495
|
# otherwise append * to treat it as a prefix.
|
|
1469
|
-
# Note: into glob is required so that ls expands wildcards from a variable
|
|
1496
|
+
# Note: into glob is required so that ls expands wildcards from a variable.
|
|
1497
|
+
# A literal prefix has its brackets wrapped as [[] and []] so that path
|
|
1498
|
+
# text like "report[1]" is not parsed as a glob character class; a
|
|
1499
|
+
# wildcard pattern is a glob as a whole, so its brackets stay classes.
|
|
1470
1500
|
let has_glob = ($glob_base =~ '[*?]')
|
|
1471
1501
|
let ls_pattern = if ($glob_base | is-empty) {
|
|
1472
1502
|
"."
|
|
1473
1503
|
} else if $has_glob {
|
|
1474
1504
|
($glob_base | into glob)
|
|
1475
1505
|
} else {
|
|
1476
|
-
($glob_base + "*" | into glob)
|
|
1506
|
+
(($glob_base | str replace -a -r '([\\[\\]])' '[$1]') + "*" | into glob)
|
|
1477
1507
|
}
|
|
1478
1508
|
|
|
1479
1509
|
# When using a glob pattern, also compute a directory listing pattern
|
|
1480
|
-
# so that file/any modes can include directories for navigation
|
|
1510
|
+
# so that file/any modes can include directories for navigation; its
|
|
1511
|
+
# directory part is a literal path
|
|
1481
1512
|
let glob_dir_pattern = if $has_glob {
|
|
1482
1513
|
let dir_part = ($glob_base | path dirname)
|
|
1483
1514
|
let dir = if ($dir_part | is-empty) or ($dir_part == $glob_base) { "." } else { $dir_part }
|
|
1484
|
-
($dir + "/*" | into glob)
|
|
1515
|
+
(($dir | str replace -a -r '([\\[\\]])' '[$1]') + "/*" | into glob)
|
|
1485
1516
|
} else {
|
|
1486
1517
|
null
|
|
1487
1518
|
}
|
|
@@ -1674,7 +1705,7 @@ ${functionName}-external
|
|
|
1674
1705
|
if (i > 0) yield "\n";
|
|
1675
1706
|
if (suggestion.kind === "literal") {
|
|
1676
1707
|
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
1677
|
-
yield `${
|
|
1708
|
+
yield `${encodeLiteral(suggestion.text)}\t${description}`;
|
|
1678
1709
|
} else {
|
|
1679
1710
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
1680
1711
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
@@ -1702,12 +1733,33 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
|
1702
1733
|
# Extract arguments from AST (handles quoted strings properly)
|
|
1703
1734
|
\$arguments = @()
|
|
1704
1735
|
\$commandElements = \$commandAst.CommandElements
|
|
1736
|
+
\$valuePrefix = \$wordToComplete
|
|
1737
|
+
\$quoteType = \$null
|
|
1705
1738
|
|
|
1706
1739
|
# Keep only complete arguments before the cursor. The current word
|
|
1707
1740
|
# (including a token ending exactly at the cursor) is appended below.
|
|
1708
1741
|
for (\$i = 1; \$i -lt \$commandElements.Count; \$i++) {
|
|
1709
1742
|
\$element = \$commandElements[\$i]
|
|
1710
|
-
if (\$element.Extent.EndOffset -ge \$cursorPosition) {
|
|
1743
|
+
if (\$element.Extent.EndOffset -ge \$cursorPosition) {
|
|
1744
|
+
# wordToComplete has synthesized delimiters and decoded escapes.
|
|
1745
|
+
# Only the AST retains the literal value and original quote type.
|
|
1746
|
+
if (\$element -is [System.Management.Automation.Language.StringConstantExpressionAst] -and
|
|
1747
|
+
\$element.Extent.EndOffset -eq \$cursorPosition) {
|
|
1748
|
+
if (\$element.StringConstantType -in @('SingleQuoted', 'DoubleQuoted')) {
|
|
1749
|
+
\$valuePrefix = \$element.Value
|
|
1750
|
+
\$quoteType = \$element.StringConstantType
|
|
1751
|
+
} elseif (\$element.Extent.Text -match "^[^=]+=['\\u2018-\\u201b]") {
|
|
1752
|
+
# Joined values are BareWord constants despite their quotes.
|
|
1753
|
+
# Quote the entire replacement to keep it one argument.
|
|
1754
|
+
\$valuePrefix = \$element.Value
|
|
1755
|
+
\$quoteType = 'SingleQuoted'
|
|
1756
|
+
} elseif (\$element.Extent.Text -match '^[^=]+=["\\u201c-\\u201e]') {
|
|
1757
|
+
\$valuePrefix = \$element.Value
|
|
1758
|
+
\$quoteType = 'DoubleQuoted'
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
break
|
|
1762
|
+
}
|
|
1711
1763
|
|
|
1712
1764
|
if (\$element -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
|
|
1713
1765
|
\$arguments += \$element.Value
|
|
@@ -1716,23 +1768,41 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
|
1716
1768
|
}
|
|
1717
1769
|
}
|
|
1718
1770
|
|
|
1771
|
+
# CompletionText replaces the whole token, including its opening quote.
|
|
1772
|
+
# Escape literals without evaluating variables or expressions.
|
|
1773
|
+
\$quoteCompletion = {
|
|
1774
|
+
param(\$text)
|
|
1775
|
+
if (\$quoteType -eq 'SingleQuoted') {
|
|
1776
|
+
"'" + (\$text -replace "['\\u2018-\\u201b]", '\$0\$0') + "'"
|
|
1777
|
+
} elseif (\$quoteType -eq 'DoubleQuoted') {
|
|
1778
|
+
'"' + (\$text -replace '[\\x60\$"\\u201c-\\u201e]', '\`\$0') + '"'
|
|
1779
|
+
} else {
|
|
1780
|
+
\$text
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1719
1784
|
# Build arguments array for completion command
|
|
1720
1785
|
\$completionArgs = @()
|
|
1721
1786
|
${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
1722
1787
|
` : ""} \$completionArgs += \$arguments
|
|
1723
|
-
\$completionArgs += \$wordToComplete
|
|
1724
1788
|
|
|
1725
1789
|
# Call completion command and capture output
|
|
1726
1790
|
try {
|
|
1727
|
-
|
|
1791
|
+
# Quote the prefix separately: array arguments expand a literal tilde
|
|
1792
|
+
# when passed to native programs on Unix.
|
|
1793
|
+
\$output = & ${programName} \$completionArgs "\$valuePrefix" 2>\$null
|
|
1728
1794
|
if (-not \$output) { return }
|
|
1729
1795
|
|
|
1730
1796
|
# Parse tab-separated output and create CompletionResult objects
|
|
1731
1797
|
\$output -split "\`n" | ForEach-Object {
|
|
1732
|
-
\$
|
|
1798
|
+
\$literal = \$_.StartsWith('__LITERAL__:', [System.StringComparison]::Ordinal)
|
|
1799
|
+
# Quoted values preserve whitespace; only remove the CR of CRLF.
|
|
1800
|
+
# Keep the existing trimming for unquoted completions.
|
|
1801
|
+
\$line = if (\$null -ne \$quoteType) { \$_.TrimEnd([char]13) } else { \$_.Trim() }
|
|
1733
1802
|
if (-not \$line) { return }
|
|
1803
|
+
if (\$literal) { \$line = \$line.Substring(12) }
|
|
1734
1804
|
|
|
1735
|
-
if (\$
|
|
1805
|
+
if (-not \$literal -and \$_.StartsWith('__FILE__:', [System.StringComparison]::Ordinal)) {
|
|
1736
1806
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
1737
1807
|
\$directive = (\$line -split "\`t")[0]
|
|
1738
1808
|
\$parts = \$directive -split ':', 5
|
|
@@ -1748,7 +1818,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1748
1818
|
# comparing so that Windows backslashes match forward slashes
|
|
1749
1819
|
# in the transported pattern.
|
|
1750
1820
|
\$normalizedPattern = if (\$pattern) { \$pattern.Replace('\\', '/') -replace '^\\./','' } else { '' }
|
|
1751
|
-
\$normalizedWord = if (\$
|
|
1821
|
+
\$normalizedWord = if (\$valuePrefix) { \$valuePrefix.Replace('\\', '/') -replace '^\\./','' } else { '' }
|
|
1752
1822
|
\$comparison = if (\$IsWindows) { [System.StringComparison]::OrdinalIgnoreCase } else { [System.StringComparison]::Ordinal }
|
|
1753
1823
|
# For wildcard patterns, compare only the directory prefix
|
|
1754
1824
|
\$comparePattern = \$normalizedPattern
|
|
@@ -1760,12 +1830,25 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1760
1830
|
\$comparePattern = ''
|
|
1761
1831
|
}
|
|
1762
1832
|
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1833
|
+
# A quoted home-pattern directory inserts an absolute path.
|
|
1834
|
+
# Compare subsequent input against the same expanded prefix.
|
|
1835
|
+
# Detect home syntax before removing ./, which can target a
|
|
1836
|
+
# literal local tilde directory rather than the home directory.
|
|
1837
|
+
\$homePattern = \$pattern -and (\$pattern -eq '~' -or \$pattern.Replace('\\', '/').StartsWith('~/'))
|
|
1838
|
+
if (\$homePattern) {
|
|
1839
|
+
# Preserve the home root when the pattern is exactly ~.
|
|
1840
|
+
\$expandedPattern = if (\$comparePattern -eq '~') { \$HOME } else { \$HOME.Replace('\\', '/').TrimEnd('/') + \$comparePattern.Substring(1) }
|
|
1841
|
+
\$expandedPattern = [IO.Path]::GetFullPath(\$expandedPattern).Replace('\\', '/')
|
|
1842
|
+
if (\$normalizedWord.StartsWith(\$expandedPattern, \$comparison)) {
|
|
1843
|
+
\$comparePattern = \$expandedPattern
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
\$prefix = if ((\$comparePattern -or \$normalizedWord) -and \$normalizedWord -and \$normalizedWord.StartsWith(\$comparePattern, \$comparison) -and \$normalizedWord.Length -ge \$comparePattern.Length -and \$valuePrefix -ne \$pattern) {
|
|
1847
|
+
\$valuePrefix
|
|
1765
1848
|
} elseif (\$pattern) {
|
|
1766
1849
|
\$pattern
|
|
1767
|
-
} elseif (\$
|
|
1768
|
-
\$
|
|
1850
|
+
} elseif (\$valuePrefix) {
|
|
1851
|
+
\$valuePrefix
|
|
1769
1852
|
} else { '' }
|
|
1770
1853
|
|
|
1771
1854
|
# Use -Force to include hidden files when requested, or when
|
|
@@ -1773,15 +1856,22 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1773
1856
|
\$prefixBasename = if (\$prefix) { Split-Path -Leaf \$prefix } else { '' }
|
|
1774
1857
|
\$forceParam = if (\$hidden -or (\$prefixBasename -and \$prefixBasename.StartsWith('.'))) { @{Force = \$true} } else { @{} }
|
|
1775
1858
|
|
|
1859
|
+
# A quoted tilde is literal when passed to a native command.
|
|
1860
|
+
# Prevent PowerShell's file provider from expanding it to home.
|
|
1861
|
+
# Parser-provided home patterns still need home expansion.
|
|
1862
|
+
\$prefixFromInput = \$valuePrefix -and \$prefix -ceq \$valuePrefix
|
|
1863
|
+
\$lookupPrefix = if (\$prefixFromInput -and \$null -ne \$quoteType -and \$prefix.StartsWith('~')) { './' + \$prefix } else { \$prefix }
|
|
1864
|
+
|
|
1776
1865
|
# If prefix is a directory without trailing slash, append
|
|
1777
1866
|
# one so Get-ChildItem lists its contents
|
|
1778
|
-
if (\$prefix -and (Test-Path -Path \$
|
|
1867
|
+
if (\$prefix -and (Test-Path -Path \$lookupPrefix -PathType Container) -and -not \$prefix.EndsWith('/') -and -not \$prefix.EndsWith('\\')) {
|
|
1779
1868
|
\$prefix = \$prefix + '/'
|
|
1869
|
+
\$lookupPrefix = \$lookupPrefix + '/'
|
|
1780
1870
|
}
|
|
1781
1871
|
|
|
1782
1872
|
# Build the glob path — when the prefix contains wildcard
|
|
1783
1873
|
# characters, use it as-is; otherwise append *
|
|
1784
|
-
\$globPath = if (\$
|
|
1874
|
+
\$globPath = if (\$lookupPrefix -match '[\*\?]') { \$lookupPrefix } else { "\${lookupPrefix}*" }
|
|
1785
1875
|
|
|
1786
1876
|
# Get file system items based on type
|
|
1787
1877
|
\$items = @()
|
|
@@ -1818,7 +1908,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1818
1908
|
# For file/any modes with glob patterns, also add directories
|
|
1819
1909
|
# from the base directory for navigation
|
|
1820
1910
|
if (\$prefix -match '[\*\?]' -and \$type -ne 'directory') {
|
|
1821
|
-
\$globDir = Split-Path -Parent \$
|
|
1911
|
+
\$globDir = Split-Path -Parent \$lookupPrefix
|
|
1822
1912
|
if (-not \$globDir) { \$globDir = '.' }
|
|
1823
1913
|
\$extraDirs = Get-ChildItem @forceParam -Directory -Path "\$globDir/*" -ErrorAction SilentlyContinue
|
|
1824
1914
|
if (\$extraDirs) { \$items = @(\$items) + @(\$extraDirs) | Select-Object -Unique }
|
|
@@ -1839,8 +1929,13 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1839
1929
|
\$items | ForEach-Object {
|
|
1840
1930
|
\$completionText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)/" } else { "\$dirPrefix\$(\$_.Name)" }
|
|
1841
1931
|
\$itemType = if (\$_.PSIsContainer) { 'Directory' } else { 'File' }
|
|
1932
|
+
# Quoting disables native tilde expansion. A home pattern
|
|
1933
|
+
# supplied by the parser must insert the discovered path.
|
|
1934
|
+
\$replacementText = if (\$null -ne \$quoteType -and -not \$prefixFromInput -and \$homePattern) {
|
|
1935
|
+
if (\$_.PSIsContainer) { \$_.FullName + '/' } else { \$_.FullName }
|
|
1936
|
+
} else { \$completionText }
|
|
1842
1937
|
[System.Management.Automation.CompletionResult]::new(
|
|
1843
|
-
\$
|
|
1938
|
+
(& \$quoteCompletion \$replacementText),
|
|
1844
1939
|
\$completionText,
|
|
1845
1940
|
'ParameterValue',
|
|
1846
1941
|
\$itemType
|
|
@@ -1850,11 +1945,14 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1850
1945
|
# Parse literal completion: text\\tlistItemText\\tdescription
|
|
1851
1946
|
\$parts = \$line -split "\`t", 3
|
|
1852
1947
|
\$completionText = \$parts[0]
|
|
1948
|
+
# Empty display text is invalid for CompletionResult. Keep
|
|
1949
|
+
# ignoring empty suggestions without dropping later records.
|
|
1950
|
+
if (-not \$completionText) { return }
|
|
1853
1951
|
\$listItemText = if (\$parts.Length -gt 1 -and \$parts[1]) { \$parts[1] } else { \$completionText }
|
|
1854
1952
|
\$toolTip = if (\$parts.Length -gt 2 -and \$parts[2]) { \$parts[2] } else { \$completionText }
|
|
1855
1953
|
|
|
1856
1954
|
[System.Management.Automation.CompletionResult]::new(
|
|
1857
|
-
\$completionText,
|
|
1955
|
+
(& \$quoteCompletion \$completionText),
|
|
1858
1956
|
\$listItemText,
|
|
1859
1957
|
'ParameterValue',
|
|
1860
1958
|
\$toolTip
|
|
@@ -1874,7 +1972,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1874
1972
|
if (suggestion.kind === "literal") {
|
|
1875
1973
|
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
1876
1974
|
const text = sanitizeForTransport(suggestion.text);
|
|
1877
|
-
yield `${text}\t${text}\t${description}`;
|
|
1975
|
+
yield `${encodeLiteral(suggestion.text)}\t${text}\t${description}`;
|
|
1878
1976
|
} else {
|
|
1879
1977
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
1880
1978
|
const hidden = suggestion.includeHidden ? "1" : "0";
|