From 723cb34573c016554dd382a0aad640dfcaa7be77 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sun, 31 May 2026 15:13:35 +0900 Subject: [PATCH 1/2] Add lightweight query output formats --- README.md | 6 +- changelog.d/unreleased/1642.fixed.md | 21 +++ changelog.d/unreleased/1941.added.md | 18 +++ src/CodeIndex/Cli/CliFlagSchema.cs | 2 +- src/CodeIndex/Cli/ConsoleUi.cs | 14 +- src/CodeIndex/Cli/QueryCommandRunner.cs | 135 +++++++++++++++++- src/CodeIndex/Mcp/McpToolDefinitions.cs | 15 +- src/CodeIndex/Mcp/McpToolHandlers.cs | 86 +++++++++-- tests/CodeIndex.Tests/McpServerTests.cs | 30 ++++ .../QueryCommandRunnerTests.cs | 76 ++++++++++ 10 files changed, 378 insertions(+), 25 deletions(-) create mode 100644 changelog.d/unreleased/1642.fixed.md create mode 100644 changelog.d/unreleased/1941.added.md diff --git a/README.md b/README.md index 91c44dbe9d..b31284104e 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ Output controls: | ASCII-only terminal output | Use `--ascii`, `CDIDX_ASCII=1`, `NO_UNICODE`, `TERM=dumb`, accessibility env hints, or a non-UTF-8 locale. Spinners use pipe, slash, dash, and backslash frames; progress bars use `#` / `-`; very narrow terminals fall back to percentage-only progress. | | Color and terminal capability | `--color auto` emits ANSI only for capable interactive terminals; `TERM=dumb`, `CI=true`, missing Unix terminal hints, `NO_COLOR`, or `CLICOLOR=0` disable ANSI/progress control sequences. `--palette basic|256|truecolor` can override the `COLORTERM` / `TERM` color-depth detection. | | UTF-8 JSON pipelines | CLI `--json` output is written as UTF-8 without a BOM and never includes ANSI escape sequences, even when color is forced for human output. | -| Script-friendly query pipelines | Use `--quiet`, `-q`, `--silent`, or `CDIDX_QUIET=1` to suppress informational stderr text while preserving errors. `--quiet` takes precedence over `--verbose`. | +| Script-friendly query pipelines | Use `--quiet`, `-q`, `--silent`, or `CDIDX_QUIET=1` to suppress informational stderr text while preserving errors. `--quiet` takes precedence over `--verbose`. Read commands that support `--format` can emit `count`, `compact`, `csv`, or `tsv` output when callers need smaller or table-shaped payloads instead of full excerpts. | Use `cdidx` when a repository will be searched repeatedly from terminals, scripts, CI, or AI tools. Use `rg` when you only need a one-off text scan. @@ -111,7 +111,7 @@ See [DISTRIBUTION.md](DISTRIBUTION.md) for the full channel matrix and ### Validate -Run `cdidx validate [--db ] [--json] [--verbose] [--kind ] [--path ]` +Run `cdidx validate [--db ] [--json] [--format ] [--verbose] [--kind ] [--path ]` to report indexed file issues such as replacement characters (`U+FFFD`), BOMs, NUL bytes, mixed line endings, UTF-16 BOMs, and likely non-UTF8 content. Validation findings are reported in the output and do not by themselves make @@ -132,7 +132,7 @@ downgrading `cdidx`. | Area | What cdidx provides | |---|---| -| Search surfaces | CLI-first output for humans and machines; full-text, symbol, reference, caller/callee, dependency, map, inspect, and excerpt commands. | +| Search surfaces | CLI-first output for humans and machines; full-text, symbol, reference, caller/callee, dependency, map, inspect, and excerpt commands. `search`, `definition`, `references`, `callers`, `callees`, `find`, and `validate` support `--format count|compact|csv|tsv|lsp|qf|sarif` for token-budgeted agents, scripts, editors, and CI reports. | | Ranking and filters | Public/exported symbol matches rank ahead of protected, internal, and private matches. Use `--no-visibility-rank` for legacy order, and `--visibility` / `--exclude-visibility` with `symbols`, `definition`, `unused`, and `hotspots`. Query defaults can be adjusted with `CDIDX_DEFAULT_LIMIT`, `CDIDX_DEFAULT_SNIPPET_LINES`, and `CDIDX_DEFAULT_MAX_LINE_WIDTH`; explicit CLI flags still win. | | Project scoping | `.sln` / `.csproj`-aware --project <name|path> filters for indexing and queries, plus `--solution ` when a workspace has multiple solution files. | | MCP integration | MCP server support for AI clients such as Claude Code, Cursor, and Windsurf, including tools, indexed-file resources, starter prompts, schema constraints for local argument validation, `mimeType` on text content blocks, logging, a compatibility server-side `notifications/initialized` ready signal on stdio or HTTP `/events` streams, and `Language support:` descriptions sourced from the same registries as `cdidx languages`. Tool schemas reject unknown arguments with `-32602`, advertise `x-stability`, and use snake_case structured JSON keys to match the CLI JSON contract. | diff --git a/changelog.d/unreleased/1642.fixed.md b/changelog.d/unreleased/1642.fixed.md new file mode 100644 index 0000000000..dcb8cefca3 --- /dev/null +++ b/changelog.d/unreleased/1642.fixed.md @@ -0,0 +1,21 @@ +--- +category: fixed +issues: + - 1642 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - src/CodeIndex/Cli/CliFlagSchema.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - src/CodeIndex/Mcp/McpToolDefinitions.cs + - src/CodeIndex/Mcp/McpToolHandlers.cs + - tests/CodeIndex.Tests/McpServerTests.cs + - tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +--- + +## English + +- **Read commands now support lightweight `count` and `compact` output formats (#1642)** — CLI `search`, `definition`, `references`, `callers`, `callees`, `find`, and `validate` can return count-only or file/line-only payloads through `--format`, while MCP search/definition/reference/caller/callee tools accept `format: "count"` or `format: "compact"` without emitting excerpts or full result metadata. + +## 日本語 + +- **read 系コマンドで軽量な `count` / `compact` 出力形式を使えるようになりました (#1642)** — CLI の `search`、`definition`、`references`、`callers`、`callees`、`find`、`validate` は `--format` により、MCP の search / definition / reference / caller / callee 系ツールは `format: "count"` または `format: "compact"` により、excerpt や完全な結果メタデータを出さずに count-only または file/line-only の payload を返せます。 diff --git a/changelog.d/unreleased/1941.added.md b/changelog.d/unreleased/1941.added.md new file mode 100644 index 0000000000..1a70c5d08a --- /dev/null +++ b/changelog.d/unreleased/1941.added.md @@ -0,0 +1,18 @@ +--- +category: added +issues: + - 1941 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - src/CodeIndex/Cli/CliFlagSchema.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +--- + +## English + +- **Built-in CSV and TSV query formatters are now available (#1941)** — formatter-aware read commands accept `--format csv` and `--format tsv`, giving scripts and CI jobs table-shaped output without reserializing JSON. + +## 日本語 + +- **組み込みの CSV / TSV query formatter を追加しました (#1941)** — formatter 対応の read 系コマンドで `--format csv` と `--format tsv` を指定でき、スクリプトや CI が JSON を再シリアライズせずに表形式の出力を扱えます。 diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index 8501977961..9046ae5864 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -205,7 +205,7 @@ private static IReadOnlyList BuildAll() new() { Name = "--workspace-db", ValuePlaceholder = "", Description = "Additional workspace member database path for dependency aggregation", Commands = Set(WorkspaceDbCommands) }, new() { Name = "--data-dir", ValuePlaceholder = "", Description = "Directory containing codeindex.db; overrides CDIDX_DATA_DIR/XDG/workspace defaults", Commands = Set(DataDirCommands) }, new() { Name = "--json", Description = "JSON output; search also accepts --json=array for a single JSON array", Commands = Set(JsonCommands) }, - new() { Name = "--format", ValuePlaceholder = "", Description = "Standard output format for editor and CI integrations", Commands = Set(FormatCommands) }, + new() { Name = "--format", ValuePlaceholder = "", Description = "Standard output format for token budgets, editor integrations, and CI", Commands = Set(FormatCommands) }, new() { Name = "--quiet", ShortName = "-q", Description = "Suppress informational stderr output; errors still print", Commands = Set(AllCommands.ToArray()) }, new() { Name = "--silent", Description = "Alias for --quiet", Commands = Set(AllCommands.ToArray()) }, new() { Name = "--profile", Description = "Emit SQL timing and EXPLAIN QUERY PLAN profile JSON after the normal result", Commands = Set(ProfileCommands) }, diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 4cf0ba5369..35cad921f1 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -68,15 +68,15 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("index-commits", "cdidx index --commits [id ...] [--db ] [--verbose] [--dry-run] [--json] [--duration-format ] [--max-file-bytes ] [--include-symbol-kind [,]] [--exclude-symbol-kind [,]]"), ("index-changed-between", "cdidx index --changed-between [--db ] [--verbose] [--dry-run] [--json] [--duration-format ] [--max-file-bytes ] [--include-symbol-kind [,]] [--exclude-symbol-kind [,]]"), ("index-files", "cdidx index --files [path ...] [--db ] [--verbose] [--dry-run] [--json] [--duration-format ] [--max-file-bytes ] [--include-symbol-kind [,]] [--exclude-symbol-kind [,]]"), - ("search", "cdidx search |--query |-- [--db ] [--json[=ndjson|array]] [--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]"), - ("definition", "cdidx definition |--query |-- [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--visibility ] [--exclude-visibility ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--exact|--exact-name] [--count] [--since ]"), + ("search", "cdidx search |--query |-- [--db ] [--json[=ndjson|array]] [--format ] [--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]"), + ("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] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--snippet-lines ] [--max-line-width ] [--exact|--exact-name] [--count]"), - ("callers", "cdidx callers |--query |-- [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--rank-by ] [--raw-kinds] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--snippet-lines ] [--max-line-width ] [--exact|--exact-name] [--count]"), - ("callees", "cdidx callees |--query |-- [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--rank-by ] [--raw-kinds] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--snippet-lines ] [--max-line-width ] [--exact|--exact-name] [--count]"), + ("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]"), + ("callers", "cdidx callers |--query |-- [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--rank-by ] [--raw-kinds] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--snippet-lines ] [--max-line-width ] [--exact|--exact-name] [--count]"), + ("callees", "cdidx callees |--query |-- [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--rank-by ] [--raw-kinds] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--snippet-lines ] [--max-line-width ] [--exact|--exact-name] [--count]"), ("symbols", "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 ]"), ("files", "cdidx files [query|--query |-- ] [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count] [--since ] [--bytes]"), - ("find", "cdidx find --path [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--exclude-path ] [--exclude-tests] [--before ] [--after ] [--max-line-width ] [--exact] [--count]"), + ("find", "cdidx find --path [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--exclude-path ] [--exclude-tests] [--before ] [--after ] [--max-line-width ] [--exact] [--count]"), ("excerpt", "cdidx excerpt --start [--end ] [--before ] [--after ] [--max-line-width ] [--focus-line ] [--focus-column ] [--focus-length ] [--db ] [--json] [--verbose]"), ("map", "cdidx map [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--bytes] [--min-entrypoint-confidence <0.0..1.0>]"), ("inspect", "cdidx inspect |--query |-- [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--max-line-width ] [--exact|--exact-name]"), @@ -86,7 +86,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("db", "cdidx db --integrity-check [--db ] [--json]"), ("diff", "cdidx diff [--json] [--summary-only] [--detailed] [--limit ]"), ("report", "cdidx report --output [--db ] [--json] [--log-lines ] [--no-log] [--include-args]"), - ("validate", "cdidx validate [--db ] [--json] [--verbose] [--kind ] [--path ]"), + ("validate", "cdidx validate [--db ] [--json] [--format ] [--verbose] [--kind ] [--path ]"), ("impact", "cdidx impact |--query |-- [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--snippet-lines ] [--max-line-width ] [--max-hops ] [--count] [--with-paths]"), ("deps", "cdidx deps [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--reverse]"), ("unused", "cdidx unused [--db ] [--json] [--verbose] [--limit |--top ] [--kind ] [--visibility ] [--exclude-visibility ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count]"), diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 7270bf0b96..0598756438 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -199,6 +199,10 @@ private sealed record StatusReadinessField( private const string OutputFormatLsp = "lsp"; private const string OutputFormatQf = "qf"; private const string OutputFormatSarif = "sarif"; + private const string OutputFormatCount = "count"; + private const string OutputFormatCompact = "compact"; + private const string OutputFormatCsv = "csv"; + private const string OutputFormatTsv = "tsv"; private static readonly HashSet InlineValueOptions = new(ValueTakingOptions.Concat(["--json"]), StringComparer.Ordinal); private const string FindUsage = "Usage: cdidx find --path [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--exclude-path ] [--exclude-tests] [--before ] [--after ] [--max-line-width ] [--exact] [--count]\n cdidx find --query --path [...]\n cdidx find [options] -- "; @@ -447,6 +451,11 @@ public static int RunSearch(string[] cmdArgs, JsonSerializerOptions jsonOptions) if (options.Json) { + if (TryWriteFormattedLocations( + options, + results.Select(r => new FormattedLocation(r.Path, r.StartLine, null, $"search match: {options.Query}")), + jsonOptions)) + return CommandExitCodes.Success; if (options.OutputFormat == OutputFormatLsp) { WriteLspLocations(results.Select(ToLspLocation), jsonOptions); @@ -576,6 +585,21 @@ private static void WriteLspLocations(IEnumerable locations, JsonSe private static bool TryWriteEmptyFormattedResult(QueryCommandOptions options, JsonSerializerOptions jsonOptions) { + if (options.OutputFormat == OutputFormatCount) + { + WriteFormattedCount(0, jsonOptions); + return true; + } + if (options.OutputFormat == OutputFormatCompact) + { + WriteCompactLocations([], jsonOptions); + return true; + } + if (options.OutputFormat == OutputFormatCsv || options.OutputFormat == OutputFormatTsv) + { + WriteDelimitedLocations([], options.OutputFormat); + return true; + } if (options.OutputFormat == OutputFormatLsp) { WriteLspLocations([], jsonOptions); @@ -591,6 +615,81 @@ private static bool TryWriteEmptyFormattedResult(QueryCommandOptions options, Js return false; } + private sealed record FormattedLocation(string File, int Line, int? Column = null, string? Label = null); + + private static bool TryWriteFormattedLocations(QueryCommandOptions options, IEnumerable locations, JsonSerializerOptions jsonOptions) + { + if (options.OutputFormat == OutputFormatCount) + { + WriteFormattedCount(locations.Count(), jsonOptions); + return true; + } + if (options.OutputFormat == OutputFormatCompact) + { + WriteCompactLocations(locations, jsonOptions); + return true; + } + if (options.OutputFormat == OutputFormatCsv || options.OutputFormat == OutputFormatTsv) + { + WriteDelimitedLocations(locations, options.OutputFormat); + return true; + } + return false; + } + + private static void WriteFormattedCount(int count, JsonSerializerOptions jsonOptions) + => Console.WriteLine(new JsonObject + { + ["count"] = count, + ["total_estimated"] = count, + }.ToJsonString(jsonOptions)); + + private static void WriteCompactLocations(IEnumerable locations, JsonSerializerOptions jsonOptions) + { + var rows = new JsonArray(); + foreach (var location in locations) + { + var row = new JsonObject + { + ["file"] = location.File, + ["line"] = location.Line, + }; + if (location.Column.HasValue) + row["column"] = location.Column.Value; + rows.Add(row); + } + Console.WriteLine(rows.ToJsonString(jsonOptions)); + } + + private static void WriteDelimitedLocations(IEnumerable locations, string outputFormat) + { + var delimiter = outputFormat == OutputFormatTsv ? "\t" : ","; + Console.WriteLine(string.Join(delimiter, ["file", "line", "column", "label"])); + foreach (var location in locations) + { + var values = new[] + { + location.File, + location.Line.ToString(CultureInfo.InvariantCulture), + location.Column?.ToString(CultureInfo.InvariantCulture) ?? string.Empty, + location.Label ?? string.Empty, + }; + Console.WriteLine(string.Join(delimiter, values.Select(value => EscapeDelimitedValue(value, outputFormat)))); + } + } + + private static string EscapeDelimitedValue(string value, string outputFormat) + { + if (outputFormat == OutputFormatTsv) + return value.Replace("\t", " ", StringComparison.Ordinal).Replace("\r", " ", StringComparison.Ordinal).Replace("\n", " ", StringComparison.Ordinal); + if (!value.Contains('"', StringComparison.Ordinal) && + !value.Contains(',', StringComparison.Ordinal) && + !value.Contains('\r', StringComparison.Ordinal) && + !value.Contains('\n', StringComparison.Ordinal)) + return value; + return "\"" + value.Replace("\"", "\"\"", StringComparison.Ordinal) + "\""; + } + private static void WriteQuickfix(IEnumerable<(string Path, int Line, int Column, string Message)> items) { foreach (var item in items) @@ -764,6 +863,11 @@ public static int RunDefinition(string[] cmdArgs, JsonSerializerOptions jsonOpti if (options.Json) { + if (TryWriteFormattedLocations( + options, + results.Select(r => new FormattedLocation(r.Path, r.StartLine, null, $"{r.Kind} {r.Name}")), + jsonOptions)) + return CommandExitCodes.Success; if (options.OutputFormat == OutputFormatLsp) { WriteLspLocations(results.Select(ToLspLocation), jsonOptions); @@ -998,6 +1102,11 @@ public static int RunReferences(string[] cmdArgs, JsonSerializerOptions jsonOpti if (options.Json) { + if (TryWriteFormattedLocations( + options, + results.Select(r => new FormattedLocation(r.Path, r.Line, r.Column, $"{r.ReferenceKind} {r.SymbolName}")), + jsonOptions)) + return CommandExitCodes.Success; if (options.OutputFormat == OutputFormatLsp) { WriteLspLocations(results.Select(ToLspLocation), jsonOptions); @@ -1144,6 +1253,11 @@ public static int RunCallers(string[] cmdArgs, JsonSerializerOptions jsonOptions if (options.Json) { + if (TryWriteFormattedLocations( + options, + results.Select(r => new FormattedLocation(r.Path, r.FirstLine, null, $"{r.CallerName ?? ""} -> {r.CalleeName}")), + jsonOptions)) + return CommandExitCodes.Success; if (options.OutputFormat == OutputFormatLsp) { WriteLspLocations(results.Select(ToLspLocation), jsonOptions); @@ -1288,6 +1402,11 @@ public static int RunCallees(string[] cmdArgs, JsonSerializerOptions jsonOptions if (options.Json) { + if (TryWriteFormattedLocations( + options, + results.Select(r => new FormattedLocation(r.Path, r.FirstLine, null, $"{r.CallerName ?? ""} -> {r.CalleeName}")), + jsonOptions)) + return CommandExitCodes.Success; if (options.OutputFormat == OutputFormatLsp) { WriteLspLocations(results.Select(ToLspLocation), jsonOptions); @@ -1994,6 +2113,11 @@ public static int RunFind(string[] cmdArgs, JsonSerializerOptions jsonOptions) if (options.Json) { + if (TryWriteFormattedLocations( + options, + results.Select(r => new FormattedLocation(r.Path, r.Line, r.Column, $"find match: {options.Query}")), + jsonOptions)) + return CommandExitCodes.Success; if (options.OutputFormat == OutputFormatLsp) { WriteLspLocations(results.Select(ToLspLocation), jsonOptions); @@ -4241,6 +4365,11 @@ public static int RunValidate(string[] cmdArgs, JsonSerializerOptions jsonOption if (options.Json) { + if (TryWriteFormattedLocations( + options, + issues.Select(i => new FormattedLocation(i.Path, i.Line, null, $"{i.Kind}: {i.Message}")), + jsonOptions)) + return CommandExitCodes.Success; if (options.OutputFormat == OutputFormatLsp) { WriteLspLocations(issues.Select(ToLspLocation), jsonOptions); @@ -4574,7 +4703,7 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) } else { - AddParseError($"Error: --format must be one of text, json, lsp, qf, or sarif; got '{formatValue}'."); + AddParseError($"Error: --format must be one of text, json, count, compact, csv, tsv, lsp, qf, or sarif; got '{formatValue}'."); } } else @@ -5152,6 +5281,10 @@ private static bool TryParseOutputFormat(string rawValue, out string format) { case OutputFormatText: case OutputFormatJson: + case OutputFormatCount: + case OutputFormatCompact: + case OutputFormatCsv: + case OutputFormatTsv: case OutputFormatLsp: case OutputFormatQf: case OutputFormatSarif: diff --git a/src/CodeIndex/Mcp/McpToolDefinitions.cs b/src/CodeIndex/Mcp/McpToolDefinitions.cs index 698ea0473b..de0ab8d831 100644 --- a/src/CodeIndex/Mcp/McpToolDefinitions.cs +++ b/src/CodeIndex/Mcp/McpToolDefinitions.cs @@ -41,7 +41,8 @@ private JsonNode HandleToolsList(JsonNode? id) ["exactSubstring"] = new JsonObject { ["type"] = "boolean", ["description"] = "Preferred explicit name for search's exact mode: case-sensitive exact substring match (bypasses FTS5).", ["default"] = false }, ["exact"] = new JsonObject { ["type"] = "boolean", ["description"] = "Backward-compatible alias for `exactSubstring`.", ["default"] = false }, ["prefix"] = new JsonObject { ["type"] = "boolean", ["description"] = "Opt into FTS5 prefix expansion for every token in `query`. Cannot be combined with `exact`/`exactSubstring`.", ["default"] = false }, - ["countOnly"] = new JsonObject { ["type"] = "boolean", ["description"] = "Return only count metadata and a small top-file histogram; omit row payloads.", ["default"] = false } + ["countOnly"] = new JsonObject { ["type"] = "boolean", ["description"] = "Return only count metadata and a small top-file histogram; omit row payloads.", ["default"] = false }, + ["format"] = new JsonObject { ["type"] = "string", ["enum"] = new JsonArray { "full", "count", "compact" }, ["description"] = "Response shape: full rows, count-only metadata, or compact file/line rows without snippets.", ["default"] = "full" } }, ["required"] = new JsonArray { "query" } }, @@ -66,7 +67,8 @@ private JsonNode HandleToolsList(JsonNode? id) ["includeGenerated"] = new JsonObject { ["type"] = "boolean", ["description"] = "Include files detected as generated code", ["default"] = false }, ["since"] = new JsonObject { ["type"] = "string", ["description"] = "Filter to symbols in files modified since this ISO 8601 timestamp" }, ["exactName"] = new JsonObject { ["type"] = "boolean", ["description"] = "Preferred explicit name for exact symbol-name equality: NFKC + Unicode CaseFold exact name match instead of substring, so `Run` no longer also returns `RunAsync`.", ["default"] = false }, - ["exact"] = new JsonObject { ["type"] = "boolean", ["description"] = "Backward-compatible alias for `exactName`.", ["default"] = false } + ["exact"] = new JsonObject { ["type"] = "boolean", ["description"] = "Backward-compatible alias for `exactName`.", ["default"] = false }, + ["format"] = new JsonObject { ["type"] = "string", ["enum"] = new JsonArray { "full", "count", "compact" }, ["description"] = "Response shape: full rows, count-only metadata, or compact file/line rows without excerpts.", ["default"] = "full" } }, ["required"] = new JsonArray { "query" } }, @@ -92,7 +94,8 @@ private JsonNode HandleToolsList(JsonNode? id) ["includeGenerated"] = new JsonObject { ["type"] = "boolean", ["description"] = "Include files detected as generated code", ["default"] = false }, ["exactName"] = new JsonObject { ["type"] = "boolean", ["description"] = "Preferred explicit name for exact referenced-symbol equality. Uses NFKC + Unicode CaseFold so `Run` no longer matches `RunAsync`.", ["default"] = false }, ["exact"] = new JsonObject { ["type"] = "boolean", ["description"] = "Backward-compatible alias for `exactName`.", ["default"] = false }, - ["countOnly"] = new JsonObject { ["type"] = "boolean", ["description"] = "Return only count metadata and a small top-file histogram; omit row payloads.", ["default"] = false } + ["countOnly"] = new JsonObject { ["type"] = "boolean", ["description"] = "Return only count metadata and a small top-file histogram; omit row payloads.", ["default"] = false }, + ["format"] = new JsonObject { ["type"] = "string", ["enum"] = new JsonArray { "full", "count", "compact" }, ["description"] = "Response shape: full rows, count-only metadata, or compact file/line/column rows without context.", ["default"] = "full" } }, ["required"] = new JsonArray { "query" } }, @@ -117,7 +120,8 @@ private JsonNode HandleToolsList(JsonNode? id) ["includeGenerated"] = new JsonObject { ["type"] = "boolean", ["description"] = "Include files detected as generated code", ["default"] = false }, ["exactName"] = new JsonObject { ["type"] = "boolean", ["description"] = "Preferred explicit name for exact callee-name equality. Uses NFKC + Unicode CaseFold so `Run` no longer matches `RunAsync`.", ["default"] = false }, ["exact"] = new JsonObject { ["type"] = "boolean", ["description"] = "Backward-compatible alias for `exactName`.", ["default"] = false }, - ["countOnly"] = new JsonObject { ["type"] = "boolean", ["description"] = "Return only count metadata and a small top-file histogram; omit row payloads.", ["default"] = false } + ["countOnly"] = new JsonObject { ["type"] = "boolean", ["description"] = "Return only count metadata and a small top-file histogram; omit row payloads.", ["default"] = false }, + ["format"] = new JsonObject { ["type"] = "string", ["enum"] = new JsonArray { "full", "count", "compact" }, ["description"] = "Response shape: full rows, count-only metadata, or compact file/line rows without excerpts.", ["default"] = "full" } }, ["required"] = new JsonArray { "query" } }, @@ -142,7 +146,8 @@ private JsonNode HandleToolsList(JsonNode? id) ["includeGenerated"] = new JsonObject { ["type"] = "boolean", ["description"] = "Include files detected as generated code", ["default"] = false }, ["exactName"] = new JsonObject { ["type"] = "boolean", ["description"] = "Preferred explicit name for exact caller/container equality. Uses NFKC + Unicode CaseFold so `Run` no longer matches `RunAsync`.", ["default"] = false }, ["exact"] = new JsonObject { ["type"] = "boolean", ["description"] = "Backward-compatible alias for `exactName`.", ["default"] = false }, - ["countOnly"] = new JsonObject { ["type"] = "boolean", ["description"] = "Return only count metadata and a small top-file histogram; omit row payloads.", ["default"] = false } + ["countOnly"] = new JsonObject { ["type"] = "boolean", ["description"] = "Return only count metadata and a small top-file histogram; omit row payloads.", ["default"] = false }, + ["format"] = new JsonObject { ["type"] = "string", ["enum"] = new JsonArray { "full", "count", "compact" }, ["description"] = "Response shape: full rows, count-only metadata, or compact file/line rows without excerpts.", ["default"] = "full" } }, ["required"] = new JsonArray { "query" } }, diff --git a/src/CodeIndex/Mcp/McpToolHandlers.cs b/src/CodeIndex/Mcp/McpToolHandlers.cs index a7f42aa0c0..12cff60949 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.cs @@ -172,6 +172,38 @@ private static void AddExactZeroHint(JsonObject payload, ExactZeroHintResult? ex private static int ReadOffset(JsonNode? args) => Math.Max(0, args?["offset"]?.GetValue() ?? 0); + private static string ReadResponseFormat(JsonNode? args) + => args?["format"]?.GetValue()?.Trim().ToLowerInvariant() ?? "full"; + + private static string? ValidateResponseFormat(string format) + => format is "full" or "count" or "compact" + ? null + : "format must be one of full, count, compact"; + + private static void ApplyCompactResults( + JsonObject payload, + IEnumerable results, + Func pathSelector, + Func lineSelector, + Func? columnSelector = null) + { + var compact = new JsonArray(); + foreach (var result in results) + { + var row = new JsonObject + { + ["file"] = pathSelector(result), + ["line"] = lineSelector(result), + }; + var column = columnSelector?.Invoke(result); + if (column.HasValue) + row["column"] = column.Value; + compact.Add(row); + } + payload["results"] = compact; + payload["format"] = "compact"; + } + private static bool AddLimitMetadata(JsonObject payload, List results, int limit, int offset = 0, bool includePagination = false) { var truncated = results.Count > limit; @@ -315,10 +347,10 @@ private static List ReadStringList(JsonNode? args, string propertyName) private static IReadOnlySet GetAllowedToolArguments(string toolName) => toolName switch { - "search" => new HashSet(StringComparer.Ordinal) { "query", "limit", "lang", "snippetLines", "maxLineWidth", "rawQuery", "path", "excludePaths", "excludeTests", "includeGenerated", "since", "noDedup", "exactSubstring", "exact", "prefix", "countOnly", "project", "solution" }, - "definition" => new HashSet(StringComparer.Ordinal) { "query", "kind", "lang", "limit", "includeBody", "lsp_compatible", "path", "excludePaths", "excludeTests", "includeGenerated", "since", "exactName", "exact", "project", "solution" }, - "references" => new HashSet(StringComparer.Ordinal) { "query", "kind", "lang", "limit", "offset", "maxLineWidth", "lsp_compatible", "path", "excludePaths", "excludeTests", "includeGenerated", "exactName", "exact", "countOnly", "project", "solution" }, - "callers" or "callees" => new HashSet(StringComparer.Ordinal) { "query", "kind", "rankBy", "lang", "limit", "offset", "path", "excludePaths", "excludeTests", "includeGenerated", "exactName", "exact", "countOnly", "project", "solution" }, + "search" => new HashSet(StringComparer.Ordinal) { "query", "limit", "lang", "snippetLines", "maxLineWidth", "rawQuery", "path", "excludePaths", "excludeTests", "includeGenerated", "since", "noDedup", "exactSubstring", "exact", "prefix", "countOnly", "format", "project", "solution" }, + "definition" => new HashSet(StringComparer.Ordinal) { "query", "kind", "lang", "limit", "includeBody", "lsp_compatible", "path", "excludePaths", "excludeTests", "includeGenerated", "since", "exactName", "exact", "format", "project", "solution" }, + "references" => new HashSet(StringComparer.Ordinal) { "query", "kind", "lang", "limit", "offset", "maxLineWidth", "lsp_compatible", "path", "excludePaths", "excludeTests", "includeGenerated", "exactName", "exact", "countOnly", "format", "project", "solution" }, + "callers" or "callees" => new HashSet(StringComparer.Ordinal) { "query", "kind", "rankBy", "lang", "limit", "offset", "path", "excludePaths", "excludeTests", "includeGenerated", "exactName", "exact", "countOnly", "format", "project", "solution" }, "symbols" => new HashSet(StringComparer.Ordinal) { "query", "names", "kind", "lang", "limit", "path", "excludePaths", "excludeTests", "includeGenerated", "since", "exactName", "exact", "project", "solution" }, "files" => new HashSet(StringComparer.Ordinal) { "query", "lang", "limit", "path", "excludePaths", "excludeTests", "includeGenerated", "since" }, "find_in_file" => new HashSet(StringComparer.Ordinal) { "query", "path", "limit", "lang", "excludePaths", "excludeTests", "includeGenerated", "before", "after", "maxLineWidth", "exact" }, @@ -747,7 +779,10 @@ private JsonNode ExecuteSearch(JsonNode? id, JsonNode? args) return CreateToolErrorResponse(id, $"Invalid 'since' timestamp: '{sinceStr}'. Use ISO 8601 format (e.g. 2024-01-01 or 2024-01-01T00:00:00Z)."); } var deduplicate = !(args?["noDedup"]?.GetValue() ?? false); - var countOnly = ReadCountOnly(args); + var format = ReadResponseFormat(args); + if (ValidateResponseFormat(format) is string formatError) + return CreateToolErrorResponse(id, formatError); + var countOnly = ReadCountOnly(args) || format == "count"; if (!TryResolveSearchExactArgument(args, out var exact, out var exactError)) return CreateToolErrorResponse(id, exactError!); var prefix = args?["prefix"]?.GetValue() ?? false; @@ -798,6 +833,8 @@ private JsonNode ExecuteSearch(JsonNode? id, JsonNode? args) ["results"] = ToJsonArray(SearchSnippetFormatter.ToCompactResults(results, query, snippetLines, exact, maxLineWidth)) }; AddResultEnvelope(structured, results.Count, truncated ? null : results.Count, truncated); + if (format == "compact") + ApplyCompactResults(structured, results, result => result.Path, result => result.StartLine); // Include top file paths in summary for quick AI orientation // AIが素早く位置把握できるよう、サマリにトップファイルパスを含める var topPaths = results.Select(r => r.Path).Distinct().Take(3); @@ -939,11 +976,27 @@ private JsonNode ExecuteDefinition(JsonNode? id, JsonNode? args) since = parsedDefSince; if (!TryResolveNameExactArgument(args, "definition", out var exact, out var exactError)) return CreateToolErrorResponse(id, exactError!); + var format = ReadResponseFormat(args); + if (ValidateResponseFormat(format) is string formatError) + return CreateToolErrorResponse(id, formatError); return WithDbReader(id, args, reader => { var results = reader.GetDefinitions(query, FetchLimitForEnvelope(limit), kind, lang, includeBody, pathPatterns, excludePaths, excludeTests, since, exact); var truncated = TrimToRequestedLimit(results, limit); + if (format == "count") + { + var total = truncated + ? reader.CountDefinitionsTotal(query, kind, lang, pathPatterns, excludePaths, excludeTests, since, exact).Count + : results.Count; + var countPayload = BuildCountOnlyPayload(total, total, truncated: false, results, result => result.Path); + countPayload["query"] = query; + countPayload["kind"] = kind; + countPayload["lang"] = lang; + countPayload["path"] = PathEcho(pathPatterns); + countPayload["excludeTests"] = excludeTests; + return CreateToolResult(id, $"Counted {ConsoleUi.Counted(total, "definition")}.", countPayload); + } if (lspCompatible) QueryCommandRunner.AttachLspLocations(results); var exactSignal = reader.GetDefinitionExactQuerySignal(lang, pathPatterns, excludePaths, excludeTests, since); @@ -965,6 +1018,8 @@ private JsonNode ExecuteDefinition(JsonNode? id, JsonNode? args) ["results"] = ToJsonArray(results) }; AddResultEnvelope(payload, results.Count, truncated ? null : results.Count, truncated); + if (format == "compact") + ApplyCompactResults(payload, results, result => result.Path, result => result.StartLine); if (exact) AddExactGraphSignal(payload, exactSignal); if (results.Count == 0) @@ -997,7 +1052,10 @@ private JsonNode ExecuteReferences(JsonNode? id, JsonNode? args) var pathPatterns = ReadScopedPathList(args); var excludePaths = ReadStringList(args, "excludePaths"); var excludeTests = args?["excludeTests"]?.GetValue() ?? false; - var countOnly = ReadCountOnly(args); + var format = ReadResponseFormat(args); + if (ValidateResponseFormat(format) is string formatError) + return CreateToolErrorResponse(id, formatError); + var countOnly = ReadCountOnly(args) || format == "count"; if (!TryResolveNameExactArgument(args, "references", out var exact, out var exactError)) return CreateToolErrorResponse(id, exactError!); @@ -1053,6 +1111,8 @@ private JsonNode ExecuteReferences(JsonNode? id, JsonNode? args) ["results"] = ToJsonArray(results) }; AddPaginatedResultEnvelope(payload, results.Count, total, truncated, offset); + if (format == "compact") + ApplyCompactResults(payload, results, result => result.Path, result => result.Line, result => result.Column); if (exact) AddExactGraphSignal(payload, exactSignal); AddSqlGraphContractSignal(payload, sqlGraphSignal); @@ -1089,7 +1149,10 @@ private JsonNode ExecuteCallers(JsonNode? id, JsonNode? args) return CreateToolErrorResponse(id, exactError!); if (!TryReadReferenceRankMode(args, out var rankMode, out var rankModeError)) return CreateToolErrorResponse(id, rankModeError!); - var countOnly = ReadCountOnly(args); + var format = ReadResponseFormat(args); + if (ValidateResponseFormat(format) is string formatError) + return CreateToolErrorResponse(id, formatError); + var countOnly = ReadCountOnly(args) || format == "count"; return WithDbReader(id, args, reader => { @@ -1140,6 +1203,8 @@ private JsonNode ExecuteCallers(JsonNode? id, JsonNode? args) ["results"] = ToJsonArray(results) }; AddPaginatedResultEnvelope(payload, results.Count, total, truncated, offset); + if (format == "compact") + ApplyCompactResults(payload, results, result => result.Path, result => result.FirstLine); payload["aggregate_truncated"] = results.Any(result => result.AggregateTruncated); if (exact) AddExactGraphSignal(payload, exactSignal); @@ -1177,7 +1242,10 @@ private JsonNode ExecuteCallees(JsonNode? id, JsonNode? args) return CreateToolErrorResponse(id, exactError!); if (!TryReadReferenceRankMode(args, out var rankMode, out var rankModeError)) return CreateToolErrorResponse(id, rankModeError!); - var countOnly = ReadCountOnly(args); + var format = ReadResponseFormat(args); + if (ValidateResponseFormat(format) is string formatError) + return CreateToolErrorResponse(id, formatError); + var countOnly = ReadCountOnly(args) || format == "count"; return WithDbReader(id, args, reader => { @@ -1228,6 +1296,8 @@ private JsonNode ExecuteCallees(JsonNode? id, JsonNode? args) ["results"] = ToJsonArray(results) }; AddPaginatedResultEnvelope(payload, results.Count, total, truncated, offset); + if (format == "compact") + ApplyCompactResults(payload, results, result => result.Path, result => result.FirstLine); payload["aggregate_truncated"] = results.Any(result => result.AggregateTruncated); if (exact) AddExactGraphSignal(payload, exactSignal); diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index 1cfd82820d..6ea1033c74 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -180,6 +180,36 @@ private void MarkFoldReady() writer.MarkCSharpSymbolNameContractReady(); } + [Fact] + public void ToolsCall_SearchFormatCompactEmitsFileLineOnly_Issue1642() + { + var request = JsonNode.Parse( + """{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search","arguments":{"query":"Run","format":"compact"}}}""")!; + + var response = _server.HandleMessage(request)!; + var structured = response["result"]!["structuredContent"]!; + var row = Assert.Single(structured["results"]!.AsArray()); + + Assert.Equal("compact", structured["format"]!.GetValue()); + Assert.Equal("src/app.cs", row!["file"]!.GetValue()); + Assert.Equal(1, row["line"]!.GetValue()); + Assert.Null(row["snippet"]); + } + + [Fact] + public void ToolsCall_SearchFormatCountAliasesCountOnly_Issue1642() + { + var request = JsonNode.Parse( + """{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search","arguments":{"query":"Run","format":"count"}}}""")!; + + var response = _server.HandleMessage(request)!; + var structured = response["result"]!["structuredContent"]!; + + Assert.True(structured["count_only"]!.GetValue()); + Assert.True(structured["count"]!.GetValue() > 0); + Assert.Empty(structured["results"]!.AsArray()); + } + [Fact] public void ToolsCall_Callers_TruncatedResponseIncludesNextOffsetAndPages() { diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index 0d1e3062e8..0cf355958a 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -86,6 +86,82 @@ public void ParseArgs_AllowsZeroMaxLineWidth() Assert.Equal(0, options.MaxLineWidth); } + [Theory] + [InlineData("count")] + [InlineData("compact")] + [InlineData("csv")] + [InlineData("tsv")] + public void ParseArgs_AcceptsLightweightOutputFormats(string format) + { + var options = QueryCommandRunner.ParseArgs(["RunSearch", "--format", format], jsonDefault: false, allowNamedQuery: true); + + Assert.True(options.Json); + Assert.Equal(format, options.OutputFormat); + Assert.Null(options.ParseError); + } + + [Fact] + public void RunSearch_FormatCompactEmitsFileLineOnly_Issue1642() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_format_compact"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.cs", + "csharp", + "public class App { void Run() { Authenticate(); } }"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Authenticate", "--db", dbPath, "--format", "compact"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + using var document = ParseJsonOutput(stdout); + var row = Assert.Single(document.RootElement.EnumerateArray()); + Assert.Equal("src/app.cs", row.GetProperty("file").GetString()); + Assert.True(row.GetProperty("line").GetInt32() > 0); + Assert.False(row.TryGetProperty("snippet", out _)); + Assert.False(row.TryGetProperty("name", out _)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunSearch_FormatCsvEmitsDelimitedRows_Issue1941() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_search_format_csv"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile( + dbPath, + "src/app.cs", + "csharp", + "public class App { void Run() { Authenticate(); } }"); + + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["Authenticate", "--db", dbPath, "--format", "csv"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + var lines = stdout.Trim().Split(Environment.NewLine); + Assert.Equal("file,line,column,label", lines[0]); + Assert.Contains("src/app.cs", lines[1]); + Assert.Contains("search match: Authenticate", lines[1]); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void ParseArgs_UsesNumericDefaultEnvironmentVariables() { From 88aececfb0a44173bf48d489587ea34e80639369 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sun, 31 May 2026 17:44:08 +0900 Subject: [PATCH 2/2] Fix query format usage tests --- src/CodeIndex/Cli/QueryCommandRunner.cs | 2 +- tests/CodeIndex.Tests/ConsoleUiTests.cs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 0598756438..a1d769afe1 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -205,7 +205,7 @@ private sealed record StatusReadinessField( private const string OutputFormatTsv = "tsv"; private static readonly HashSet InlineValueOptions = new(ValueTakingOptions.Concat(["--json"]), StringComparer.Ordinal); - private const string FindUsage = "Usage: cdidx find --path [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--exclude-path ] [--exclude-tests] [--before ] [--after ] [--max-line-width ] [--exact] [--count]\n cdidx find --query --path [...]\n cdidx find [options] -- "; + private const string FindUsage = "Usage: cdidx find --path [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--exclude-path ] [--exclude-tests] [--before ] [--after ] [--max-line-width ] [--exact] [--count]\n cdidx find --query --path [...]\n cdidx find [options] -- "; public static int RunBatch(string[] cmdArgs, JsonSerializerOptions jsonOptions) { diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index 047c04ba8c..16249f058d 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -69,9 +69,9 @@ 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 |-- [--db ] [--json[=ndjson|array]] [--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]", output); - Assert.Contains("cdidx definition |--query |-- [--db ] [--json] [--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] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--snippet-lines ] [--max-line-width ] [--exact|--exact-name] [--count]", output); + Assert.Contains("cdidx search |--query |-- [--db ] [--json[=ndjson|array]] [--format ] [--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]", 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] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--max-line-width ] [--exact|--exact-name]", output); Assert.Contains("--snippet-lines Search snippet length (1-20, default: 8)", output); Assert.Contains("--snippet-focus search only: long-line focus mode (leftmost|quality|proximity, default: quality)", output); @@ -104,7 +104,7 @@ public void PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags() Assert.Contains("cdidx map [--db ] [--json] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--bytes]", 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] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count] [--since ] [--bytes]", output); - Assert.Contains("cdidx validate [--db ] [--json] [--verbose] [--kind ] [--path ]", output); + Assert.Contains("cdidx validate [--db ] [--json] [--format ] [--verbose] [--kind ] [--path ]", output); Assert.Contains("Note: if a query itself starts with '-', pass it with --query or -- ", output); Assert.DoesNotContain("cdidx validate [--db ] [--json] [--limit ] [--lang ]", output); Assert.Contains("cdidx unused [--db ] [--json] [--verbose] [--limit |--top ] [--kind ] [--visibility ] [--exclude-visibility ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count]", output); @@ -218,7 +218,7 @@ public void PrintUsage_QueryLinesMatchImplementedOptions() { var output = CaptureFullUsageOutput(showBanner: false); - Assert.Contains("cdidx search |--query |-- [--db ] [--json[=ndjson|array]] [--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]", output); + Assert.Contains("cdidx search |--query |-- [--db ] [--json[=ndjson|array]] [--format ] [--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]", 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] [--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);