diff --git a/changelog.d/unreleased/1525.fixed.md b/changelog.d/unreleased/1525.fixed.md new file mode 100644 index 0000000000..1d50dfb7f1 --- /dev/null +++ b/changelog.d/unreleased/1525.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 1525 +affected: + - src/CodeIndex/Cli/ConsoleUi.cs + - tests/CodeIndex.Tests/ConsoleUiTests.cs +--- + +## English + +- **`cdidx --completions fish` now lists `--start` / `--end` for `excerpt`, restoring shell parity (#1525)** — the bash and zsh completion scripts already enumerated every `excerpt` flag, but the fish completion omitted `--start` and `--end`, so fish users typing `cdidx excerpt … --st` or `--en` got no suggestion even though both flags are required by the documented `cdidx excerpt --start [--end ] …` signature. The fish script now emits `complete -c cdidx -n '__fish_seen_subcommand_from excerpt' -l start` / `-l end` entries that mirror the bash/zsh tables, and a new `PrintCompletions_ExcerptFlagSetsMatchAcrossShells` test parses each shell's completion output for the `excerpt` branch and asserts the bash, zsh, and fish flag sets are identical (modulo the universal `--help`), so future shell-completion drift is caught at build time instead of in user reports. + +## 日本語 + +- **`cdidx --completions fish` の `excerpt` 補完に `--start` / `--end` を追加し、シェル間で同等の補完を提供するようにしました (#1525)** — bash / zsh の補完スクリプトは `excerpt` の全フラグを列挙していましたが、fish 補完だけ `--start` / `--end` が抜けており、fish ユーザーが `cdidx excerpt … --st` や `--en` と入力しても補完候補が出ませんでした (これらは `cdidx excerpt --start [--end ] …` のドキュメント上必須のフラグです)。fish スクリプトに `complete -c cdidx -n '__fish_seen_subcommand_from excerpt' -l start` / `-l end` を追加して bash / zsh と揃え、さらに新しい `PrintCompletions_ExcerptFlagSetsMatchAcrossShells` テストが各シェルの `excerpt` 分岐から補完対象のフラグ集合を抽出し、bash / zsh / fish のフラグ集合が一致すること (どこでも有効な `--help` は除外) を検証することで、将来の補完スクリプト乖離をユーザー報告ではなくビルド時点で検出できるようにしました。 diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index fd38499f30..a18bfc041e 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -806,6 +806,8 @@ private static string GetFishCompletions() lines.Add("complete -c cdidx -n '__fish_seen_subcommand_from find' -l query -r -d 'Literal query'"); lines.Add("complete -c cdidx -n '__fish_seen_subcommand_from find excerpt' -l before -r -d 'Context lines before'"); lines.Add("complete -c cdidx -n '__fish_seen_subcommand_from find excerpt' -l after -r -d 'Context lines after'"); + lines.Add("complete -c cdidx -n '__fish_seen_subcommand_from excerpt' -l start -r -d 'Start line'"); + lines.Add("complete -c cdidx -n '__fish_seen_subcommand_from excerpt' -l end -r -d 'End line'"); lines.Add("complete -c cdidx -n '__fish_seen_subcommand_from search references excerpt find inspect' -l max-line-width -r -d 'Clamp long single-line payloads (0 disables clamping)'"); lines.Add("complete -c cdidx -n '__fish_seen_subcommand_from excerpt' -l focus-line -r -d 'Focused line to keep visible when clamping'"); lines.Add("complete -c cdidx -n '__fish_seen_subcommand_from excerpt' -l focus-column -r -d 'Focused column to keep visible when clamping'"); diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index 8678041ad6..2cea4dac40 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -1,3 +1,4 @@ +using System.Text.RegularExpressions; using CodeIndex.Cli; namespace CodeIndex.Tests; @@ -293,6 +294,72 @@ public void PrintCompletions_BashAndZshKeepFocusOptionsExcerptOnly(string shell) } } + [Fact] + public void PrintCompletions_ExcerptFlagSetsMatchAcrossShells() + { + var bashExcerpt = ExtractBashSubcommandFlags(ConsoleUi.GetCompletionScript("bash"), "excerpt", "references"); + var zshExcerpt = ExtractZshSubcommandFlags(ConsoleUi.GetCompletionScript("zsh"), "excerpt", "references"); + var fishExcerpt = ExtractFishSubcommandFlags(ConsoleUi.GetCompletionScript("fish"), "excerpt"); + + // --help is universal in bash but is not enumerated by the zsh/fish scripts; + // exclude it from the parity comparison so the flag sets line up cleanly. + bashExcerpt.Remove("help"); + + Assert.Equal(bashExcerpt, zshExcerpt); + Assert.Equal(bashExcerpt, fishExcerpt); + // Sanity check: required excerpt flags are present in every shell. + foreach (var flag in new[] { "db", "json", "start", "end", "before", "after", "max-line-width", "focus-line", "focus-column", "focus-length" }) + { + Assert.Contains(flag, bashExcerpt); + Assert.Contains(flag, zshExcerpt); + Assert.Contains(flag, fishExcerpt); + } + } + + private static SortedSet ExtractBashSubcommandFlags(string script, string subcommand, string nextSubcommand) + { + var startMarker = $"[ \"$cmd\" = \"{subcommand}\" ]; then"; + var endMarker = $"[ \"$cmd\" = \"{nextSubcommand}\" ]; then"; + var branch = ExtractBetween(script, startMarker, endMarker); + var quoted = Regex.Match(branch, "compgen -W \"(?[^\"]*)\""); + Assert.True(quoted.Success, $"bash branch for {subcommand} did not contain a compgen list"); + var flags = new SortedSet(StringComparer.Ordinal); + foreach (var token in quoted.Groups["flags"].Value.Split(' ', StringSplitOptions.RemoveEmptyEntries)) + { + if (token.StartsWith("--", StringComparison.Ordinal) && token.Length > 2) + flags.Add(token[2..]); + } + return flags; + } + + private static SortedSet ExtractZshSubcommandFlags(string script, string subcommand, string nextSubcommand) + { + var startMarker = $"[[ $subcmd == {subcommand} ]]; then"; + var endMarker = $"[[ $subcmd == {nextSubcommand} ]]; then"; + var branch = ExtractBetween(script, startMarker, endMarker); + var flags = new SortedSet(StringComparer.Ordinal); + foreach (Match match in Regex.Matches(branch, @"'--(?[a-z][a-z0-9-]*)\[")) + flags.Add(match.Groups["name"].Value); + return flags; + } + + private static SortedSet ExtractFishSubcommandFlags(string script, string subcommand) + { + var flags = new SortedSet(StringComparer.Ordinal); + var pattern = new Regex($@"__fish_seen_subcommand_from\s+(?[^']+)'\s+-l\s+(?[a-z][a-z0-9-]*)\b"); + foreach (var line in script.Split('\n')) + { + var match = pattern.Match(line); + if (!match.Success) + continue; + var subcmds = match.Groups["list"].Value.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (Array.IndexOf(subcmds, subcommand) < 0) + continue; + flags.Add(match.Groups["flag"].Value); + } + return flags; + } + [Fact] public void GetCompletionLangs_IncludesWindowsBatchAndYamlAliases() {