From 0c0f55fc130e9f9a84de78c5733762c9d00a18af Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 12 Jun 2026 00:11:06 +0900 Subject: [PATCH 1/5] Fix search max results alias (#3521) --- USER_GUIDE.md | 10 +-- changelog.d/unreleased/3521.fixed.md | 19 +++++ src/CodeIndex/Cli/CliFlagSchema.cs | 1 + src/CodeIndex/Cli/ConsoleUi.cs | 5 +- src/CodeIndex/Cli/QueryCommandRunner.cs | 72 ++++++++++++++++++- .../QueryCommandRunnerSearchTests.cs | 39 ++++++++++ 6 files changed, 137 insertions(+), 9 deletions(-) create mode 100644 changelog.d/unreleased/3521.fixed.md diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 4c49a86911..6065211732 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -393,7 +393,7 @@ be audited whenever the matching help text changes. | Setting | Current default | Source of truth | |---|---|---| -| Query/diff result limit | `20` (`--limit`, alias `--top` for query commands), max `10000` | CLI help and query runners | +| Query/diff result limit | `20` (`--limit`, aliases `--top` for query commands and `--max-results` for `search`), max `10000` | CLI help and query runners | | Search snippet lines | `8` (`--snippet-lines`, max `20`) | CLI help and search runner | | Max line width | `512` (`--max-line-width`, `0` disables) | `LineWidthFormatter.DefaultMaxLineWidth` | | Index max file size | `4MiB` unless `CDIDX_MAX_FILE_BYTES` is set | index runner help | @@ -1229,7 +1229,7 @@ same source location. | `--open-issues ` | `suggestions export --format issue-drafts` | Preflight drafts against an open-issues JSON file such as `gh issue list --state open --json number,title,labels,url`. Inputs are capped at 8 MiB, 32 JSON nesting levels, 1000 issue entries, 32 labels per issue, and bounded title/URL/label strings. | | `--check` | `status` | Verify that `.cdidx/codeindex.db` exactly matches the current indexable workspace by comparing DB file paths/checksums against a fresh filesystem scan. Matching indexes exit `0`; stale indexes exit `5`. | | `--dry-run` | `index` | Scan files and report what would change without writing to the database | -| `--limit ` | Query result commands except `suggestions` | Max results (default: 20, max: 10000; `map` uses it per section) | +| `--limit ` / `--max-results ` | Query result commands except `suggestions`; `--max-results` is `search` only | Max results (default: 20, max: 10000; `map` uses it per section) | | `--lang ` | Query commands | Filter by language (case-insensitive; `--lang Python` is treated as `--lang python`). Common aliases such as `c#`, `cs`, `kt`, and `kts` are also accepted. Unknown values emit an `Available: ` hint on zero-result responses in human-readable output. | | `--visibility ` | `definition`, `symbols`, `unused`, `hotspots` | Include only symbols with the requested visibility values: `public`, `protected`, `internal`, `private`. `public` matches stored exported aliases such as `pub`, `open`, and `export`; `private` also matches `fileprivate`. | | `--exclude-visibility ` | `definition`, `symbols`, `unused`, `hotspots` | Exclude symbols with the requested visibility values. Accepts the same comma-separated values and alias expansion as `--visibility`. | @@ -1282,6 +1282,7 @@ same source location. | `--reverse` | `deps` | Reverse lookup: show files that depend ON the matched path | | `--strict-not-found` | Query commands | Return exit code `2` when a valid query produces zero rows. Without this flag, zero-result queries exit `0` and keep their normal empty/zero-result output. | | `--top ` | Query commands | Alias for `--limit` | +| `--max-results ` | `search` | Alias for `--limit` | | `--color ` | All commands | Control ANSI color output. Accepts `auto` (default), `always`, or `never`. Precedence: `--color` flag > `CLICOLOR_FORCE` > `NO_COLOR` > `CLICOLOR=0` > terminal capability auto-detect. Auto mode treats redirected stdout and StringWriter-style test capture as non-ANSI; on Windows it also accepts ConPTY/Windows Terminal virtual-terminal support and terminal hints such as `WT_SESSION`, `WT_PROFILE_ID`, `TERM_PROGRAM`, or non-`dumb` `TERM`. Use `--color=always` to keep colored kind labels through a pager such as `cdidx symbols Foo \| less -R`; use `--color=never` (or `NO_COLOR=1`) to suppress ANSI even on a TTY. | | `--palette ` | All commands | Choose the ANSI palette used when color output is enabled. Accepts `basic` (8-color SGR 30–37, the default fallback for minimal SSH/CI terminals), `256` (256-color `\x1b[38;5;Nm`), or `truecolor` (24-bit RGB `\x1b[38;2;R;G;Bm`). Precedence: `--palette` flag > `CDIDX_COLOR_PALETTE` env var > `COLORTERM` / `TERM` auto-detect. The basic palette avoids `\x1b[90m` (bright-black / dim), which is unreadable on many minimal terminals. | | `--metrics ` | All commands (and MCP tool calls) | Append one JSONL metrics record per CLI command / MCP tool call to ``. The `CDIDX_METRICS=` environment variable provides the same destination as a fallback when the flag is not passed. Best-effort: any IO failure (missing directory, read-only mount, etc.) is swallowed silently and never breaks the underlying command. | @@ -2682,7 +2683,7 @@ render できます。 | 設定 | 現在の既定値 | Source of truth | |---|---|---| -| Query/diff result limit | `20`(`--limit`、query command では alias `--top`)、最大 `10000` | CLI help と query runners | +| Query/diff result limit | `20`(`--limit`、query command では alias `--top`、`search` では `--max-results`)、最大 `10000` | CLI help と query runners | | Search snippet lines | `8`(`--snippet-lines`、最大 `20`) | CLI help と search runner | | Max line width | `512`(`--max-line-width`、`0` で無効) | `LineWidthFormatter.DefaultMaxLineWidth` | | Index max file size | `CDIDX_MAX_FILE_BYTES` 未設定時は `4MiB` | index runner help | @@ -3527,7 +3528,7 @@ raw match density を正確に測る、といった理由で全 raw chunk hit | `--open-issues ` | `suggestions export --format issue-drafts` | `gh issue list --state open --json number,title,labels,url` などの open issue JSON と照合して draft を事前重複確認します。入力は 8 MiB、JSON ネスト 32 段、issue entry 1000 件、issue ごとの label 32 件、title / URL / label 文字列長の上限に制限されます。 | | `--check` | `status` | DB のファイル path/checksum と現在の index 対象 workspace を比較し、`.cdidx/codeindex.db` が完全一致するか確認。完全一致なら終了コード `0`、stale なら `5` | | `--dry-run` | `index` | DB に書き込まず、どの変更が発生するかだけを走査して報告 | -| `--limit ` | `suggestions` 以外のクエリ結果コマンド | 最大結果数(デフォルト: 20、最大: 10000。`map` では各セクションごとの件数) | +| `--limit ` / `--max-results ` | `suggestions` 以外のクエリ結果コマンド。`--max-results` は `search` のみ | 最大結果数(デフォルト: 20、最大: 10000。`map` では各セクションごとの件数) | | `--visibility ` | `definition`, `symbols`, `unused`, `hotspots` | `public`, `protected`, `internal`, `private` の可視性でシンボルを絞り込む。`public` は `pub`、`open`、`export` などの保存済み exported alias にも一致し、`private` は `fileprivate` にも一致 | | `--exclude-visibility ` | `definition`, `symbols`, `unused`, `hotspots` | 指定した可視性のシンボルを除外する。値と alias 展開は `--visibility` と同じ | | `--path ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect`, `validate` | glob 形式のパスパターンで結果を絞る。`*` と `?` がワイルドカード。繰り返し指定可(複数値は OR で結合) | @@ -3577,6 +3578,7 @@ raw match density を正確に測る、といった理由で全 raw chunk hit | `--reverse` | `deps` | 逆引き: 指定パスに依存しているファイルを表示 | | `--workspace-db ` | `deps` | file dependency query に別の CodeIndex DB を追加する。最大 7 個の distinct な追加 DB(`--db` を含め合計 8 個)まで繰り返し指定でき、JSON edge には同じ相対パスを区別できるよう `source_db` / `target_db` が含まれる。 | | `--top ` | クエリ系 | `--limit` のエイリアス | +| `--max-results ` | `search` | `--limit` のエイリアス | | `--color ` | 全コマンド | ANSI カラー出力の制御。`auto`(既定)、`always`、`never` を受け付ける。優先順位: `--color` フラグ > `CLICOLOR_FORCE` > `NO_COLOR` > `CLICOLOR=0` > 端末能力の自動判定。auto では redirected stdout と StringWriter 風のテスト capture を非 ANSI とみなし、Windows では ConPTY / Windows Terminal の virtual-terminal 対応と `WT_SESSION`、`WT_PROFILE_ID`、`TERM_PROGRAM`、非 `dumb` の `TERM` などの端末ヒントも見る。`cdidx symbols Foo \| less -R` のような pager pipe でも色を維持したい場合は `--color=always`、TTY 上でも ANSI を抑止したい場合は `--color=never`(または `NO_COLOR=1`)を指定する。 | | `--palette ` | 全コマンド | カラー出力が有効なときに用いる ANSI パレットを選択する。`basic`(標準8色 SGR 30–37、最小 SSH/CI 端末向けの既定フォールバック)、`256`(256色 `\x1b[38;5;Nm`)、`truecolor`(24ビット RGB `\x1b[38;2;R;G;Bm`)を受け付ける。優先順位: `--palette` フラグ > `CDIDX_COLOR_PALETTE` 環境変数 > `COLORTERM` / `TERM` 自動判定。`basic` パレットは最小端末で読みにくい `\x1b[90m`(暗灰 / dim)を避ける。 | | `--metrics ` | 全コマンド(および MCP ツール呼び出し) | CLI コマンド / MCP ツール呼び出し 1 回ごとに JSONL レコードを 1 行ずつ `` に追記する。フラグ未指定時のフォールバックとして `CDIDX_METRICS=` 環境変数でも同じ出力先を指定できる。Best-effort のため、ディレクトリが無い・read-only マウント等の IO 失敗は黙って握り潰し、本体コマンドを壊さない。 | diff --git a/changelog.d/unreleased/3521.fixed.md b/changelog.d/unreleased/3521.fixed.md new file mode 100644 index 0000000000..3c2e2062e0 --- /dev/null +++ b/changelog.d/unreleased/3521.fixed.md @@ -0,0 +1,19 @@ +--- +category: fixed +issues: + - 3521 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - src/CodeIndex/Cli/CliFlagSchema.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs + - USER_GUIDE.md +--- + +## English + +- **Search accepts `--max-results` and guides dash-prefixed queries (#3521)** — `cdidx search` now treats `--max-results` as a `--limit` alias and tells users how to pass a literal query such as `--profile` through `--query` or `--`. + +## 日本語 + +- **`search` が `--max-results` と dash 始まりの query を案内するようになりました (#3521)** — `cdidx search` は `--max-results` を `--limit` のエイリアスとして扱い、`--profile` のような literal query を `--query` または `--` で渡す方法を案内します。 diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index 9593377625..250eb709b1 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -244,6 +244,7 @@ private static IReadOnlyList BuildAll() new() { Name = "--slow-query-ms", ValuePlaceholder = "", Description = "Log profiled SQL statements at or above this millisecond threshold", Commands = Set(ProfileCommands) }, new() { Name = "--trace", ValuePlaceholder = "", Description = "Emit one structured JSON query trace line to stderr or a daily log file", Commands = Set(TraceCommands) }, new() { Name = "--limit", ValuePlaceholder = "", Description = "Max results", Commands = Set(LimitCapableCommands.Concat(new[] { "suggestions" }).ToArray()) }, + new() { Name = "--max-results", ValuePlaceholder = "", Description = "Search alias for --limit", Commands = Set("search") }, new() { Name = "--top", ValuePlaceholder = "", Description = "Max results", Commands = Set(LimitCapableCommands) }, new() { Name = "--offset", ValuePlaceholder = "", Description = "Suggestions: skip this many filtered rows before output", Commands = Set("suggestions") }, new() { Name = "--lang", ValuePlaceholder = "", Description = "Filter by language", Commands = Set(LangCapableCommands) }, diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index ba31188742..cbd39fef0e 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -87,7 +87,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("index-commits", "cdidx index --commits [commit-ref ...] [--db ] [--verbose] [--dry-run] [--json] [--memory-trace] [--duration-format ] [--max-file-bytes ] [--include-symbol-kind [,]] [--exclude-symbol-kind [,]]"), ("index-changed-between", "cdidx index --changed-between [--db ] [--verbose] [--dry-run] [--json] [--memory-trace] [--duration-format ] [--max-file-bytes ] [--include-symbol-kind [,]] [--exclude-symbol-kind [,]]"), ("index-files", "cdidx index --files [path ...] [--db ] [--verbose] [--dry-run] [--json] [--memory-trace] [--duration-format ] [--max-file-bytes ] [--include-symbol-kind [,]] [--exclude-symbol-kind [,]]"), - ("search", "cdidx search |--query |-- |--recipe |--list-recipes [--db ] [--json[=ndjson|array]] [--pretty] [--format ] [--open-issues ] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--snippet-lines ] [--snippet-focus ] [--max-line-width ] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--since ] [--no-dedup] [--no-visibility-rank] [--require-before ] [--require-after ] [--reject-before ] [--reject-after ] [--guard-window ]"), + ("search", "cdidx search |--query |-- |--recipe |--list-recipes [--db ] [--json[=ndjson|array]] [--pretty] [--format ] [--open-issues ] [--verbose] [--limit |--top |--max-results ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--snippet-lines ] [--snippet-focus ] [--max-line-width ] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--since ] [--no-dedup] [--no-visibility-rank] [--require-before ] [--require-after ] [--reject-before ] [--reject-after ] [--guard-window ]"), ("definition", "cdidx definition |--query |-- [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--visibility ] [--exclude-visibility ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--exact|--exact-name] [--count] [--since ]"), ("goto", "cdidx goto |--query |-- [--db ] [--json] [--limit |--top ] [--lang ] [--kind ] [--path ] [--exclude-path ] [--exclude-tests] [--exact|--exact-name] [--all]"), ("references", "cdidx references |--query |-- [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--snippet-lines ] [--max-line-width ] [--exact|--exact-name] [--count]"), @@ -1028,7 +1028,8 @@ private static void PrintFlagReference(Action WriteHelpLine) WriteHelpLine(" --quiet, -q, --silent Query commands: suppress informational stderr output, including zero-result hints and summaries; errors still print. Overrides --verbose stderr text."); WriteHelpLine(" --profile Read commands: append SQL timing, row-count, and EXPLAIN QUERY PLAN JSON after the normal result"); WriteHelpLine(" --slow-query-ms Read commands: log profiled SQL statements that take at least ms (use 0 to log every statement)"); - Console.WriteLine(" --limit , --top Max results to return (default: 20)"); + Console.WriteLine(" --limit , --top , --max-results "); + Console.WriteLine(" Max results to return (default: 20)"); Console.WriteLine(" --lang Filter by language (aliases: bat, cmd, cshtml, razor, ts, tsx, cts, mts)"); Console.WriteLine(" --path Restrict matches to glob-style path patterns (* and ?)"); WriteHelpLine($" --query Pass a query literal, useful when the query starts with '-' (`search`/`find` max {QueryLimits.MaxQueryLength} chars)"); diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 3b0a8f6536..859b7b985d 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -104,6 +104,7 @@ public static class QueryCommandRunner "--db", "--data-dir", "--limit", + "--max-results", "--top", "--lang", "--kind", @@ -608,7 +609,7 @@ public static int RunSearch(string[] cmdArgs, JsonSerializerOptions jsonOptions) WriteUsageError( "search requires a query argument", GetUsageLineOrThrow("search"), - "Add the text you want to search for after the command, for example: `cdidx search authenticate`."); + BuildMissingSearchQueryHint(cmdArgs)); return CommandExitCodes.UsageError; } if (options.Query.Length > QueryLimits.MaxQueryLength) @@ -6388,10 +6389,11 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) } break; case "--limit": + case "--max-results": case "--top": - if (!TryReadRawOptionValue(args, ref i, "--limit", inlineValue, out var limitValue, out var missingLimitError)) + if (!TryReadRawOptionValue(args, ref i, normalizedArg, inlineValue, out var limitValue, out var missingLimitError)) AddParseError(missingLimitError!); - else if (TryParsePositiveInt(limitValue!, "--limit", out var parsedLimit, out var limitError)) + else if (TryParsePositiveInt(limitValue!, normalizedArg, out var parsedLimit, out var limitError)) { WarnIfDuplicateSingleValueOption("--limit", limitValue!); limit = parsedLimit; @@ -8258,6 +8260,69 @@ private static bool TryWriteUnexpectedPositionals(string commandName, QueryComma return true; } + private static string BuildMissingSearchQueryHint(string[] cmdArgs) + { + var candidate = FindOptionLookingSearchLiteralCandidate(cmdArgs); + if (candidate != null) + { + var display = ConsoleUi.FormatBoundedValue(candidate); + return $"Add the text you want to search for after the command. If you meant to search for `{display}`, pass it as `--query \"{display}\"` or after `--`, for example: `cdidx search -- \"{display}\"`."; + } + + return "Add the text you want to search for after the command, for example: `cdidx search authenticate`. If the query itself starts with `--`, pass it as `--query \"--profile\"` or after `--`, for example: `cdidx search -- \"--profile\"`."; + } + + private static string? FindOptionLookingSearchLiteralCandidate(string[] cmdArgs) + { + for (var i = 0; i < cmdArgs.Length; i++) + { + var arg = cmdArgs[i]; + if (arg == "--") + return i + 1 < cmdArgs.Length && cmdArgs[i + 1].StartsWith("-", StringComparison.Ordinal) + ? cmdArgs[i + 1] + : null; + + var inlineValue = TrySplitInlineOptionValue(arg, out var inlineOptionName) + ? arg[(inlineOptionName!.Length + 1)..] + : null; + var normalizedArg = inlineOptionName ?? arg; + if (ValueTakingOptions.Contains(normalizedArg)) + { + if (inlineValue == null) + i++; + continue; + } + + if (!arg.StartsWith("--", StringComparison.Ordinal)) + continue; + if (SearchMissingQueryControlFlags.Contains(normalizedArg)) + continue; + + return arg; + } + + return null; + } + + private static readonly HashSet SearchMissingQueryControlFlags = + [ + "--exact", + "--exact-name", + "--exact-substring", + "--prefix", + "--fts", + "--json", + "--pretty", + "--count", + "--no-dedup", + "--no-visibility-rank", + "--exclude-tests", + "--strict-not-found", + "--verbose", + "--quiet", + "--silent", + ]; + private static string GetUsageLineOrThrow(string commandName) => ConsoleUi.GetUsageLine(commandName) ?? throw new InvalidOperationException($"Missing usage line for command '{commandName}'."); @@ -9615,6 +9680,7 @@ private static void WriteSqlGraphContractWarningIfNeeded(bool json, SqlGraphCont new Dictionary(StringComparer.Ordinal) { ["--limit"] = 10_000, + ["--max-results"] = 10_000, ["--snippet-lines"] = SearchSnippetFormatter.MaxSnippetLines, ["--max-line-width"] = LineWidthFormatter.MaxAllowedLineWidth, ["--slow-query-ms"] = 3_600_000, diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs index 8f1cd7d55d..50ea836452 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs @@ -40,6 +40,45 @@ public void RunSearch_FormatCompactEmitsFileLineOnly_Issue1642() } } + [Fact] + public void RunSearch_MaxResultsAliasLimitsSearchResults_Issue3521() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_runner_search_max_results_3521"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/one.cs", "csharp", "class One { string Value = \"needle\"; }"); + TestProjectHelper.InsertIndexedFile(dbPath, "src/two.cs", "csharp", "class Two { string Value = \"needle\"; }"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["needle", "--db", dbPath, "--json=array", "--max-results", "1"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Single(document.RootElement.EnumerateArray()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_MissingDashLiteralQuerySuggestsEscapes_Issue3521() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["--exact", "--profile", "--limit", "5"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("Error: search requires a query argument", stderr); + Assert.Contains("`--query \"--profile\"`", stderr); + Assert.Contains("`cdidx search -- \"--profile\"`", stderr); + } + [Fact] public void RunSearch_FormatLspEmitsLocationArray() { From af2034130c34b1c157fff520172da16214304074 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 12 Jun 2026 00:17:34 +0900 Subject: [PATCH 2/5] Improve search path glob guidance (#3445) --- USER_GUIDE.md | 4 ++-- changelog.d/unreleased/3445.fixed.md | 17 +++++++++++++++++ src/CodeIndex/Cli/QueryCommandRunner.cs | 17 ++++++++++++++++- .../QueryCommandRunnerSearchTests.cs | 13 +++++++++++++ 4 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 changelog.d/unreleased/3445.fixed.md diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 6065211732..374717f360 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -1233,7 +1233,7 @@ same source location. | `--lang ` | Query commands | Filter by language (case-insensitive; `--lang Python` is treated as `--lang python`). Common aliases such as `c#`, `cs`, `kt`, and `kts` are also accepted. Unknown values emit an `Available: ` hint on zero-result responses in human-readable output. | | `--visibility ` | `definition`, `symbols`, `unused`, `hotspots` | Include only symbols with the requested visibility values: `public`, `protected`, `internal`, `private`. `public` matches stored exported aliases such as `pub`, `open`, and `export`; `private` also matches `fileprivate`. | | `--exclude-visibility ` | `definition`, `symbols`, `unused`, `hotspots` | Exclude symbols with the requested visibility values. Accepts the same comma-separated values and alias expansion as `--visibility`. | -| `--path ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect`, `validate` | Restrict results to glob-style path patterns. `*` and `?` are wildcards. Repeatable; multiple values are OR'd together | +| `--path ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect`, `validate` | Restrict results to glob-style path patterns. `*` and `?` are wildcards. Repeatable; multiple values are OR'd together. Quote shell globs such as `--path 'src/**'` so the shell passes one literal pattern. | | `--query ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `inspect`, `impact` | Pass a query literal explicitly, useful when the query starts with `-`. Query commands except `find` also accept `-- ` as a one-token query escape while continuing to parse later options. | | `--recipe ` | `search` | Run a reusable audit recipe such as `risky-code`. Normal search filters and snippet controls apply to every recipe query; text, `--json` / `--format json`, and `--format issue-drafts` are supported. | | `--list-recipes` | `search` | List available search audit recipes with query text, recommended labels, exact-match mode, and false-positive guidance. | @@ -3531,7 +3531,7 @@ raw match density を正確に測る、といった理由で全 raw chunk hit | `--limit ` / `--max-results ` | `suggestions` 以外のクエリ結果コマンド。`--max-results` は `search` のみ | 最大結果数(デフォルト: 20、最大: 10000。`map` では各セクションごとの件数) | | `--visibility ` | `definition`, `symbols`, `unused`, `hotspots` | `public`, `protected`, `internal`, `private` の可視性でシンボルを絞り込む。`public` は `pub`、`open`、`export` などの保存済み exported alias にも一致し、`private` は `fileprivate` にも一致 | | `--exclude-visibility ` | `definition`, `symbols`, `unused`, `hotspots` | 指定した可視性のシンボルを除外する。値と alias 展開は `--visibility` と同じ | -| `--path ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect`, `validate` | glob 形式のパスパターンで結果を絞る。`*` と `?` がワイルドカード。繰り返し指定可(複数値は OR で結合) | +| `--path ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect`, `validate` | glob 形式のパスパターンで結果を絞る。`*` と `?` がワイルドカード。繰り返し指定可(複数値は OR で結合)。`--path 'src/**'` のように shell glob を引用し、shell が 1 つの literal pattern として渡すようにする。 | | `--query ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `inspect`, `impact` | クエリを明示的なリテラルとして渡す。クエリが `-` で始まる場合に有用。`find` 以外のクエリ系コマンドでは `-- ` も1トークンのクエリエスケープとして受け付け、その後のオプション解析を続ける。 | | `--recipe ` | `search` | `risky-code` などの再利用可能な audit recipe を実行する。通常の search filter と snippet control は recipe 内の各 query に適用され、text、`--json` / `--format json`、`--format issue-drafts` に対応する。 | | `--list-recipes` | `search` | 利用可能な search audit recipe を query text、推奨 label、exact-match mode、false-positive guidance 付きで一覧表示する。 | diff --git a/changelog.d/unreleased/3445.fixed.md b/changelog.d/unreleased/3445.fixed.md new file mode 100644 index 0000000000..c6d5676d8a --- /dev/null +++ b/changelog.d/unreleased/3445.fixed.md @@ -0,0 +1,17 @@ +--- +category: fixed +issues: + - 3445 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs + - USER_GUIDE.md +--- + +## English + +- **Search explains expanded `--path` globs (#3445)** — when a shell-expanded path glob leaves extra path-like positional arguments, `cdidx search` now suggests quoting `--path` so one literal glob reaches the CLI. + +## 日本語 + +- **`search` が展開済み `--path` glob の対処を案内するようになりました (#3445)** — shell 展開された path glob が余剰の path 風 positional argument として残った場合、`cdidx search` は `--path` を引用して 1 つの literal glob として渡すよう案内します。 diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 859b7b985d..6e4c07ef83 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -8239,11 +8239,26 @@ private static bool TryWriteUnexpectedExtraPositionals(string commandName, Query CommandErrorWriter.Write( $"unexpected extra positional {ConsoleUi.Counted(options.ExtraNames.Count, "argument")} for {commandName}: {string.Join(", ", options.ExtraNames.Select(name => $"`{name}`"))}.", - "quote multi-word queries as a single argument, or remove the extra positional values.", + BuildUnexpectedExtraPositionalsHint(commandName, options), GetUsageLineOrThrow(commandName)); return true; } + private static string BuildUnexpectedExtraPositionalsHint(string commandName, QueryCommandOptions options) + { + if (string.Equals(commandName, "search", StringComparison.Ordinal) + && options.PathPatterns.Count > 0 + && options.ExtraNames.Any(IsPathLikeArgument)) + { + return "quote --path globs so the shell passes one literal pattern, e.g. `--path 'src/CodeIndex/**'`; remove the expanded path arguments and rerun."; + } + + return "quote multi-word queries as a single argument, or remove the extra positional values."; + } + + private static bool IsPathLikeArgument(string value) => + value.Contains('/') || value.Contains('\\'); + private static bool TryWriteUnexpectedPositionals(string commandName, QueryCommandOptions options) { var unexpected = new List(); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs index 50ea836452..3454dbd1a0 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs @@ -79,6 +79,19 @@ public void RunSearch_MissingDashLiteralQuerySuggestsEscapes_Issue3521() Assert.Contains("`cdidx search -- \"--profile\"`", stderr); } + [Fact] + public void RunSearch_PathGlobExpansionHintSuggestsQuotedPath_Issue3445() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["needle", "--path", "src/CodeIndex/Cli", "src/CodeIndex/Database"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("Error: unexpected extra positional 1 argument for search: `src/CodeIndex/Database`.", stderr); + Assert.Contains("quote --path globs so the shell passes one literal pattern", stderr); + Assert.Contains("`--path 'src/CodeIndex/**'`", stderr); + } + [Fact] public void RunSearch_FormatLspEmitsLocationArray() { From 1ce921237d47d8ee6b5980b1c3fdec117293a51d Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 12 Jun 2026 00:34:32 +0900 Subject: [PATCH 3/5] Strengthen punctuation search hint (#3386) --- USER_GUIDE.md | 4 ++++ changelog.d/unreleased/3386.fixed.md | 18 ++++++++++++++++++ src/CodeIndex/Cli/SearchQueryAdvisor.cs | 6 ++++-- tests/CodeIndex.Tests/McpServerTests.cs | 3 +++ .../QueryCommandRunnerSearchHintTests.cs | 7 ++++++- 5 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 changelog.d/unreleased/3386.fixed.md diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 374717f360..853c39b3e7 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -1190,6 +1190,8 @@ Default `cdidx search` is literal-safe unless you explicitly opt into raw FTS5: | Raw FTS5 mode | With `--fts`, the query is passed to FTS5 as raw syntax. Supported operators include `content:term` column filters, `NEAR(foo bar, 5)`, `foo OR bar`, `foo NOT bar`, parenthesized groups, prefix tokens such as `foo*`, and quoted phrases such as `"foo bar"`. | | No `--fts` | Operator-like characters are treated as literal query content except for cdidx's documented literal-safe prefix shorthand. | +For punctuation-heavy code phrases such as `catch { }`, normal search may emit a rerun hint. Use `--exact-substring` when braces, operators, punctuation, and case need byte-for-byte matching. + Search case behavior depends on the selected mode: | Mode or text | Case and token behavior | @@ -3490,6 +3492,8 @@ cdidx report --output report.tgz --json | raw FTS5 mode | `--fts` 付きでは query を raw FTS5 構文としてそのまま渡します。利用できる演算子には `content:term` の列 filter、`NEAR(foo bar, 5)`、`foo OR bar`、`foo NOT bar`、括弧 grouping、`foo*` のような prefix token、`"foo bar"` のような quoted phrase があります。 | | `--fts` なし | cdidx が明示している literal-safe prefix shorthand を除き、operator に見える文字も literal な query 内容として扱います。 | +`catch { }` のように記号の多いコード片では、通常検索が再実行ヒントを出す場合があります。brace、operator、punctuation、大文字小文字まで byte-for-byte に一致させたい場合は `--exact-substring` を使います。 + 検索の大小文字の扱いは mode ごとに異なります: | mode / text | 大小文字と token の扱い | diff --git a/changelog.d/unreleased/3386.fixed.md b/changelog.d/unreleased/3386.fixed.md new file mode 100644 index 0000000000..01fdfd200e --- /dev/null +++ b/changelog.d/unreleased/3386.fixed.md @@ -0,0 +1,18 @@ +--- +category: fixed +issues: + - 3386 +affected: + - src/CodeIndex/Cli/SearchQueryAdvisor.cs + - tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs + - tests/CodeIndex.Tests/McpServerTests.cs + - USER_GUIDE.md +--- + +## English + +- **Punctuation-heavy search hints now include a concrete exact-substring rerun path (#3386)** — CLI and MCP search recovery hints now tell users to rerun punctuation-heavy code phrases with `--exact-substring` or `exactSubstring=true`. + +## 日本語 + +- **記号の多い検索ヒントが exact-substring の再実行方法を具体的に案内するようになりました (#3386)** — CLI と MCP の search recovery hint は、記号の多いコード片を `--exact-substring` または `exactSubstring=true` で再実行するよう案内します。 diff --git a/src/CodeIndex/Cli/SearchQueryAdvisor.cs b/src/CodeIndex/Cli/SearchQueryAdvisor.cs index 99fe2df2c5..e480bcdf26 100644 --- a/src/CodeIndex/Cli/SearchQueryAdvisor.cs +++ b/src/CodeIndex/Cli/SearchQueryAdvisor.cs @@ -3,8 +3,10 @@ namespace CodeIndex.Cli; internal static class SearchQueryAdvisor { internal const string ExactSubstringHintReason = "punctuation_heavy_query"; - internal const string CliExactSubstringSuggestedAction = "This looks like a literal code phrase; try --exact-substring for punctuation-sensitive matching."; - internal const string McpExactSubstringSuggestedAction = "This looks like a literal code phrase; try exactSubstring for punctuation-sensitive matching."; + internal const string CliExactSubstringSuggestedAction = + "This looks like a literal code phrase; rerun with `--exact-substring`, for example: `cdidx search --exact-substring --query \"...\"`, for punctuation-sensitive matching."; + internal const string McpExactSubstringSuggestedAction = + "This looks like a literal code phrase; rerun the search with exactSubstring=true for punctuation-sensitive matching."; internal static SearchQueryHint? BuildExactSubstringHint(string? query, bool rawQuery, bool exact, bool prefix) => ShouldSuggestExactSubstring(query, rawQuery, exact, prefix) diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index 798a93e3f6..5e0402fed8 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -4463,6 +4463,9 @@ public void ToolsCall_Search_PunctuationHeavyQueryAddsExactSubstringRecoveryHint var recoveryHint = response["result"]!["structuredContent"]!["recovery_hint"]!; Assert.Equal("punctuation_heavy_query", recoveryHint["reason"]!.GetValue()); + Assert.Equal( + "This looks like a literal code phrase; rerun the search with exactSubstring=true for punctuation-sensitive matching.", + recoveryHint["suggested_action"]!.GetValue()); Assert.Equal("search", recoveryHint["tool"]!.GetValue()); Assert.Equal("CommandText = $", recoveryHint["args"]!["query"]!.GetValue()); Assert.True(recoveryHint["args"]!["exactSubstring"]!.GetValue()); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs index 7b70c43655..b7545690de 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs @@ -59,7 +59,9 @@ public void RunSearch_PunctuationHeavyTextSuggestsExactSubstring() Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Contains("src/sql.cs", stdout); - Assert.Contains("Hint: This looks like a literal code phrase; try --exact-substring for punctuation-sensitive matching.", stderr); + Assert.Contains( + "Hint: This looks like a literal code phrase; rerun with `--exact-substring`, for example: `cdidx search --exact-substring --query \"...\"`, for punctuation-sensitive matching.", + stderr); } finally { @@ -90,6 +92,9 @@ public void RunSearch_PunctuationHeavyJsonAddsExactSubstringHint() Assert.Equal(CommandExitCodes.Success, exitCode); Assert.Equal(string.Empty, stderr); Assert.Equal("punctuation_heavy_query", hint.GetProperty("reason").GetString()); + Assert.Equal( + "This looks like a literal code phrase; rerun with `--exact-substring`, for example: `cdidx search --exact-substring --query \"...\"`, for punctuation-sensitive matching.", + hint.GetProperty("suggested_action").GetString()); Assert.Equal("--exact-substring", hint.GetProperty("flag").GetString()); Assert.Equal("exactSubstring", hint.GetProperty("mcp_argument").GetString()); } From fef3c5e33d4fcb9ad635bebf87ffa0838156cfcc Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 12 Jun 2026 00:54:57 +0900 Subject: [PATCH 4/5] Boost multiword search phrases (#3389) --- USER_GUIDE.md | 4 +++ changelog.d/unreleased/3389.fixed.md | 17 +++++++++ src/CodeIndex/Database/DbSearchReader.cs | 27 ++++++++++---- .../QueryCommandRunnerSearchTests.cs | 36 +++++++++++++++++++ 4 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 changelog.d/unreleased/3389.fixed.md diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 853c39b3e7..d2c93f9148 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -1192,6 +1192,8 @@ Default `cdidx search` is literal-safe unless you explicitly opt into raw FTS5: For punctuation-heavy code phrases such as `catch { }`, normal search may emit a rerun hint. Use `--exact-substring` when braces, operators, punctuation, and case need byte-for-byte matching. +For whitespace-containing literal queries passed as one argument, such as `cdidx search "not supported"`, normal search still uses FTS token matching but ranks chunks containing the exact phrase ahead of token-only matches. + Search case behavior depends on the selected mode: | Mode or text | Case and token behavior | @@ -3494,6 +3496,8 @@ cdidx report --output report.tgz --json `catch { }` のように記号の多いコード片では、通常検索が再実行ヒントを出す場合があります。brace、operator、punctuation、大文字小文字まで byte-for-byte に一致させたい場合は `--exact-substring` を使います。 +`cdidx search "not supported"` のように空白を含む literal query を 1 引数で渡した場合、通常検索は引き続き FTS token matching を使いますが、exact phrase を含む chunk を token-only match より前に並べます。 + 検索の大小文字の扱いは mode ごとに異なります: | mode / text | 大小文字と token の扱い | diff --git a/changelog.d/unreleased/3389.fixed.md b/changelog.d/unreleased/3389.fixed.md new file mode 100644 index 0000000000..2d83bbc017 --- /dev/null +++ b/changelog.d/unreleased/3389.fixed.md @@ -0,0 +1,17 @@ +--- +category: fixed +issues: + - 3389 +affected: + - src/CodeIndex/Database/DbSearchReader.cs + - tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs + - USER_GUIDE.md +--- + +## English + +- **Multiword literal searches rank exact phrase chunks first (#3389)** — normal `search` now boosts chunks containing the exact whitespace phrase, such as `not supported`, ahead of token-only matches. + +## 日本語 + +- **複数語の literal search が exact phrase を含む chunk を先に並べるようになりました (#3389)** — 通常の `search` は `not supported` のような空白を含む exact phrase を持つ chunk を、token-only match より前に並べます。 diff --git a/src/CodeIndex/Database/DbSearchReader.cs b/src/CodeIndex/Database/DbSearchReader.cs index ccb16bd19a..909e026305 100644 --- a/src/CodeIndex/Database/DbSearchReader.cs +++ b/src/CodeIndex/Database/DbSearchReader.cs @@ -123,7 +123,7 @@ public List Search(string query, int limit = 20, string? lang = nu var coverageTokens = exact ? new List() : GetSearchCoverageTokens(normalizedQuery, rawQuery); var hasGuardFilters = guardFilters is { Count: > 0 }; var searchMatchLineContext = SearchMatchLineContext.Create(query, lang, exact); - var exactSubstringBoost = !exact && !rawQuery && IsPunctuationHeavyLiteralQuery(query); + var exactLiteralBoost = !exact && !rawQuery && ShouldBoostExactLiteralSearch(query); var guardedCandidateLimit = hasGuardFilters ? GetGuardedSearchCandidateLimit(limit, cursor) : 0; using var cmd = _conn.CreateCommand(); string sql; @@ -165,7 +165,7 @@ FROM fts_chunks if (since != null && _fileColumns.Contains("modified")) sql += " AND f.modified >= @since"; AppendPathFilters(ref sql, pathPatterns, excludePathPatterns, excludeTests); - sql += $" ORDER BY {GetSearchOrderSql(coverageTokens.Count, exactSubstringBoost)}"; + sql += $" ORDER BY {GetSearchOrderSql(coverageTokens.Count, exactLiteralBoost)}"; if (hasGuardFilters) sql += " LIMIT @candidateFetchLimit"; else @@ -459,7 +459,7 @@ FROM fts_chunks sql += " AND f.modified >= @since"; AppendPathFilters(ref sql, pathPatterns, excludePathPatterns, excludeTests); - sql += $" ORDER BY {GetSearchOrderSql(coverageTokens.Count, exactSubstringBoost: false)}"; + sql += $" ORDER BY {GetSearchOrderSql(coverageTokens.Count, exactLiteralBoost: false)}"; cmd.CommandText = sql; if (exact) @@ -1298,13 +1298,22 @@ private static bool OverlapsOrTouches((int Start, int End) interval, int start, } } - private static string GetSearchOrderSql(int coverageTokenCount, bool exactSubstringBoost) + private static string GetSearchOrderSql(int coverageTokenCount, bool exactLiteralBoost) { var coverageOrder = GetSearchCoverageOrderSql(coverageTokenCount); - var exactSubstringOrder = exactSubstringBoost + var exactLiteralOrder = exactLiteralBoost ? $"CASE WHEN instr({GetExactSearchTextSql("c.content", "f.lang")}, {GetExactSearchTextSql("@rankingQuery", "f.lang")}) > 0 THEN 0 ELSE 1 END, " : string.Empty; - return $"{PathBucketOrder}, {exactSubstringOrder}{ExactSymbolMatchOrder}, {PrefixSymbolMatchOrder}, {SearchVisibilityOrder}, {PathTextMatchOrder}, {ChunkTextMatchOrder}, {ChunkStructuredFieldOrder}, {ChunkSymbolKindOrder}, {ChunkSymbolDepthOrder}, {coverageOrder}rank, f.modified DESC, f.path, c.id ASC"; + return $"{PathBucketOrder}, {exactLiteralOrder}{ExactSymbolMatchOrder}, {PrefixSymbolMatchOrder}, {SearchVisibilityOrder}, {PathTextMatchOrder}, {ChunkTextMatchOrder}, {ChunkStructuredFieldOrder}, {ChunkSymbolKindOrder}, {ChunkSymbolDepthOrder}, {coverageOrder}rank, f.modified DESC, f.path, c.id ASC"; + } + + private static bool ShouldBoostExactLiteralSearch(string query) + { + var trimmed = query.Trim(); + if (trimmed.Length == 0 || !trimmed.Any(char.IsLetterOrDigit)) + return false; + + return IsPunctuationHeavyLiteralQuery(trimmed) || IsMultiwordLiteralPhrase(trimmed); } private static bool IsPunctuationHeavyLiteralQuery(string query) @@ -1318,6 +1327,12 @@ private static bool IsPunctuationHeavyLiteralQuery(string query) return punctuationCount >= 2 || tokens.Any(IsStandaloneCodeOperatorToken); } + private static bool IsMultiwordLiteralPhrase(string query) + { + var tokens = query.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); + return tokens.Length > 1 && tokens.All(token => token.Any(char.IsLetterOrDigit)); + } + private static bool IsStandaloneCodeOperatorToken(string token) => token.Length > 0 && token.All(ch => !char.IsLetterOrDigit(ch) && !char.IsWhiteSpace(ch) && ch != '_') diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs index 3454dbd1a0..85a5e4a8a2 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs @@ -92,6 +92,42 @@ public void RunSearch_PathGlobExpansionHintSuggestsQuotedPath_Issue3445() Assert.Contains("`--path 'src/CodeIndex/**'`", stderr); } + [Fact] + public void RunSearch_MultiwordLiteralRanksExactPhraseContentFirst_Issue3389() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_multiword_phrase_3389"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/not supported noise.cs", + "csharp", + "class Noise { string Message = \"not every platform is supported\"; }"); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/z_phrase.cs", + "csharp", + "class Phrase { string Message = \"feature is not supported here\"; }"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["not supported", "--db", dbPath, "--json=array", "--limit", "2"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var rows = document.RootElement.EnumerateArray().ToList(); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, rows.Count); + Assert.Equal("src/z_phrase.cs", rows[0].GetProperty("path").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void RunSearch_FormatLspEmitsLocationArray() { From 5a3d77a7001adc8ae1c9898fc669fcd67f8c410f Mon Sep 17 00:00:00 2001 From: Widthdom Date: Fri, 12 Jun 2026 08:56:46 +0900 Subject: [PATCH 5/5] Preserve top limit diagnostics (#3521) --- src/CodeIndex/Cli/QueryCommandRunner.cs | 5 +++-- tests/CodeIndex.Tests/ConsoleUiTests.cs | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 6e4c07ef83..05af1b7f64 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -6391,9 +6391,10 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) case "--limit": case "--max-results": case "--top": - if (!TryReadRawOptionValue(args, ref i, normalizedArg, inlineValue, out var limitValue, out var missingLimitError)) + var limitOptionName = normalizedArg == "--top" ? "--limit" : normalizedArg; + if (!TryReadRawOptionValue(args, ref i, limitOptionName, inlineValue, out var limitValue, out var missingLimitError)) AddParseError(missingLimitError!); - else if (TryParsePositiveInt(limitValue!, normalizedArg, out var parsedLimit, out var limitError)) + else if (TryParsePositiveInt(limitValue!, limitOptionName, out var parsedLimit, out var limitError)) { WarnIfDuplicateSingleValueOption("--limit", limitValue!); limit = parsedLimit; diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index aecbf9c203..87b42889cf 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -115,7 +115,7 @@ public void PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags() Assert.Contains("cdidx references |--query |-- ", output); Assert.Contains("cdidx callers |--query |-- ", output); Assert.Contains("cdidx callees |--query |-- ", output); - Assert.Contains("cdidx search |--query |-- |--recipe |--list-recipes [--db ] [--json[=ndjson|array]] [--pretty] [--format ] [--open-issues ] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--snippet-lines ] [--snippet-focus ] [--max-line-width ] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--since ] [--no-dedup] [--no-visibility-rank] [--require-before ] [--require-after ] [--reject-before ] [--reject-after ] [--guard-window ]", output); + Assert.Contains("cdidx search |--query |-- |--recipe |--list-recipes [--db ] [--json[=ndjson|array]] [--pretty] [--format ] [--open-issues ] [--verbose] [--limit |--top |--max-results ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--snippet-lines ] [--snippet-focus ] [--max-line-width ] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--since ] [--no-dedup] [--no-visibility-rank] [--require-before ] [--require-after ] [--reject-before ] [--reject-after ] [--guard-window ]", output); Assert.Contains("cdidx definition |--query |-- [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--visibility ] [--exclude-visibility ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--exact|--exact-name] [--count] [--since ]", output); Assert.Contains("cdidx references |--query |-- [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--snippet-lines ] [--max-line-width ] [--exact|--exact-name] [--count]", output); Assert.Contains("cdidx inspect |--query |-- [--db ] [--json] [--pretty] [--compact] [--fields ] [--body-only] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--max-line-width ] [--exact|--exact-name]", output); @@ -269,7 +269,7 @@ public void PrintUsage_QueryLinesMatchImplementedOptions() { var output = CaptureFullUsageOutput(showBanner: false); - Assert.Contains("cdidx search |--query |-- |--recipe |--list-recipes [--db ] [--json[=ndjson|array]] [--pretty] [--format ] [--open-issues ] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--snippet-lines ] [--snippet-focus ] [--max-line-width ] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--since ] [--no-dedup] [--no-visibility-rank] [--require-before ] [--require-after ] [--reject-before ] [--reject-after ] [--guard-window ]", output); + Assert.Contains("cdidx search |--query |-- |--recipe |--list-recipes [--db ] [--json[=ndjson|array]] [--pretty] [--format ] [--open-issues ] [--verbose] [--limit |--top |--max-results ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--snippet-lines ] [--snippet-focus ] [--max-line-width ] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--since ] [--no-dedup] [--no-visibility-rank] [--require-before ] [--require-after ] [--reject-before ] [--reject-after ] [--guard-window ]", output); Assert.Contains("cdidx symbols [query|--query |-- ] [--name ] [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--visibility ] [--exclude-visibility ] [--path ] [--exclude-path ] [--exclude-tests] [--exact|--exact-name] [--count] [--since ]", output); Assert.Contains("cdidx files [query|--query |-- ] [--db ] [--json[=ndjson|array]] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count] [--since ] [--bytes]", output); Assert.Contains("cdidx hotspots [--db ] [--json] [--verbose] [--limit |--top ] [--kind ] [--visibility ] [--exclude-visibility ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count]", output);