@optique/core 1.0.15-dev.2835 → 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.
@@ -815,10 +815,13 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
815
815
  local -a matches
816
816
  local -i i
817
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//:/\\\\:}"
818
821
  if [[ -n "\${descriptions[i]}" ]]; then
819
- matches+=("\${completions[i]}:\${descriptions[i]}")
822
+ matches+=("\${value}:\${descriptions[i]}")
820
823
  else
821
- matches+=("\${completions[i]}")
824
+ matches+=("\${value}")
822
825
  fi
823
826
  done
824
827
  _describe 'commands' matches
@@ -1730,12 +1733,33 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
1730
1733
  # Extract arguments from AST (handles quoted strings properly)
1731
1734
  \$arguments = @()
1732
1735
  \$commandElements = \$commandAst.CommandElements
1736
+ \$valuePrefix = \$wordToComplete
1737
+ \$quoteType = \$null
1733
1738
 
1734
1739
  # Keep only complete arguments before the cursor. The current word
1735
1740
  # (including a token ending exactly at the cursor) is appended below.
1736
1741
  for (\$i = 1; \$i -lt \$commandElements.Count; \$i++) {
1737
1742
  \$element = \$commandElements[\$i]
1738
- if (\$element.Extent.EndOffset -ge \$cursorPosition) { break }
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
+ }
1739
1763
 
1740
1764
  if (\$element -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
1741
1765
  \$arguments += \$element.Value
@@ -1744,21 +1768,37 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
1744
1768
  }
1745
1769
  }
1746
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
+
1747
1784
  # Build arguments array for completion command
1748
1785
  \$completionArgs = @()
1749
1786
  ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1750
