@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.
@@ -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 _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
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+=("\${completions[i]}:\${descriptions[i]}")
823
+ matches+=("\${value}:\${descriptions[i]}")
820
824
  else
821
- matches+=("\${completions[i]}")
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 _${programName.replace(/[^a-zA-Z0-9]/g, "_")} ${programName}
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.replace(/[^a-zA-Z0-9]/g, "_")}_complete`;
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) { break }
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
- \$output = & ${programName} \$completionArgs 2>\$null
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
- \$line = \$_.Trim()
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 (\$wordToComplete) { \$wordToComplete.Replace('\\', '/') -replace '^\\./','' } else { '' }
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
- \$prefix = if ((\$comparePattern -or \$normalizedWord) -and \$normalizedWord -and \$normalizedWord.StartsWith(\$comparePattern, \$comparison) -and \$normalizedWord.Length -ge \$comparePattern.Length -and \$wordToComplete -ne \$pattern) {
1794
- \$wordToComplete
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 (\$wordToComplete) {
1798
- \$wordToComplete
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 \$prefix -PathType Container) -and -not \$prefix.EndsWith('/') -and -not \$prefix.EndsWith('\\')) {
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 (\$prefix -match '[\*\?]') { \$prefix } else { "\${prefix}*" }
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 \$prefix
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
- \$completionText,
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
@@ -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 _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
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+=("\${completions[i]}:\${descriptions[i]}")
823
+ matches+=("\${value}:\${descriptions[i]}")
820
824
  else
821
- matches+=("\${completions[i]}")
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 _${programName.replace(/[^a-zA-Z0-9]/g, "_")} ${programName}
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.replace(/[^a-zA-Z0-9]/g, "_")}_complete`;
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) { break }
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
- \$output = & ${programName} \$completionArgs 2>\$null
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
- \$line = \$_.Trim()
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 (\$wordToComplete) { \$wordToComplete.Replace('\\', '/') -replace '^\\./','' } else { '' }
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
- \$prefix = if ((\$comparePattern -or \$normalizedWord) -and \$normalizedWord -and \$normalizedWord.StartsWith(\$comparePattern, \$comparison) -and \$normalizedWord.Length -ge \$comparePattern.Length -and \$wordToComplete -ne \$pattern) {
1794
- \$wordToComplete
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 (\$wordToComplete) {
1798
- \$wordToComplete
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 \$prefix -PathType Container) -and -not \$prefix.EndsWith('/') -and -not \$prefix.EndsWith('\\')) {
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 (\$prefix -match '[\*\?]') { \$prefix } else { "\${prefix}*" }
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 \$prefix
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
- \$completionText,
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.0.15-dev.2835",
3
+ "version": "1.0.15-dev.2854",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",