@optique/core 1.0.15-dev.2835 → 1.0.15-dev.2854
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 +94 -19
- package/dist/completion.js +94 -19
- package/package.json +1 -1
package/dist/completion.cjs
CHANGED
|
@@ -679,8 +679,9 @@ const zsh = {
|
|
|
679
679
|
generateScript(programName, args = []) {
|
|
680
680
|
validateProgramName(programName);
|
|
681
681
|
const escapedArgs = args.map((arg) => `'${arg.replace(/'/g, "'\\''")}'`).join(" ");
|
|
682
|
+
const functionName = /[.-]/.test(programName) ? `_optique:${programName}` : `_${programName}`;
|
|
682
683
|
return `#compdef ${programName}
|
|
683
|
-
function
|
|
684
|
+
function ${functionName} () {
|
|
684
685
|
local current="\$words[CURRENT]"
|
|
685
686
|
local -a prev
|
|
686
687
|
# Extract previous arguments, skipping empty ones
|
|
@@ -815,10 +816,13 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
815
816
|
local -a matches
|
|
816
817
|
local -i i
|
|
817
818
|
for (( i=1; i <= \${#completions[@]}; i++ )); do
|
|
819
|
+
# _describe treats colons as separators and backslashes as escapes.
|
|
820
|
+
value="\${completions[i]//\\\\/\\\\\\\\}"
|
|
821
|
+
value="\${value//:/\\\\:}"
|
|
818
822
|
if [[ -n "\${descriptions[i]}" ]]; then
|
|
819
|
-
matches+=("\${
|
|
823
|
+
matches+=("\${value}:\${descriptions[i]}")
|
|
820
824
|
else
|
|
821
|
-
matches+=("\${
|
|
825
|
+
matches+=("\${value}")
|
|
822
826
|
fi
|
|
823
827
|
done
|
|
824
828
|
_describe 'commands' matches
|
|
@@ -826,7 +830,13 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
826
830
|
}
|
|
827
831
|
|
|
828
832
|
if (( $+functions[compdef] )); then
|
|
829
|
-
compdef
|
|
833
|
+
compdef ${functionName} ${programName}
|
|
834
|
+
fi
|
|
835
|
+
|
|
836
|
+
# An autoloaded file runs as a function body. Dispatch its initial request;
|
|
837
|
+
# sourcing the file only defines and registers the completion helper.
|
|
838
|
+
if [[ \$ZSH_EVAL_CONTEXT == *:loadautofunc || \$ZSH_EVAL_CONTEXT == *:shfunc ]]; then
|
|
839
|
+
${functionName} "\$@"
|
|
830
840
|
fi
|
|
831
841
|
`;
|
|
832
842
|
},
|
|
@@ -852,7 +862,7 @@ const fish = {
|
|
|
852
862
|
generateScript(programName, args = []) {
|
|
853
863
|
validateProgramName(programName);
|
|
854
864
|
const escapedArgs = args.map((arg) => `'${arg.replace(/'/g, "\\'")}'`).join(" ");
|
|
855
|
-
const functionName = `__${programName
|
|
865
|
+
const functionName = `__${programName}_complete`;
|
|
856
866
|
return `
|
|
857
867
|
function ${functionName}
|
|
858
868
|
set -l tokens (commandline -poc)
|
|
@@ -1730,12 +1740,33 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
|
1730
1740
|
# Extract arguments from AST (handles quoted strings properly)
|
|
1731
1741
|
\$arguments = @()
|
|
1732
1742
|
\$commandElements = \$commandAst.CommandElements
|
|
1743
|
+
\$valuePrefix = \$wordToComplete
|
|
1744
|
+
\$quoteType = \$null
|
|
1733
1745
|
|
|
1734
1746
|
# Keep only complete arguments before the cursor. The current word
|
|
1735
1747
|
# (including a token ending exactly at the cursor) is appended below.
|
|
1736
1748
|
for (\$i = 1; \$i -lt \$commandElements.Count; \$i++) {
|
|
1737
1749
|
\$element = \$commandElements[\$i]
|
|
1738
|
-
if (\$element.Extent.EndOffset -ge \$cursorPosition) {
|
|
1750
|
+
if (\$element.Extent.EndOffset -ge \$cursorPosition) {
|
|
1751
|
+
# wordToComplete has synthesized delimiters and decoded escapes.
|
|
1752
|
+
# Only the AST retains the literal value and original quote type.
|
|
1753
|
+
if (\$element -is [System.Management.Automation.Language.StringConstantExpressionAst] -and
|
|
1754
|
+
\$element.Extent.EndOffset -eq \$cursorPosition) {
|
|
1755
|
+
if (\$element.StringConstantType -in @('SingleQuoted', 'DoubleQuoted')) {
|
|
1756
|
+
\$valuePrefix = \$element.Value
|
|
1757
|
+
\$quoteType = \$element.StringConstantType
|
|
1758
|
+
} elseif (\$element.Extent.Text -match "^[^=]+=['\\u2018-\\u201b]") {
|
|
1759
|
+
# Joined values are BareWord constants despite their quotes.
|
|
1760
|
+
# Quote the entire replacement to keep it one argument.
|
|
1761
|
+
\$valuePrefix = \$element.Value
|
|
1762
|
+
\$quoteType = 'SingleQuoted'
|
|
1763
|
+
} elseif (\$element.Extent.Text -match '^[^=]+=["\\u201c-\\u201e]') {
|
|
1764
|
+
\$valuePrefix = \$element.Value
|
|
1765
|
+
\$quoteType = 'DoubleQuoted'
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
break
|
|
1769
|
+
}
|
|
1739
1770
|
|
|
1740
1771
|
if (\$element -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
|
|
1741
1772
|
\$arguments += \$element.Value
|
|
@@ -1744,21 +1775,37 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
|
1744
1775
|
}
|
|
1745
1776
|
}
|
|
1746
1777
|
|
|
1778
|
+
# CompletionText replaces the whole token, including its opening quote.
|
|
1779
|
+
# Escape literals without evaluating variables or expressions.
|
|
1780
|
+
\$quoteCompletion = {
|
|
1781
|
+
param(\$text)
|
|
1782
|
+
if (\$quoteType -eq 'SingleQuoted') {
|
|
1783
|
+
"'" + (\$text -replace "['\\u2018-\\u201b]", '\$0\$0') + "'"
|
|
1784
|
+
} elseif (\$quoteType -eq 'DoubleQuoted') {
|
|
1785
|
+
'"' + (\$text -replace '[\\x60\$"\\u201c-\\u201e]', '\`\$0') + '"'
|
|
1786
|
+
} else {
|
|
1787
|
+
\$text
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1747
1791
|
# Build arguments array for completion command
|
|
1748
1792
|
\$completionArgs = @()
|
|
1749
1793
|
${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
1750
1794
|
` : ""} \$completionArgs += \$arguments
|
|
1751
|
-
\$completionArgs += \$wordToComplete
|
|
1752
1795
|
|
|
1753
1796
|
# Call completion command and capture output
|
|
1754
1797
|
try {
|
|
1755
|
-
|
|
1798
|
+
# Quote the prefix separately: array arguments expand a literal tilde
|
|
1799
|
+
# when passed to native programs on Unix.
|
|
1800
|
+
\$output = & ${programName} \$completionArgs "\$valuePrefix" 2>\$null
|
|
1756
1801
|
if (-not \$output) { return }
|
|
1757
1802
|
|
|
1758
1803
|
# Parse tab-separated output and create CompletionResult objects
|
|
1759
1804
|
\$output -split "\`n" | ForEach-Object {
|
|
1760
1805
|
\$literal = \$_.StartsWith('__LITERAL__:', [System.StringComparison]::Ordinal)
|
|
1761
|
-
|
|
1806
|
+
# Quoted values preserve whitespace; only remove the CR of CRLF.
|
|
1807
|
+
# Keep the existing trimming for unquoted completions.
|
|
1808
|
+
\$line = if (\$null -ne \$quoteType) { \$_.TrimEnd([char]13) } else { \$_.Trim() }
|
|
1762
1809
|
if (-not \$line) { return }
|
|
1763
1810
|
if (\$literal) { \$line = \$line.Substring(12) }
|
|
1764
1811
|
|
|
@@ -1778,7 +1825,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1778
1825
|
# comparing so that Windows backslashes match forward slashes
|
|
1779
1826
|
# in the transported pattern.
|
|
1780
1827
|
\$normalizedPattern = if (\$pattern) { \$pattern.Replace('\\', '/') -replace '^\\./','' } else { '' }
|
|
1781
|
-
\$normalizedWord = if (\$
|
|
1828
|
+
\$normalizedWord = if (\$valuePrefix) { \$valuePrefix.Replace('\\', '/') -replace '^\\./','' } else { '' }
|
|
1782
1829
|
\$comparison = if (\$IsWindows) { [System.StringComparison]::OrdinalIgnoreCase } else { [System.StringComparison]::Ordinal }
|
|
1783
1830
|
# For wildcard patterns, compare only the directory prefix
|
|
1784
1831
|
\$comparePattern = \$normalizedPattern
|
|
@@ -1790,12 +1837,25 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1790
1837
|
\$comparePattern = ''
|
|
1791
1838
|
}
|
|
1792
1839
|
}
|
|
1793
|
-
|
|
1794
|
-
|
|
1840
|
+
# A quoted home-pattern directory inserts an absolute path.
|
|
1841
|
+
# Compare subsequent input against the same expanded prefix.
|
|
1842
|
+
# Detect home syntax before removing ./, which can target a
|
|
1843
|
+
# literal local tilde directory rather than the home directory.
|
|
1844
|
+
\$homePattern = \$pattern -and (\$pattern -eq '~' -or \$pattern.Replace('\\', '/').StartsWith('~/'))
|
|
1845
|
+
if (\$homePattern) {
|
|
1846
|
+
# Preserve the home root when the pattern is exactly ~.
|
|
1847
|
+
\$expandedPattern = if (\$comparePattern -eq '~') { \$HOME } else { \$HOME.Replace('\\', '/').TrimEnd('/') + \$comparePattern.Substring(1) }
|
|
1848
|
+
\$expandedPattern = [IO.Path]::GetFullPath(\$expandedPattern).Replace('\\', '/')
|
|
1849
|
+
if (\$normalizedWord.StartsWith(\$expandedPattern, \$comparison)) {
|
|
1850
|
+
\$comparePattern = \$expandedPattern
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
\$prefix = if ((\$comparePattern -or \$normalizedWord) -and \$normalizedWord -and \$normalizedWord.StartsWith(\$comparePattern, \$comparison) -and \$normalizedWord.Length -ge \$comparePattern.Length -and \$valuePrefix -ne \$pattern) {
|
|
1854
|
+
\$valuePrefix
|
|
1795
1855
|
} elseif (\$pattern) {
|
|
1796
1856
|
\$pattern
|
|
1797
|
-
} elseif (\$
|
|
1798
|
-
\$
|
|
1857
|
+
} elseif (\$valuePrefix) {
|
|
1858
|
+
\$valuePrefix
|
|
1799
1859
|
} else { '' }
|
|
1800
1860
|
|
|
1801
1861
|
# Use -Force to include hidden files when requested, or when
|
|
@@ -1803,15 +1863,22 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1803
1863
|
\$prefixBasename = if (\$prefix) { Split-Path -Leaf \$prefix } else { '' }
|
|
1804
1864
|
\$forceParam = if (\$hidden -or (\$prefixBasename -and \$prefixBasename.StartsWith('.'))) { @{Force = \$true} } else { @{} }
|
|
1805
1865
|
|
|
1866
|
+
# A quoted tilde is literal when passed to a native command.
|
|
1867
|
+
# Prevent PowerShell's file provider from expanding it to home.
|
|
1868
|
+
# Parser-provided home patterns still need home expansion.
|
|
1869
|
+
\$prefixFromInput = \$valuePrefix -and \$prefix -ceq \$valuePrefix
|
|
1870
|
+
\$lookupPrefix = if (\$prefixFromInput -and \$null -ne \$quoteType -and \$prefix.StartsWith('~')) { './' + \$prefix } else { \$prefix }
|
|
1871
|
+
|
|
1806
1872
|
# If prefix is a directory without trailing slash, append
|
|
1807
1873
|
# one so Get-ChildItem lists its contents
|
|
1808
|
-
if (\$prefix -and (Test-Path -Path \$
|
|
1874
|
+
if (\$prefix -and (Test-Path -Path \$lookupPrefix -PathType Container) -and -not \$prefix.EndsWith('/') -and -not \$prefix.EndsWith('\\')) {
|
|
1809
1875
|
\$prefix = \$prefix + '/'
|
|
1876
|
+
\$lookupPrefix = \$lookupPrefix + '/'
|
|
1810
1877
|
}
|
|
1811
1878
|
|
|
1812
1879
|
# Build the glob path — when the prefix contains wildcard
|
|
1813
1880
|
# characters, use it as-is; otherwise append *
|
|
1814
|
-
\$globPath = if (\$
|
|
1881
|
+
\$globPath = if (\$lookupPrefix -match '[\*\?]') { \$lookupPrefix } else { "\${lookupPrefix}*" }
|
|
1815
1882
|
|
|
1816
1883
|
# Get file system items based on type
|
|
1817
1884
|
\$items = @()
|
|
@@ -1848,7 +1915,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1848
1915
|
# For file/any modes with glob patterns, also add directories
|
|
1849
1916
|
# from the base directory for navigation
|
|
1850
1917
|
if (\$prefix -match '[\*\?]' -and \$type -ne 'directory') {
|
|
1851
|
-
\$globDir = Split-Path -Parent \$
|
|
1918
|
+
\$globDir = Split-Path -Parent \$lookupPrefix
|
|
1852
1919
|
if (-not \$globDir) { \$globDir = '.' }
|
|
1853
1920
|
\$extraDirs = Get-ChildItem @forceParam -Directory -Path "\$globDir/*" -ErrorAction SilentlyContinue
|
|
1854
1921
|
if (\$extraDirs) { \$items = @(\$items) + @(\$extraDirs) | Select-Object -Unique }
|
|
@@ -1869,8 +1936,13 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1869
1936
|
\$items | ForEach-Object {
|
|
1870
1937
|
\$completionText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)/" } else { "\$dirPrefix\$(\$_.Name)" }
|
|
1871
1938
|
\$itemType = if (\$_.PSIsContainer) { 'Directory' } else { 'File' }
|
|
1939
|
+
# Quoting disables native tilde expansion. A home pattern
|
|
1940
|
+
# supplied by the parser must insert the discovered path.
|
|
1941
|
+
\$replacementText = if (\$null -ne \$quoteType -and -not \$prefixFromInput -and \$homePattern) {
|
|
1942
|
+
if (\$_.PSIsContainer) { \$_.FullName + '/' } else { \$_.FullName }
|
|
1943
|
+
} else { \$completionText }
|
|
1872
1944
|
[System.Management.Automation.CompletionResult]::new(
|
|
1873
|
-
\$
|
|
1945
|
+
(& \$quoteCompletion \$replacementText),
|
|
1874
1946
|
\$completionText,
|
|
1875
1947
|
'ParameterValue',
|
|
1876
1948
|
\$itemType
|
|
@@ -1880,11 +1952,14 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1880
1952
|
# Parse literal completion: text\\tlistItemText\\tdescription
|
|
1881
1953
|
\$parts = \$line -split "\`t", 3
|
|
1882
1954
|
\$completionText = \$parts[0]
|
|
1955
|
+
# Empty display text is invalid for CompletionResult. Keep
|
|
1956
|
+
# ignoring empty suggestions without dropping later records.
|
|
1957
|
+
if (-not \$completionText) { return }
|
|
1883
1958
|
\$listItemText = if (\$parts.Length -gt 1 -and \$parts[1]) { \$parts[1] } else { \$completionText }
|
|
1884
1959
|
\$toolTip = if (\$parts.Length -gt 2 -and \$parts[2]) { \$parts[2] } else { \$completionText }
|
|
1885
1960
|
|
|
1886
1961
|
[System.Management.Automation.CompletionResult]::new(
|
|
1887
|
-
\$completionText,
|
|
1962
|
+
(& \$quoteCompletion \$completionText),
|
|
1888
1963
|
\$listItemText,
|
|
1889
1964
|
'ParameterValue',
|
|
1890
1965
|
\$toolTip
|
package/dist/completion.js
CHANGED
|
@@ -679,8 +679,9 @@ const zsh = {
|
|
|
679
679
|
generateScript(programName, args = []) {
|
|
680
680
|
validateProgramName(programName);
|
|
681
681
|
const escapedArgs = args.map((arg) => `'${arg.replace(/'/g, "'\\''")}'`).join(" ");
|
|
682
|
+
const functionName = /[.-]/.test(programName) ? `_optique:${programName}` : `_${programName}`;
|
|
682
683
|
return `#compdef ${programName}
|
|
683
|
-
function
|
|
684
|
+
function ${functionName} () {
|
|
684
685
|
local current="\$words[CURRENT]"
|
|
685
686
|
local -a prev
|
|
686
687
|
# Extract previous arguments, skipping empty ones
|
|
@@ -815,10 +816,13 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
815
816
|
local -a matches
|
|
816
817
|
local -i i
|
|
817
818
|
for (( i=1; i <= \${#completions[@]}; i++ )); do
|
|
819
|
+
# _describe treats colons as separators and backslashes as escapes.
|
|
820
|
+
value="\${completions[i]//\\\\/\\\\\\\\}"
|
|
821
|
+
value="\${value//:/\\\\:}"
|
|
818
822
|
if [[ -n "\${descriptions[i]}" ]]; then
|
|
819
|
-
matches+=("\${
|
|
823
|
+
matches+=("\${value}:\${descriptions[i]}")
|
|
820
824
|
else
|
|
821
|
-
matches+=("\${
|
|
825
|
+
matches+=("\${value}")
|
|
822
826
|
fi
|
|
823
827
|
done
|
|
824
828
|
_describe 'commands' matches
|
|
@@ -826,7 +830,13 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
|
|
|
826
830
|
}
|
|
827
831
|
|
|
828
832
|
if (( $+functions[compdef] )); then
|
|
829
|
-
compdef
|
|
833
|
+
compdef ${functionName} ${programName}
|
|
834
|
+
fi
|
|
835
|
+
|
|
836
|
+
# An autoloaded file runs as a function body. Dispatch its initial request;
|
|
837
|
+
# sourcing the file only defines and registers the completion helper.
|
|
838
|
+
if [[ \$ZSH_EVAL_CONTEXT == *:loadautofunc || \$ZSH_EVAL_CONTEXT == *:shfunc ]]; then
|
|
839
|
+
${functionName} "\$@"
|
|
830
840
|
fi
|
|
831
841
|
`;
|
|
832
842
|
},
|
|
@@ -852,7 +862,7 @@ const fish = {
|
|
|
852
862
|
generateScript(programName, args = []) {
|
|
853
863
|
validateProgramName(programName);
|
|
854
864
|
const escapedArgs = args.map((arg) => `'${arg.replace(/'/g, "\\'")}'`).join(" ");
|
|
855
|
-
const functionName = `__${programName
|
|
865
|
+
const functionName = `__${programName}_complete`;
|
|
856
866
|
return `
|
|
857
867
|
function ${functionName}
|
|
858
868
|
set -l tokens (commandline -poc)
|
|
@@ -1730,12 +1740,33 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
|
1730
1740
|
# Extract arguments from AST (handles quoted strings properly)
|
|
1731
1741
|
\$arguments = @()
|
|
1732
1742
|
\$commandElements = \$commandAst.CommandElements
|
|
1743
|
+
\$valuePrefix = \$wordToComplete
|
|
1744
|
+
\$quoteType = \$null
|
|
1733
1745
|
|
|
1734
1746
|
# Keep only complete arguments before the cursor. The current word
|
|
1735
1747
|
# (including a token ending exactly at the cursor) is appended below.
|
|
1736
1748
|
for (\$i = 1; \$i -lt \$commandElements.Count; \$i++) {
|
|
1737
1749
|
\$element = \$commandElements[\$i]
|
|
1738
|
-
if (\$element.Extent.EndOffset -ge \$cursorPosition) {
|
|
1750
|
+
if (\$element.Extent.EndOffset -ge \$cursorPosition) {
|
|
1751
|
+
# wordToComplete has synthesized delimiters and decoded escapes.
|
|
1752
|
+
# Only the AST retains the literal value and original quote type.
|
|
1753
|
+
if (\$element -is [System.Management.Automation.Language.StringConstantExpressionAst] -and
|
|
1754
|
+
\$element.Extent.EndOffset -eq \$cursorPosition) {
|
|
1755
|
+
if (\$element.StringConstantType -in @('SingleQuoted', 'DoubleQuoted')) {
|
|
1756
|
+
\$valuePrefix = \$element.Value
|
|
1757
|
+
\$quoteType = \$element.StringConstantType
|
|
1758
|
+
} elseif (\$element.Extent.Text -match "^[^=]+=['\\u2018-\\u201b]") {
|
|
1759
|
+
# Joined values are BareWord constants despite their quotes.
|
|
1760
|
+
# Quote the entire replacement to keep it one argument.
|
|
1761
|
+
\$valuePrefix = \$element.Value
|
|
1762
|
+
\$quoteType = 'SingleQuoted'
|
|
1763
|
+
} elseif (\$element.Extent.Text -match '^[^=]+=["\\u201c-\\u201e]') {
|
|
1764
|
+
\$valuePrefix = \$element.Value
|
|
1765
|
+
\$quoteType = 'DoubleQuoted'
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
break
|
|
1769
|
+
}
|
|
1739
1770
|
|
|
1740
1771
|
if (\$element -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
|
|
1741
1772
|
\$arguments += \$element.Value
|
|
@@ -1744,21 +1775,37 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
|
|
|
1744
1775
|
}
|
|
1745
1776
|
}
|
|
1746
1777
|
|
|
1778
|
+
# CompletionText replaces the whole token, including its opening quote.
|
|
1779
|
+
# Escape literals without evaluating variables or expressions.
|
|
1780
|
+
\$quoteCompletion = {
|
|
1781
|
+
param(\$text)
|
|
1782
|
+
if (\$quoteType -eq 'SingleQuoted') {
|
|
1783
|
+
"'" + (\$text -replace "['\\u2018-\\u201b]", '\$0\$0') + "'"
|
|
1784
|
+
} elseif (\$quoteType -eq 'DoubleQuoted') {
|
|
1785
|
+
'"' + (\$text -replace '[\\x60\$"\\u201c-\\u201e]', '\`\$0') + '"'
|
|
1786
|
+
} else {
|
|
1787
|
+
\$text
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1747
1791
|
# Build arguments array for completion command
|
|
1748
1792
|
\$completionArgs = @()
|
|
1749
1793
|
${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
1750
1794
|
` : ""} \$completionArgs += \$arguments
|
|
1751
|
-
\$completionArgs += \$wordToComplete
|
|
1752
1795
|
|
|
1753
1796
|
# Call completion command and capture output
|
|
1754
1797
|
try {
|
|
1755
|
-
|
|
1798
|
+
# Quote the prefix separately: array arguments expand a literal tilde
|
|
1799
|
+
# when passed to native programs on Unix.
|
|
1800
|
+
\$output = & ${programName} \$completionArgs "\$valuePrefix" 2>\$null
|
|
1756
1801
|
if (-not \$output) { return }
|
|
1757
1802
|
|
|
1758
1803
|
# Parse tab-separated output and create CompletionResult objects
|
|
1759
1804
|
\$output -split "\`n" | ForEach-Object {
|
|
1760
1805
|
\$literal = \$_.StartsWith('__LITERAL__:', [System.StringComparison]::Ordinal)
|
|
1761
|
-
|
|
1806
|
+
# Quoted values preserve whitespace; only remove the CR of CRLF.
|
|
1807
|
+
# Keep the existing trimming for unquoted completions.
|
|
1808
|
+
\$line = if (\$null -ne \$quoteType) { \$_.TrimEnd([char]13) } else { \$_.Trim() }
|
|
1762
1809
|
if (-not \$line) { return }
|
|
1763
1810
|
if (\$literal) { \$line = \$line.Substring(12) }
|
|
1764
1811
|
|
|
@@ -1778,7 +1825,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1778
1825
|
# comparing so that Windows backslashes match forward slashes
|
|
1779
1826
|
# in the transported pattern.
|
|
1780
1827
|
\$normalizedPattern = if (\$pattern) { \$pattern.Replace('\\', '/') -replace '^\\./','' } else { '' }
|
|
1781
|
-
\$normalizedWord = if (\$
|
|
1828
|
+
\$normalizedWord = if (\$valuePrefix) { \$valuePrefix.Replace('\\', '/') -replace '^\\./','' } else { '' }
|
|
1782
1829
|
\$comparison = if (\$IsWindows) { [System.StringComparison]::OrdinalIgnoreCase } else { [System.StringComparison]::Ordinal }
|
|
1783
1830
|
# For wildcard patterns, compare only the directory prefix
|
|
1784
1831
|
\$comparePattern = \$normalizedPattern
|
|
@@ -1790,12 +1837,25 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1790
1837
|
\$comparePattern = ''
|
|
1791
1838
|
}
|
|
1792
1839
|
}
|
|
1793
|
-
|
|
1794
|
-
|
|
1840
|
+
# A quoted home-pattern directory inserts an absolute path.
|
|
1841
|
+
# Compare subsequent input against the same expanded prefix.
|
|
1842
|
+
# Detect home syntax before removing ./, which can target a
|
|
1843
|
+
# literal local tilde directory rather than the home directory.
|
|
1844
|
+
\$homePattern = \$pattern -and (\$pattern -eq '~' -or \$pattern.Replace('\\', '/').StartsWith('~/'))
|
|
1845
|
+
if (\$homePattern) {
|
|
1846
|
+
# Preserve the home root when the pattern is exactly ~.
|
|
1847
|
+
\$expandedPattern = if (\$comparePattern -eq '~') { \$HOME } else { \$HOME.Replace('\\', '/').TrimEnd('/') + \$comparePattern.Substring(1) }
|
|
1848
|
+
\$expandedPattern = [IO.Path]::GetFullPath(\$expandedPattern).Replace('\\', '/')
|
|
1849
|
+
if (\$normalizedWord.StartsWith(\$expandedPattern, \$comparison)) {
|
|
1850
|
+
\$comparePattern = \$expandedPattern
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
\$prefix = if ((\$comparePattern -or \$normalizedWord) -and \$normalizedWord -and \$normalizedWord.StartsWith(\$comparePattern, \$comparison) -and \$normalizedWord.Length -ge \$comparePattern.Length -and \$valuePrefix -ne \$pattern) {
|
|
1854
|
+
\$valuePrefix
|
|
1795
1855
|
} elseif (\$pattern) {
|
|
1796
1856
|
\$pattern
|
|
1797
|
-
} elseif (\$
|
|
1798
|
-
\$
|
|
1857
|
+
} elseif (\$valuePrefix) {
|
|
1858
|
+
\$valuePrefix
|
|
1799
1859
|
} else { '' }
|
|
1800
1860
|
|
|
1801
1861
|
# Use -Force to include hidden files when requested, or when
|
|
@@ -1803,15 +1863,22 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1803
1863
|
\$prefixBasename = if (\$prefix) { Split-Path -Leaf \$prefix } else { '' }
|
|
1804
1864
|
\$forceParam = if (\$hidden -or (\$prefixBasename -and \$prefixBasename.StartsWith('.'))) { @{Force = \$true} } else { @{} }
|
|
1805
1865
|
|
|
1866
|
+
# A quoted tilde is literal when passed to a native command.
|
|
1867
|
+
# Prevent PowerShell's file provider from expanding it to home.
|
|
1868
|
+
# Parser-provided home patterns still need home expansion.
|
|
1869
|
+
\$prefixFromInput = \$valuePrefix -and \$prefix -ceq \$valuePrefix
|
|
1870
|
+
\$lookupPrefix = if (\$prefixFromInput -and \$null -ne \$quoteType -and \$prefix.StartsWith('~')) { './' + \$prefix } else { \$prefix }
|
|
1871
|
+
|
|
1806
1872
|
# If prefix is a directory without trailing slash, append
|
|
1807
1873
|
# one so Get-ChildItem lists its contents
|
|
1808
|
-
if (\$prefix -and (Test-Path -Path \$
|
|
1874
|
+
if (\$prefix -and (Test-Path -Path \$lookupPrefix -PathType Container) -and -not \$prefix.EndsWith('/') -and -not \$prefix.EndsWith('\\')) {
|
|
1809
1875
|
\$prefix = \$prefix + '/'
|
|
1876
|
+
\$lookupPrefix = \$lookupPrefix + '/'
|
|
1810
1877
|
}
|
|
1811
1878
|
|
|
1812
1879
|
# Build the glob path — when the prefix contains wildcard
|
|
1813
1880
|
# characters, use it as-is; otherwise append *
|
|
1814
|
-
\$globPath = if (\$
|
|
1881
|
+
\$globPath = if (\$lookupPrefix -match '[\*\?]') { \$lookupPrefix } else { "\${lookupPrefix}*" }
|
|
1815
1882
|
|
|
1816
1883
|
# Get file system items based on type
|
|
1817
1884
|
\$items = @()
|
|
@@ -1848,7 +1915,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1848
1915
|
# For file/any modes with glob patterns, also add directories
|
|
1849
1916
|
# from the base directory for navigation
|
|
1850
1917
|
if (\$prefix -match '[\*\?]' -and \$type -ne 'directory') {
|
|
1851
|
-
\$globDir = Split-Path -Parent \$
|
|
1918
|
+
\$globDir = Split-Path -Parent \$lookupPrefix
|
|
1852
1919
|
if (-not \$globDir) { \$globDir = '.' }
|
|
1853
1920
|
\$extraDirs = Get-ChildItem @forceParam -Directory -Path "\$globDir/*" -ErrorAction SilentlyContinue
|
|
1854
1921
|
if (\$extraDirs) { \$items = @(\$items) + @(\$extraDirs) | Select-Object -Unique }
|
|
@@ -1869,8 +1936,13 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1869
1936
|
\$items | ForEach-Object {
|
|
1870
1937
|
\$completionText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)/" } else { "\$dirPrefix\$(\$_.Name)" }
|
|
1871
1938
|
\$itemType = if (\$_.PSIsContainer) { 'Directory' } else { 'File' }
|
|
1939
|
+
# Quoting disables native tilde expansion. A home pattern
|
|
1940
|
+
# supplied by the parser must insert the discovered path.
|
|
1941
|
+
\$replacementText = if (\$null -ne \$quoteType -and -not \$prefixFromInput -and \$homePattern) {
|
|
1942
|
+
if (\$_.PSIsContainer) { \$_.FullName + '/' } else { \$_.FullName }
|
|
1943
|
+
} else { \$completionText }
|
|
1872
1944
|
[System.Management.Automation.CompletionResult]::new(
|
|
1873
|
-
\$
|
|
1945
|
+
(& \$quoteCompletion \$replacementText),
|
|
1874
1946
|
\$completionText,
|
|
1875
1947
|
'ParameterValue',
|
|
1876
1948
|
\$itemType
|
|
@@ -1880,11 +1952,14 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
|
|
|
1880
1952
|
# Parse literal completion: text\\tlistItemText\\tdescription
|
|
1881
1953
|
\$parts = \$line -split "\`t", 3
|
|
1882
1954
|
\$completionText = \$parts[0]
|
|
1955
|
+
# Empty display text is invalid for CompletionResult. Keep
|
|
1956
|
+
# ignoring empty suggestions without dropping later records.
|
|
1957
|
+
if (-not \$completionText) { return }
|
|
1883
1958
|
\$listItemText = if (\$parts.Length -gt 1 -and \$parts[1]) { \$parts[1] } else { \$completionText }
|
|
1884
1959
|
\$toolTip = if (\$parts.Length -gt 2 -and \$parts[2]) { \$parts[2] } else { \$completionText }
|
|
1885
1960
|
|
|
1886
1961
|
[System.Management.Automation.CompletionResult]::new(
|
|
1887
|
-
\$completionText,
|
|
1962
|
+
(& \$quoteCompletion \$completionText),
|
|
1888
1963
|
\$listItemText,
|
|
1889
1964
|
'ParameterValue',
|
|
1890
1965
|
\$toolTip
|