1787
  ` : ""} \$completionArgs += \$arguments
1751
- \$completionArgs += \$wordToComplete
1752
1788
 
1753
1789
  # Call completion command and capture output
1754
1790
  try {
1755
- \$output = & ${programName} \$completionArgs 2>\$null
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
1756
1794
  if (-not \$output) { return }
1757
1795
 
1758
1796
  # Parse tab-separated output and create CompletionResult objects
1759
1797
  \$output -split "\`n" | ForEach-Object {
1760
1798
  \$literal = \$_.StartsWith('__LITERAL__:', [System.StringComparison]::Ordinal)
1761
- \$line = \$_.Trim()
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() }
1762
1802
  if (-not \$line) { return }
1763
1803
  if (\$literal) { \$line = \$line.Substring(12) }
1764
1804
 
@@ -1778,7 +1818,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1778
1818
  # comparing so that Windows backslashes match forward slashes
1779
1819
  # in the transported pattern.
1780
1820
  \$normalizedPattern = if (\$pattern) { \$pattern.Replace('\\', '/') -replace '^\\./','' } else { '' }
1781
- \$normalizedWord = if (\$wordToComplete) { \$wordToComplete.Replace('\\', '/') -replace '^\\./','' } else { '' }
1821
+ \$normalizedWord = if (\$valuePrefix) { \$valuePrefix.Replace('\\', '/') -replace '^\\./','' } else { '' }
1782
1822
  \$comparison = if (\$IsWindows) { [System.StringComparison]::OrdinalIgnoreCase } else { [System.StringComparison]::Ordinal }
1783
1823
  # For wildcard patterns, compare only the directory prefix
1784
1824
  \$comparePattern = \$normalizedPattern
@@ -1790,12 +1830,25 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1790
1830
  \$comparePattern = ''
1791
1831
  }
1792
1832
  }
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
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
1795
1848
  } elseif (\$pattern) {
1796
1849
  \$pattern
1797
- } elseif (\$wordToComplete) {
1798
- \$wordToComplete
1850
+ } elseif (\$valuePrefix) {
1851
+ \$valuePrefix
1799
1852
  } else { '' }
1800
1853
 
1801
1854
  # Use -Force to include hidden files when requested, or when
@@ -1803,15 +1856,22 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1803
1856
  \$prefixBasename = if (\$prefix) { Split-Path -Leaf \$prefix } else { '' }
1804
1857
  \$forceParam = if (\$hidden -or (\$prefixBasename -and \$prefixBasename.StartsWith('.'))) { @{Force = \$true} } else { @{} }
1805
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
+
1806
1865
  # If prefix is a directory without trailing slash, append
1807
1866
  # 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('\\')) {
1867
+ if (\$prefix -and (Test-Path -Path \$lookupPrefix -PathType Container) -and -not \$prefix.EndsWith('/') -and -not \$prefix.EndsWith('\\')) {
1809
1868
  \$prefix = \$prefix + '/'
1869
+ \$lookupPrefix = \$lookupPrefix + '/'
1810
1870
  }
1811
1871
 
1812
1872
  # Build the glob path — when the prefix contains wildcard
1813
1873
  # characters, use it as-is; otherwise append *
1814
- \$globPath = if (\$prefix -match '[\*\?]') { \$prefix } else { "\${prefix}*" }
1874
+ \$globPath = if (\$lookupPrefix -match '[\*\?]') { \$lookupPrefix } else { "\${lookupPrefix}*" }
1815
1875
 
1816
1876
  # Get file system items based on type
1817
1877
  \$items = @()
@@ -1848,7 +1908,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1848
1908
  # For file/any modes with glob patterns, also add directories
1849
1909
  # from the base directory for navigation
1850
1910
  if (\$prefix -match '[\*\?]' -and \$type -ne 'directory') {
1851
- \$globDir = Split-Path -Parent \$prefix
1911
+ \$globDir = Split-Path -Parent \$lookupPrefix
1852
1912
  if (-not \$globDir) { \$globDir = '.' }
1853
1913
  \$extraDirs = Get-ChildItem @forceParam -Directory -Path "\$globDir/*" -ErrorAction SilentlyContinue
1854
1914
  if (\$extraDirs) { \$items = @(\$items) + @(\$extraDirs) | Select-Object -Unique }
@@ -1869,8 +1929,13 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1869
1929
  \$items | ForEach-Object {
1870
1930
  \$completionText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)/" } else { "\$dirPrefix\$(\$_.Name)" }
1871
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 }
1872
1937
  [System.Management.Automation.CompletionResult]::new(
1873
- \$completionText,
1938
+ (& \$quoteCompletion \$replacementText),
1874
1939
  \$completionText,
1875
1940
  'ParameterValue',
1876
1941
  \$itemType
@@ -1880,11 +1945,14 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1880
1945
  # Parse literal completion: text\\tlistItemText\\tdescription
1881
1946
  \$parts = \$line -split "\`t", 3
1882
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 }
1883
1951
  \$listItemText = if (\$parts.Length -gt 1 -and \$parts[1]) { \$parts[1] } else { \$completionText }
1884
1952
  \$toolTip = if (\$parts.Length -gt 2 -and \$parts[2]) { \$parts[2] } else { \$completionText }
1885
1953
 
1886
1954
  [System.Management.Automation.CompletionResult]::new(
1887
- \$completionText,
1955
+ (& \$quoteCompletion \$completionText),
1888
1956
  \$listItemText,
1889
1957
  'ParameterValue',
1890
1958
  \$toolTip
@@ -815,10 +815,13 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
815
815
  local -a matches
816
816
  local -i i
817
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//:/\\\\:}"
818
821
  if [[ -n "\${descriptions[i]}" ]]; then
819
- matches+=("\${completions[i]}:\${descriptions[i]}")
822
+ matches+=("\${value}:\${descriptions[i]}")
820
823
  else
821
- matches+=("\${completions[i]}")
824
+ matches+=("\${value}")
822
825
  fi
823
826
  done
824
827
  _describe 'commands' matches
@@ -1730,12 +1733,33 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
1730
1733
  # Extract arguments from AST (handles quoted strings properly)
1731
1734
  \$arguments = @()
1732
1735
  \$commandElements = \$commandAst.CommandElements
1736
+ \$valuePrefix = \$wordToComplete
1737
+ \$quoteType = \$null
1733
1738
 
1734
1739
  # Keep only complete arguments before the cursor. The current word
1735
1740
  # (including a token ending exactly at the cursor) is appended below.
1736
1741
  for (\$i = 1; \$i -lt \$commandElements.Count; \$i++) {
1737
1742
  \$element = \$commandElements[\$i]
1738
- if (\$element.Extent.EndOffset -ge \$cursorPosition) { break }
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
+ }
1739
1763
 
1740
1764
  if (\$element -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
1741
1765
  \$arguments += \$element.Value
@@ -1744,21 +1768,37 @@ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
1744
1768
  }
1745
1769
  }
1746
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
+
1747
1784
  # Build arguments array for completion command
1748
1785
  \$completionArgs = @()
1749
1786
  ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1750
1787
  ` : ""} \$completionArgs += \$arguments
1751
- \$completionArgs += \$wordToComplete
1752
1788
 
1753
1789
  # Call completion command and capture output
