@optique/core 1.0.15-dev.2822 → 1.0.15-dev.2835
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 +107 -51
- package/dist/completion.js +107 -51
- 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"
|
|
@@ -816,7 +833,7 @@ fi
|
|
|
816
833
|
*encodeSuggestions(suggestions) {
|
|
817
834
|
for (const suggestion of suggestions) if (suggestion.kind === "literal") {
|
|
818
835
|
const description = suggestion.description == null ? "" : sanitizeForTransport(require_message.formatMessage(suggestion.description, { colors: false }));
|
|
819
|
-
yield `${
|
|
836
|
+
yield `${encodeLiteral(suggestion.text)}\0${description}\0`;
|
|
820
837
|
} else {
|
|
821
838
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
822
839
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
@@ -875,7 +892,12 @@ function ${functionName}
|
|
|
875
892
|
${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current 2>/dev/null)\n` : ` set -l output (${programName} $prev $current 2>/dev/null)\n`}
|
|
876
893
|
# Process each line of output
|
|
877
894
|
for line in $output
|
|
878
|
-
|
|
895
|
+
set -l literal 0
|
|
896
|
+
if string match -q '__LITERAL__:*' -- $line
|
|
897
|
+
set line (string sub -s 13 -- $line)
|
|
898
|
+
set literal 1
|
|
899
|
+
end
|
|
900
|
+
if test $literal -eq 0; and string match -q '__FILE__:*' -- $line
|
|
879
901
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
880
902
|
set -l directive (string split \\t -- $line)[1]
|
|
881
903
|
set -l parts (string split ':' -- $directive)
|
|
@@ -902,15 +924,13 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
|
|
|
902
924
|
# Normalize leading ./ so that ./src/ma matches pattern src/
|
|
903
925
|
set -l __norm_current (string replace -r '^\\./' '' -- "$current")
|
|
904
926
|
set -l __norm_pattern (string replace -r '^\\./' '' -- "$pattern")
|
|
905
|
-
# For wildcard patterns, compare only the directory prefix
|
|
927
|
+
# For wildcard patterns, compare only the directory prefix.
|
|
928
|
+
# Use a regex: fish wildcards have no character classes.
|
|
929
|
+
# Only the basename can hold wildcards; a ? or * in a
|
|
930
|
+
# directory name such as What?/ is literal.
|
|
906
931
|
set -l __compare_pattern "$__norm_pattern"
|
|
907
|
-
if string match -
|
|
908
|
-
set __compare_pattern (string replace -r '[
|
|
909
|
-
if string match -q '*/*' -- "$__compare_pattern"
|
|
910
|
-
set __compare_pattern (string replace -r '/[^/]*$' '/' -- "$__compare_pattern")
|
|
911
|
-
else
|
|
912
|
-
set __compare_pattern ""
|
|
913
|
-
end
|
|
932
|
+
if string match -qr '[*?][^/]*$' -- "$__compare_pattern"
|
|
933
|
+
set __compare_pattern (string replace -r '[^/]*$' '' -- "$__compare_pattern")
|
|
914
934
|
end
|
|
915
935
|
set -l __cp_len (string length -- "$__compare_pattern")
|
|
916
936
|
set -l __nc_len (string length -- "$__norm_current")
|
|
@@ -945,54 +965,82 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
|
|
|
945
965
|
end
|
|
946
966
|
|
|
947
967
|
# Pre-expand file candidates. When the glob base came from
|
|
948
|
-
# the program's pattern and contains * or ?, use
|
|
949
|
-
# complete glob expression. Otherwise append * as a
|
|
968
|
+
# the program's pattern and its basename contains * or ?, use
|
|
969
|
+
# it as the complete glob expression. Otherwise append * as a
|
|
970
|
+
# prefix, so a literal ? in a directory name such as What?/
|
|
971
|
+
# still lists that directory. A pattern equal to the current
|
|
972
|
+
# word is the typed text echoed back (as path() does), so its
|
|
973
|
+
# ? and * are literal characters, not wildcards.
|
|
950
974
|
# Note: [ is NOT treated as a glob because it commonly
|
|
951
975
|
# appears in literal filenames like [draft] or foo[1].txt.
|
|
952
976
|
set -l __has_glob 0
|
|
953
977
|
if test $__from_pattern -eq 1
|
|
954
|
-
and string match -
|
|
978
|
+
and string match -qr '[*?][^/]*$' -- "$glob_base"
|
|
979
|
+
and test "$pattern" != "$current"
|
|
955
980
|
set __has_glob 1
|
|
956
981
|
end
|
|
957
982
|
set -l __candidates
|
|
958
983
|
if test $__has_glob -eq 1
|
|
959
984
|
# Safe glob expansion without eval: split the pattern into
|
|
960
|
-
# directory and filter
|
|
961
|
-
#
|
|
962
|
-
|
|
985
|
+
# a directory prefix and a basename filter, list the
|
|
986
|
+
# directory, then match basenames against a regex built
|
|
987
|
+
# from the filter. The prefix keeps one empty element for
|
|
988
|
+
# bare patterns so that $__glob_prefix* still expands.
|
|
989
|
+
set -l __glob_prefix ""
|
|
990
|
+
if string match -q '*/*' -- "$glob_base"
|
|
991
|
+
set __glob_prefix (string replace -r '[^/]*$' '' -- "$glob_base")
|
|
992
|
+
end
|
|
963
993
|
set -l __glob_filter (string replace -r '.*/' '' -- "$glob_base")
|
|
964
|
-
|
|
965
|
-
|
|
994
|
+
# Translate the filter into an anchored regex: * and ? are
|
|
995
|
+
# wildcards and every other character is literal. Fish's
|
|
996
|
+
# own wildcards cannot be used here because ? is literal
|
|
997
|
+
# when the qmark-noglob feature is enabled (fish 4 default).
|
|
998
|
+
set -l __glob_regex '^'
|
|
999
|
+
for __ch in (string split '' -- "$__glob_filter")
|
|
1000
|
+
if test "$__ch" = '*'
|
|
1001
|
+
set __glob_regex "$__glob_regex.*"
|
|
1002
|
+
else if test "$__ch" = '?'
|
|
1003
|
+
set __glob_regex "$__glob_regex."
|
|
1004
|
+
else
|
|
1005
|
+
set __glob_regex "$__glob_regex"(string escape --style=regex -- "$__ch")
|
|
1006
|
+
end
|
|
966
1007
|
end
|
|
1008
|
+
set __glob_regex "$__glob_regex"'$'
|
|
967
1009
|
# Match files by the glob filter. Fish's * does not match
|
|
968
|
-
# dotfiles, so also scan .* when the filter targets them
|
|
969
|
-
|
|
1010
|
+
# dotfiles, so also scan .* when the filter targets them or
|
|
1011
|
+
# hidden files are requested.
|
|
1012
|
+
set -l __entries $__glob_prefix*
|
|
1013
|
+
if test "$hidden" = "1"; or string match -q '.*' -- "$__glob_filter"
|
|
1014
|
+
set -a __entries $__glob_prefix.*
|
|
1015
|
+
end
|
|
1016
|
+
for __item in $__entries
|
|
970
1017
|
if test -e "$__item"
|
|
971
|
-
set -l __bn (
|
|
972
|
-
if
|
|
973
|
-
|
|
1018
|
+
set -l __bn (string replace -r '.*/' '' -- "$__item")
|
|
1019
|
+
if contains -- "$__bn" . ..
|
|
1020
|
+
continue
|
|
974
1021
|
end
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
if string match -q '.*' -- "$__glob_filter"
|
|
978
|
-
for __item in $__glob_dir/.*
|
|
979
|
-
if test -e "$__item"
|
|
980
|
-
set -l __bn (basename -- "$__item")
|
|
981
|
-
if test "$__bn" = "." -o "$__bn" = ".."
|
|
982
|
-
continue
|
|
983
|
-
end
|
|
984
|
-
if string match -q -- "$__glob_filter" "$__bn"
|
|
985
|
-
set -a __candidates "$__item"
|
|
986
|
-
end
|
|
1022
|
+
if string match -qr -- "$__glob_regex" "$__bn"
|
|
1023
|
+
set -a __candidates "$__item"
|
|
987
1024
|
end
|
|
988
1025
|
end
|
|
989
1026
|
end
|
|
990
1027
|
# For file/any modes, also include directories for navigation.
|
|
991
1028
|
# Skip for directory mode so the pattern filter is respected.
|
|
1029
|
+
# Directories the filter already matched are not repeated.
|
|
992
1030
|
if test "$type" != "directory"
|
|
993
|
-
|
|
1031
|
+
set -l __nav_dirs $__glob_prefix*/
|
|
1032
|
+
if test "$hidden" = "1"
|
|
1033
|
+
set -a __nav_dirs $__glob_prefix.*/
|
|
1034
|
+
end
|
|
1035
|
+
for __item in $__nav_dirs
|
|
1036
|
+
set __item (string replace -r '/$' '' -- "$__item")
|
|
1037
|
+
set -l __bn (string replace -r '.*/' '' -- "$__item")
|
|
1038
|
+
if contains -- "$__bn" . ..
|
|
1039
|
+
continue
|
|
1040
|
+
end
|
|
994
1041
|
if test -d "$__item"
|
|
995
|
-
|
|
1042
|
+
and not string match -qr -- "$__glob_regex" "$__bn"
|
|
1043
|
+
set -a __candidates "$__item"
|
|
996
1044
|
end
|
|
997
1045
|
end
|
|
998
1046
|
end
|
|
@@ -1140,7 +1188,7 @@ complete -c ${programName} -f -a '(${functionName})'
|
|
|
1140
1188
|
if (i > 0) yield "\n";
|
|
1141
1189
|
if (suggestion.kind === "literal") {
|
|
1142
1190
|
const description = suggestion.description == null ? "" : sanitizeForTransport(require_message.formatMessage(suggestion.description, { colors: false }));
|
|
1143
|
-
yield `${
|
|
1191
|
+
yield `${encodeLiteral(suggestion.text)}\t${description}`;
|
|
1144
1192
|
} else {
|
|
1145
1193
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
1146
1194
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
@@ -1379,8 +1427,10 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1379
1427
|
}
|
|
1380
1428
|
|
|
1381
1429
|
# Process each line of output
|
|
1382
|
-
$output | lines | each {|
|
|
1383
|
-
|
|
1430
|
+
$output | lines | each {|raw_line|
|
|
1431
|
+
let literal = ($raw_line | str starts-with '__LITERAL__:')
|
|
1432
|
+
let line = if $literal { $raw_line | str substring 12.. } else { $raw_line }
|
|
1433
|
+
if (not $literal) and ($line | str starts-with '__FILE__:') {
|
|
1384
1434
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
1385
1435
|
let directive = ($line | split row "\t" | first)
|
|
1386
1436
|
let parts = ($directive | split row ':')
|
|
@@ -1440,22 +1490,26 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1440
1490
|
# Generate file completions based on type.
|
|
1441
1491
|
# When the glob base contains wildcard characters, use it as-is;
|
|
1442
1492
|
# otherwise append * to treat it as a prefix.
|
|
1443
|
-
# Note: into glob is required so that ls expands wildcards from a variable
|
|
1493
|
+
# Note: into glob is required so that ls expands wildcards from a variable.
|
|
1494
|
+
# A literal prefix has its brackets wrapped as [[] and []] so that path
|
|
1495
|
+
# text like "report[1]" is not parsed as a glob character class; a
|
|
1496
|
+
# wildcard pattern is a glob as a whole, so its brackets stay classes.
|
|
1444
1497
|
let has_glob = ($glob_base =~ '[*?]')
|
|
1445
1498
|
let ls_pattern = if ($glob_base | is-empty) {
|
|
1446
1499
|
"."
|
|
1447
1500
|
} else if $has_glob {
|
|
1448
1501
|
($glob_base | into glob)
|
|
1449
1502
|
} else {
|
|
1450
|
-
($glob_base + "*" | into glob)
|
|
1503
|
+
(($glob_base | str replace -a -r '([\\[\\]])' '[$1]') + "*" | into glob)
|
|
1451
1504
|
}
|
|
1452
1505
|
|
|
1453
1506
|
# When using a glob pattern, also compute a directory listing pattern
|
|
1454
|
-
# so that file/any modes can include directories for navigation
|
|
1507
|
+
# so that file/any modes can include directories for navigation; its
|
|
1508
|
+
# directory part is a literal path
|
|
1455
1509
|
let glob_dir_pattern = if $has_glob {
|
|
1456
1510
|
let dir_part = ($glob_base | path dirname)
|
|
1457
1511
|
let dir = if ($dir_part | is-empty) or ($dir_part == $glob_base) { "." } else { $dir_part }
|
|
1458
|
-
($dir + "/*" | into glob)
|
|
1512
|
+
(($dir | str replace -a -r '([\\[\\]])' '[$1]') + "/*" | into glob)
|
|
1459
1513
|
} else {
|
|
1460
1514
|
null
|
|
1461
1515
|
}
|
|
@@ -1648,7 +1702,7 @@ ${functionName}-external
|
|
|
1648
1702
|
if (i > 0) yield "\n";
|
|
1649
1703
|
if (suggestion.kind === "literal") {
|
|
1650
1704
|
const description = suggestion.description == null ? "" : sanitizeForTransport(require_message.formatMessage(suggestion.description, { colors: false }));
|
|
1651
|
-
yield `${
|
|
1705
|
+
yield `${encodeLiteral(suggestion.text)}\t${description}`;
|
|
1652
1706
|
} else {
|
|
1653
1707
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
1654
1708
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
@@ -1703,10 +1757,12 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1703
1757
|
|
|
1704
1758
|
# Parse tab-separated output and create CompletionResult objects
|
|
1705
1759
|
\$output -split "\`n" | ForEach-Object {
|
|
1760
|
+
\$literal = \$_.StartsWith('__LITERAL__:', [System.StringComparison]::Ordinal)
|
|
1706
1761
|
\$line = \$_.Trim()
|
|
1707
1762
|
if (-not \$line) { return }
|
|
1763
|
+
if (\$literal) { \$line = \$line.Substring(12) }
|
|
1708
1764
|
|
|
1709
|
-
if (\$
|
|
1765
|
+
if (-not \$literal -and \$_.StartsWith('__FILE__:', [System.StringComparison]::Ordinal)) {
|
|
1710
1766
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
1711
1767
|
\$directive = (\$line -split "\`t")[0]
|
|
1712
1768
|
\$parts = \$directive -split ':', 5
|
|
@@ -1848,7 +1904,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1848
1904
|
if (suggestion.kind === "literal") {
|
|
1849
1905
|
const description = suggestion.description == null ? "" : sanitizeForTransport(require_message.formatMessage(suggestion.description, { colors: false }));
|
|
1850
1906
|
const text = sanitizeForTransport(suggestion.text);
|
|
1851
|
-
yield `${text}\t${text}\t${description}`;
|
|
1907
|
+
yield `${encodeLiteral(suggestion.text)}\t${text}\t${description}`;
|
|
1852
1908
|
} else {
|
|
1853
1909
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
1854
1910
|
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"
|
|
@@ -816,7 +833,7 @@ fi
|
|
|
816
833
|
*encodeSuggestions(suggestions) {
|
|
817
834
|
for (const suggestion of suggestions) if (suggestion.kind === "literal") {
|
|
818
835
|
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
819
|
-
yield `${
|
|
836
|
+
yield `${encodeLiteral(suggestion.text)}\0${description}\0`;
|
|
820
837
|
} else {
|
|
821
838
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
822
839
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
@@ -875,7 +892,12 @@ function ${functionName}
|
|
|
875
892
|
${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current 2>/dev/null)\n` : ` set -l output (${programName} $prev $current 2>/dev/null)\n`}
|
|
876
893
|
# Process each line of output
|
|
877
894
|
for line in $output
|
|
878
|
-
|
|
895
|
+
set -l literal 0
|
|
896
|
+
if string match -q '__LITERAL__:*' -- $line
|
|
897
|
+
set line (string sub -s 13 -- $line)
|
|
898
|
+
set literal 1
|
|
899
|
+
end
|
|
900
|
+
if test $literal -eq 0; and string match -q '__FILE__:*' -- $line
|
|
879
901
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
880
902
|
set -l directive (string split \\t -- $line)[1]
|
|
881
903
|
set -l parts (string split ':' -- $directive)
|
|
@@ -902,15 +924,13 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
|
|
|
902
924
|
# Normalize leading ./ so that ./src/ma matches pattern src/
|
|
903
925
|
set -l __norm_current (string replace -r '^\\./' '' -- "$current")
|
|
904
926
|
set -l __norm_pattern (string replace -r '^\\./' '' -- "$pattern")
|
|
905
|
-
# For wildcard patterns, compare only the directory prefix
|
|
927
|
+
# For wildcard patterns, compare only the directory prefix.
|
|
928
|
+
# Use a regex: fish wildcards have no character classes.
|
|
929
|
+
# Only the basename can hold wildcards; a ? or * in a
|
|
930
|
+
# directory name such as What?/ is literal.
|
|
906
931
|
set -l __compare_pattern "$__norm_pattern"
|
|
907
|
-
if string match -
|
|
908
|
-
set __compare_pattern (string replace -r '[
|
|
909
|
-
if string match -q '*/*' -- "$__compare_pattern"
|
|
910
|
-
set __compare_pattern (string replace -r '/[^/]*$' '/' -- "$__compare_pattern")
|
|
911
|
-
else
|
|
912
|
-
set __compare_pattern ""
|
|
913
|
-
end
|
|
932
|
+
if string match -qr '[*?][^/]*$' -- "$__compare_pattern"
|
|
933
|
+
set __compare_pattern (string replace -r '[^/]*$' '' -- "$__compare_pattern")
|
|
914
934
|
end
|
|
915
935
|
set -l __cp_len (string length -- "$__compare_pattern")
|
|
916
936
|
set -l __nc_len (string length -- "$__norm_current")
|
|
@@ -945,54 +965,82 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
|
|
|
945
965
|
end
|
|
946
966
|
|
|
947
967
|
# Pre-expand file candidates. When the glob base came from
|
|
948
|
-
# the program's pattern and contains * or ?, use
|
|
949
|
-
# complete glob expression. Otherwise append * as a
|
|
968
|
+
# the program's pattern and its basename contains * or ?, use
|
|
969
|
+
# it as the complete glob expression. Otherwise append * as a
|
|
970
|
+
# prefix, so a literal ? in a directory name such as What?/
|
|
971
|
+
# still lists that directory. A pattern equal to the current
|
|
972
|
+
# word is the typed text echoed back (as path() does), so its
|
|
973
|
+
# ? and * are literal characters, not wildcards.
|
|
950
974
|
# Note: [ is NOT treated as a glob because it commonly
|
|
951
975
|
# appears in literal filenames like [draft] or foo[1].txt.
|
|
952
976
|
set -l __has_glob 0
|
|
953
977
|
if test $__from_pattern -eq 1
|
|
954
|
-
and string match -
|
|
978
|
+
and string match -qr '[*?][^/]*$' -- "$glob_base"
|
|
979
|
+
and test "$pattern" != "$current"
|
|
955
980
|
set __has_glob 1
|
|
956
981
|
end
|
|
957
982
|
set -l __candidates
|
|
958
983
|
if test $__has_glob -eq 1
|
|
959
984
|
# Safe glob expansion without eval: split the pattern into
|
|
960
|
-
# directory and filter
|
|
961
|
-
#
|
|
962
|
-
|
|
985
|
+
# a directory prefix and a basename filter, list the
|
|
986
|
+
# directory, then match basenames against a regex built
|
|
987
|
+
# from the filter. The prefix keeps one empty element for
|
|
988
|
+
# bare patterns so that $__glob_prefix* still expands.
|
|
989
|
+
set -l __glob_prefix ""
|
|
990
|
+
if string match -q '*/*' -- "$glob_base"
|
|
991
|
+
set __glob_prefix (string replace -r '[^/]*$' '' -- "$glob_base")
|
|
992
|
+
end
|
|
963
993
|
set -l __glob_filter (string replace -r '.*/' '' -- "$glob_base")
|
|
964
|
-
|
|
965
|
-
|
|
994
|
+
# Translate the filter into an anchored regex: * and ? are
|
|
995
|
+
# wildcards and every other character is literal. Fish's
|
|
996
|
+
# own wildcards cannot be used here because ? is literal
|
|
997
|
+
# when the qmark-noglob feature is enabled (fish 4 default).
|
|
998
|
+
set -l __glob_regex '^'
|
|
999
|
+
for __ch in (string split '' -- "$__glob_filter")
|
|
1000
|
+
if test "$__ch" = '*'
|
|
1001
|
+
set __glob_regex "$__glob_regex.*"
|
|
1002
|
+
else if test "$__ch" = '?'
|
|
1003
|
+
set __glob_regex "$__glob_regex."
|
|
1004
|
+
else
|
|
1005
|
+
set __glob_regex "$__glob_regex"(string escape --style=regex -- "$__ch")
|
|
1006
|
+
end
|
|
966
1007
|
end
|
|
1008
|
+
set __glob_regex "$__glob_regex"'$'
|
|
967
1009
|
# Match files by the glob filter. Fish's * does not match
|
|
968
|
-
# dotfiles, so also scan .* when the filter targets them
|
|
969
|
-
|
|
1010
|
+
# dotfiles, so also scan .* when the filter targets them or
|
|
1011
|
+
# hidden files are requested.
|
|
1012
|
+
set -l __entries $__glob_prefix*
|
|
1013
|
+
if test "$hidden" = "1"; or string match -q '.*' -- "$__glob_filter"
|
|
1014
|
+
set -a __entries $__glob_prefix.*
|
|
1015
|
+
end
|
|
1016
|
+
for __item in $__entries
|
|
970
1017
|
if test -e "$__item"
|
|
971
|
-
set -l __bn (
|
|
972
|
-
if
|
|
973
|
-
|
|
1018
|
+
set -l __bn (string replace -r '.*/' '' -- "$__item")
|
|
1019
|
+
if contains -- "$__bn" . ..
|
|
1020
|
+
continue
|
|
974
1021
|
end
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
if string match -q '.*' -- "$__glob_filter"
|
|
978
|
-
for __item in $__glob_dir/.*
|
|
979
|
-
if test -e "$__item"
|
|
980
|
-
set -l __bn (basename -- "$__item")
|
|
981
|
-
if test "$__bn" = "." -o "$__bn" = ".."
|
|
982
|
-
continue
|
|
983
|
-
end
|
|
984
|
-
if string match -q -- "$__glob_filter" "$__bn"
|
|
985
|
-
set -a __candidates "$__item"
|
|
986
|
-
end
|
|
1022
|
+
if string match -qr -- "$__glob_regex" "$__bn"
|
|
1023
|
+
set -a __candidates "$__item"
|
|
987
1024
|
end
|
|
988
1025
|
end
|
|
989
1026
|
end
|
|
990
1027
|
# For file/any modes, also include directories for navigation.
|
|
991
1028
|
# Skip for directory mode so the pattern filter is respected.
|
|
1029
|
+
# Directories the filter already matched are not repeated.
|
|
992
1030
|
if test "$type" != "directory"
|
|
993
|
-
|
|
1031
|
+
set -l __nav_dirs $__glob_prefix*/
|
|
1032
|
+
if test "$hidden" = "1"
|
|
1033
|
+
set -a __nav_dirs $__glob_prefix.*/
|
|
1034
|
+
end
|
|
1035
|
+
for __item in $__nav_dirs
|
|
1036
|
+
set __item (string replace -r '/$' '' -- "$__item")
|
|
1037
|
+
set -l __bn (string replace -r '.*/' '' -- "$__item")
|
|
1038
|
+
if contains -- "$__bn" . ..
|
|
1039
|
+
continue
|
|
1040
|
+
end
|
|
994
1041
|
if test -d "$__item"
|
|
995
|
-
|
|
1042
|
+
and not string match -qr -- "$__glob_regex" "$__bn"
|
|
1043
|
+
set -a __candidates "$__item"
|
|
996
1044
|
end
|
|
997
1045
|
end
|
|
998
1046
|
end
|
|
@@ -1140,7 +1188,7 @@ complete -c ${programName} -f -a '(${functionName})'
|
|
|
1140
1188
|
if (i > 0) yield "\n";
|
|
1141
1189
|
if (suggestion.kind === "literal") {
|
|
1142
1190
|
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
1143
|
-
yield `${
|
|
1191
|
+
yield `${encodeLiteral(suggestion.text)}\t${description}`;
|
|
1144
1192
|
} else {
|
|
1145
1193
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
1146
1194
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
@@ -1379,8 +1427,10 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1379
1427
|
}
|
|
1380
1428
|
|
|
1381
1429
|
# Process each line of output
|
|
1382
|
-
$output | lines | each {|
|
|
1383
|
-
|
|
1430
|
+
$output | lines | each {|raw_line|
|
|
1431
|
+
let literal = ($raw_line | str starts-with '__LITERAL__:')
|
|
1432
|
+
let line = if $literal { $raw_line | str substring 12.. } else { $raw_line }
|
|
1433
|
+
if (not $literal) and ($line | str starts-with '__FILE__:') {
|
|
1384
1434
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
1385
1435
|
let directive = ($line | split row "\t" | first)
|
|
1386
1436
|
let parts = ($directive | split row ':')
|
|
@@ -1440,22 +1490,26 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
|
|
|
1440
1490
|
# Generate file completions based on type.
|
|
1441
1491
|
# When the glob base contains wildcard characters, use it as-is;
|
|
1442
1492
|
# otherwise append * to treat it as a prefix.
|
|
1443
|
-
# Note: into glob is required so that ls expands wildcards from a variable
|
|
1493
|
+
# Note: into glob is required so that ls expands wildcards from a variable.
|
|
1494
|
+
# A literal prefix has its brackets wrapped as [[] and []] so that path
|
|
1495
|
+
# text like "report[1]" is not parsed as a glob character class; a
|
|
1496
|
+
# wildcard pattern is a glob as a whole, so its brackets stay classes.
|
|
1444
1497
|
let has_glob = ($glob_base =~ '[*?]')
|
|
1445
1498
|
let ls_pattern = if ($glob_base | is-empty) {
|
|
1446
1499
|
"."
|
|
1447
1500
|
} else if $has_glob {
|
|
1448
1501
|
($glob_base | into glob)
|
|
1449
1502
|
} else {
|
|
1450
|
-
($glob_base + "*" | into glob)
|
|
1503
|
+
(($glob_base | str replace -a -r '([\\[\\]])' '[$1]') + "*" | into glob)
|
|
1451
1504
|
}
|
|
1452
1505
|
|
|
1453
1506
|
# When using a glob pattern, also compute a directory listing pattern
|
|
1454
|
-
# so that file/any modes can include directories for navigation
|
|
1507
|
+
# so that file/any modes can include directories for navigation; its
|
|
1508
|
+
# directory part is a literal path
|
|
1455
1509
|
let glob_dir_pattern = if $has_glob {
|
|
1456
1510
|
let dir_part = ($glob_base | path dirname)
|
|
1457
1511
|
let dir = if ($dir_part | is-empty) or ($dir_part == $glob_base) { "." } else { $dir_part }
|
|
1458
|
-
($dir + "/*" | into glob)
|
|
1512
|
+
(($dir | str replace -a -r '([\\[\\]])' '[$1]') + "/*" | into glob)
|
|
1459
1513
|
} else {
|
|
1460
1514
|
null
|
|
1461
1515
|
}
|
|
@@ -1648,7 +1702,7 @@ ${functionName}-external
|
|
|
1648
1702
|
if (i > 0) yield "\n";
|
|
1649
1703
|
if (suggestion.kind === "literal") {
|
|
1650
1704
|
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
1651
|
-
yield `${
|
|
1705
|
+
yield `${encodeLiteral(suggestion.text)}\t${description}`;
|
|
1652
1706
|
} else {
|
|
1653
1707
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
1654
1708
|
const hidden = suggestion.includeHidden ? "1" : "0";
|
|
@@ -1703,10 +1757,12 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1703
1757
|
|
|
1704
1758
|
# Parse tab-separated output and create CompletionResult objects
|
|
1705
1759
|
\$output -split "\`n" | ForEach-Object {
|
|
1760
|
+
\$literal = \$_.StartsWith('__LITERAL__:', [System.StringComparison]::Ordinal)
|
|
1706
1761
|
\$line = \$_.Trim()
|
|
1707
1762
|
if (-not \$line) { return }
|
|
1763
|
+
if (\$literal) { \$line = \$line.Substring(12) }
|
|
1708
1764
|
|
|
1709
|
-
if (\$
|
|
1765
|
+
if (-not \$literal -and \$_.StartsWith('__FILE__:', [System.StringComparison]::Ordinal)) {
|
|
1710
1766
|
# Parse file completion directive: __FILE__:type:extensions:pattern:hidden
|
|
1711
1767
|
\$directive = (\$line -split "\`t")[0]
|
|
1712
1768
|
\$parts = \$directive -split ':', 5
|
|
@@ -1848,7 +1904,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1848
1904
|
if (suggestion.kind === "literal") {
|
|
1849
1905
|
const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
|
|
1850
1906
|
const text = sanitizeForTransport(suggestion.text);
|
|
1851
|
-
yield `${text}\t${text}\t${description}`;
|
|
1907
|
+
yield `${encodeLiteral(suggestion.text)}\t${text}\t${description}`;
|
|
1852
1908
|
} else {
|
|
1853
1909
|
const extensions = encodeExtensions(suggestion.extensions);
|
|
1854
1910
|
const hidden = suggestion.includeHidden ? "1" : "0";
|