1754
1790
  try {
1755
- \$output = & ${programName} \$completionArgs 2>\$null
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
1756
1794
  if (-not \$output) { return }
1757
1795
 
1758
1796
  # Parse tab-separated output and create CompletionResult objects
1759
1797
  \$output -split "\`n" | ForEach-Object {
1760
1798
  \$literal = \$_.StartsWith('__LITERAL__:', [System.StringComparison]::Ordinal)
1761
- \$line = \$_.Trim()
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() }
1762
1802
  if (-not \$line) { return }
1763
1803
  if (\$literal) { \$line = \$line.Substring(12) }
1764
1804
 
@@ -1778,7 +1818,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1778
1818
  # comparing so that Windows backslashes match forward slashes
1779
1819
  # in the transported pattern.
1780
1820
  \$normalizedPattern = if (\$pattern) { \$pattern.Replace('\\', '/') -replace '^\\./','' } else { '' }
1781
- \$normalizedWord = if (\$wordToComplete) { \$wordToComplete.Replace('\\', '/') -replace '^\\./','' } else { '' }
1821
+ \$normalizedWord = if (\$valuePrefix) { \$valuePrefix.Replace('\\', '/') -replace '^\\./','' } else { '' }
1782
1822
  \$comparison = if (\$IsWindows) { [System.StringComparison]::OrdinalIgnoreCase } else { [System.StringComparison]::Ordinal }
1783
1823
  # For wildcard patterns, compare only the directory prefix
1784
1824
  \$comparePattern = \$normalizedPattern
@@ -1790,12 +1830,25 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1790
1830
  \$comparePattern = ''
1791
1831
  }
1792
1832
  }
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
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
1795
1848
  } elseif (\$pattern) {
1796
1849
  \$pattern
1797
- } elseif (\$wordToComplete) {
1798
- \$wordToComplete
1850
+ } elseif (\$valuePrefix) {
1851
+ \$valuePrefix
1799
1852
  } else { '' }
1800
1853
 
1801
1854
  # Use -Force to include hidden files when requested, or when
@@ -1803,15 +1856,22 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1803
1856
  \$prefixBasename = if (\$prefix) { Split-Path -Leaf \$prefix } else { '' }
1804
1857
  \$forceParam = if (\$hidden -or (\$prefixBasename -and \$prefixBasename.StartsWith('.'))) { @{Force = \$true} } else { @{} }
1805
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
+
1806
1865
  # If prefix is a directory without trailing slash, append
1807
1866
  # 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('\\')) {
1867
+ if (\$prefix -and (Test-Path -Path \$lookupPrefix -PathType Container) -and -not \$prefix.EndsWith('/') -and -not \$prefix.EndsWith('\\')) {
1809
1868
  \$prefix = \$prefix + '/'
1869
+ \$lookupPrefix = \$lookupPrefix + '/'
1810
1870
  }
1811
1871
 
1812
1872
  # Build the glob path — when the prefix contains wildcard
1813
1873
  # characters, use it as-is; otherwise append *
1814
- \$globPath = if (\$prefix -match '[\*\?]') { \$prefix } else { "\${prefix}*" }
1874
+ \$globPath = if (\$lookupPrefix -match '[\*\?]') { \$lookupPrefix } else { "\${lookupPrefix}*" }
1815
1875
 
1816
1876
  # Get file system items based on type
1817
1877
  \$items = @()
@@ -1848,7 +1908,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1848
1908
  # For file/any modes with glob patterns, also add directories
1849
1909
  # from the base directory for navigation
1850
1910
  if (\$prefix -match '[\*\?]' -and \$type -ne 'directory') {
1851
- \$globDir = Split-Path -Parent \$prefix
1911
+ \$globDir = Split-Path -Parent \$lookupPrefix
1852
1912
  if (-not \$globDir) { \$globDir = '.' }
1853
1913
  \$extraDirs = Get-ChildItem @forceParam -Directory -Path "\$globDir/*" -ErrorAction SilentlyContinue
1854
1914
  if (\$extraDirs) { \$items = @(\$items) + @(\$extraDirs) | Select-Object -Unique }
@@ -1869,8 +1929,13 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1869
1929
  \$items | ForEach-Object {
1870
1930
  \$completionText = if (\$_.PSIsContainer) { "\$dirPrefix\$(\$_.Name)/" } else { "\$dirPrefix\$(\$_.Name)" }
1871
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 }
1872
1937
  [System.Management.Automation.CompletionResult]::new(
1873
- \$completionText,
1938
+ (& \$quoteCompletion \$replacementText),
1874
1939
  \$completionText,
1875
1940
  'ParameterValue',
1876
1941
  \$itemType
@@ -1880,11 +1945,14 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1880
1945
  # Parse literal completion: text\\tlistItemText\\tdescription
1881
1946
  \$parts = \$line -split "\`t", 3
1882
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 }
1883
1951
  \$listItemText = if (\$parts.Length -gt 1 -and \$parts[1]) { \$parts[1] } else { \$completionText }
1884
1952
  \$toolTip = if (\$parts.Length -gt 2 -and \$parts[2]) { \$parts[2] } else { \$completionText }
1885
1953
 
1886
1954
  [System.Management.Automation.CompletionResult]::new(
1887
- \$completionText,
1955
+ (& \$quoteCompletion \$completionText),
1888
1956
  \$listItemText,
1889
1957
  'ParameterValue',
1890
1958
  \$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.2849",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",