diff --git a/USER_GUIDE.md b/USER_GUIDE.md index d196035d29..a6fbcd06cf 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -952,9 +952,9 @@ cdidx report --output report.tgz --json | `--exclude-tests` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect` | Exclude likely test files and prefer production code | | `--include-generated` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect`, `deps`, `impact`, `unused`, `hotspots` | Include files detected as generated code; generated files are excluded from query results by default | | `--workspace-db ` | `deps` | Add another CodeIndex database to the file-dependency query. Repeat it to aggregate multiple workspace member DBs; JSON edges include `source_db` and `target_db` so same relative paths can be disambiguated. | -| `--snippet-lines ` | `search` | Search snippet length for human-readable output and JSON/MCP snippets (default: 8, max: 20) | +| `--snippet-lines ` | `search`, `references`, `callers`, `callees`, `impact` | Search snippet length or graph `--body` excerpt length (default: 8, max: 20) | | `--snippet-focus ` | `search` | Choose how long search-result lines pick the visible focus when clamped. `quality` (default) prefers full-query matches and strong tokens; `proximity` favors dense multi-token clusters; `leftmost` keeps legacy earliest-match behavior. | -| `--max-line-width ` | `search`, `references`, `find`, `excerpt`, `inspect` | Clamp very long single-line snippet/reference/excerpt payloads around the relevant match (`0` disables clamping; default: 512, max: 4096) | +| `--max-line-width ` | `search`, `references`, `callers`, `callees`, `find`, `excerpt`, `impact`, `inspect` | Clamp very long single-line snippet/reference/excerpt payloads around the relevant match (`0` disables clamping; default: 512, max: 4096) | | `--fts` | `search` | Use raw FTS5 query syntax; malformed input is reported as a usage error with a hint. `fts_chunks` exposes only the `content` column, so `content:` is the only valid column qualifier. | | `--exact` | `search`, `find`, `symbols`, `definition`, `references`, `callers`, `callees`, `inspect` | Backward-compatible shorthand. Prefer `--exact-substring` for `search`, keep `--exact` for `find`, and prefer `--exact-name` for symbol / graph commands plus `inspect`. Pass at most one of `--exact`, `--exact-substring`, `--exact-name`; combining two or more is rejected with `Error: pass only one of --exact, --exact-substring, --exact-name.`. CLI JSON and MCP `structuredContent` expose `exact_index_available` / `degraded_reason`; MCP also keeps the legacy camelCase aliases `exactIndexAvailable` / `degradedReason` for backward compatibility. | | `--exact-substring` | `search` | Preferred explicit name for search exactness: case-sensitive exact substring (FTS5 bypassed). | @@ -962,7 +962,7 @@ cdidx report --output report.tgz --json | `--exact-name` | `symbols`, `definition`, `references`, `callers`, `callees`, `inspect` | Preferred explicit name for symbol-name exactness: NFKC + Unicode CaseFold exact equality (`Ä` / `ä`, `Run` / `Run`, ligatures, sharp-S, and Greek final sigma collapse). Unicode CaseFold remains locale-invariant, so Turkish dotted `İ` is still distinct from plain `i`. For C#, pass the canonical extracted name (`operator +`, `operator checked +`, `explicit operator Money`, `implicit operator decimal`, `Item`) rather than source keywords like `this` / `explicit`. Falls back to ASCII `COLLATE NOCASE` while the DB still contains stale fold metadata; prefer `cdidx backfill-fold`, or use a plain `cdidx index .` if it rewrites or purges every stale row, otherwise `--rebuild`. `status --json` exposes `fold_ready` and `csharp_symbol_name_ready` so AI clients can tell which path is active. When a read-only legacy DB is missing the fallback exact-match indexes, human-readable output warns and CLI JSON / MCP `structuredContent` expose degraded-state metadata. | | `--kind ` | `definition`, `references`, `callers`, `callees`, `symbols`, `hotspots`, `unused`, `validate` | Filter by kind (case-insensitive; `--kind FUNCTION` is treated as `--kind function`). `definition` / `symbols` / `hotspots` / `unused` use symbol kinds (`function`, `lambda`, `async_function`, `generator`, `async_generator`, `test.method`, `class`, `struct`, `interface`, `protocol`, `enum`, `property`, `event`, `delegate`, `namespace`, `import`); `references` accepts all indexed reference kinds (`call`, `instantiate`, `subscribe`, `attribute`, `annotation`, `type_reference`); `callers` / `callees` accept only the call-graph kinds (`call`, `instantiate`, `subscribe`) and reject non-call-graph kinds (`--kind attribute` / `--kind annotation` / `--kind type_reference`) with a usage error — metadata rows are attributed to the enclosing body-range symbol rather than the annotated target, and `type_reference` rows are compile-time type-position edges (declaration types, generic constraints, `is`/`as`/`instanceof`, XML-doc `cref`) rather than runtime calls, so `callers` / `callees` cannot answer either correctly; use `references --kind attribute` / `references --kind annotation` / `references --kind type_reference` instead. `references` defaults to every indexed reference kind so metadata usages remain visible, while `callers` / `callees` / `hotspots` / `impact` default to the call-graph kinds only (`call`, `instantiate`, `subscribe`) and exclude metadata edges (`attribute`, `annotation`, `type_reference`). Identical constructor `call` + `instantiate` rows at one physical site still collapse; `validate` uses issue kinds such as `bom` | | `--rank-by ` | `callers`, `callees` | Choose the caller/callee ranking model. `weighted` is the default and scores `instantiate=3.0`, `call=1.0`, `subscribe=0.1`; `count` sorts by raw `reference_count`; `kind` groups by reference kind first, then count. | -| `--body` | `definition`, `inspect` | Include reconstructed body content when the language extractor can infer the body range | +| `--body` | `definition`, `references`, `callers`, `callees`, `impact`, `inspect` | Include reconstructed body content or capped graph-location excerpts | | `--count` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `impact`, `unused`, `hotspots` | Return only counts. `search` / `definition` / `references` / `callers` / `callees` / `symbols` / `files` / `find` / `unused` ignore `--limit` and return authoritative totals; `impact` and `hotspots` still report the visible page count and may truncate with `--limit` (with `--json`: a single count object; commands that expose file counts add `files`) | | `--group-by ` | `hotspots` | Choose the hotspot grouping unit. The default is `symbol` for non-SQL scopes and `statement` for `--lang sql`, preserving SQL's statement-oriented grouping; JSON includes `grouped_by` so mixed-language callers can verify the active unit. `file` rolls symbol hotspot volume up to target files. | | `--group-by-name` | `hotspots` | Collapse rows that share the same `(name, kind)` across files into one representative result while preserving `definition_sites` / `paths` metadata in JSON. Hotspot ordering uses a weighted invocation score (`call` / `instantiate` = 1.0, `subscribe` = 0.3) while still showing the raw reference count; metadata-only edges such as `attribute`, `annotation`, and `type_reference` remain excluded from default hotspots. | @@ -1501,7 +1501,7 @@ If the checkout changed because of `git reset`, `git rebase`, `git commit --amen - Start by checking freshness with `status --check --json` when search correctness matters. If the index does not match the workspace, run `cdidx .` before trusting symbol or graph results. Use `map` / `map --json` for a quick overview of languages, modules, likely entrypoints, and high-activity areas. - Use `languages` as the source of truth for canonical `--lang` values and current symbol / graph support. Avoid relying on memorized per-language extraction details in prompts or agent instructions; support changes over time and the CLI reports `graph_supported`, `graph_support_reason`, and related trust metadata where it matters. - When you have a likely symbol name, run `symbols` first to resolve candidates. Add `--exact-name` once the intended symbol is known. Use `outline` for a single file's structure and `inspect` when you want bundled definition, reference, caller, and callee context in one request. -- Use `definition --body` for implementation text, then `references`, `callers`, `callees`, or `impact` for graph questions in supported languages. Prefer `--exact` after a candidate has been resolved so names such as `Run` do not expand to `RunAsync` or `RunImpact`. Treat graph fallback and degraded metadata as guidance about confidence, not as decoration. +- Use `definition --body` for implementation text, then `references --body`, `callers --body`, `callees --body`, or `impact --body` when graph results need inline excerpts. Prefer `--exact` after a candidate has been resolved so names such as `Run` do not expand to `RunAsync` or `RunImpact`. Treat graph fallback and degraded metadata as guidance about confidence, not as decoration. - Use `search` for raw text, comments, strings, option names, generated code, or languages where the current `languages` output says structured graph support is unavailable. Use `--include-generated` when generated code is the target. Use `--exact-substring` for punctuation-heavy literals and `--fts` only when you intentionally want raw FTS5 syntax such as `NEAR` or `OR`. - Scope broad searches early with `--path `, repeatable `--exclude-path `, and `--exclude-tests` unless tests are the target. Generated files are hidden from query results by default; pass `--include-generated` when needed. For noisy minified or transpiled files, reduce payload size with `--snippet-lines ` and `--max-line-width `. - Use `files` to discover candidate paths, `find` to re-locate exact text within known files, and `excerpt` to fetch only the needed lines instead of opening entire files. @@ -2818,15 +2818,13 @@ cdidx report --output report.tgz --json | `--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 で結合) | -| `--exclude-path ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect` | glob 形式のパスパターンを除外する。`*` と `?` がワイルドカード。繰り返し指定可 | | `--query ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `inspect`, `impact` | クエリを明示的なリテラルとして渡す。クエリが `-` で始まる場合に有用。`find` 以外のクエリ系コマンドでは `-- ` も1トークンのクエリエスケープとして受け付け、その後のオプション解析を続ける。 | | `--exclude-path ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect` | glob 形式のパスパターンを除外する。`*` と `?` がワイルドカード。繰り返し指定可 | | `--exclude-tests` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect` | テストらしいパスを除外し、本番コードを優先 | -| `--snippet-lines ` | `search` | 人間向け出力と JSON/MCP スニペットの抜粋行数(デフォルト: 8、最大: 20) | +| `--include-generated` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect`, `deps`, `impact`, `unused`, `hotspots` | 生成コードとして検出されたファイルを含める。生成ファイルは既定でクエリ結果から除外される | +| `--snippet-lines ` | `search`, `references`, `callers`, `callees`, `impact` | search スニペット、または graph `--body` 抜粋の行数(デフォルト: 8、最大: 20) | | `--snippet-focus ` | `search` | 長い検索結果行をクランプするときの焦点選択。`quality`(デフォルト)は全文一致や強いトークンを優先し、`proximity` は近接した複数トークンを優先し、`leftmost` は従来の最左一致を使う。 | -| `--max-line-width ` | `search`, `references`, `find`, `excerpt`, `inspect` | 極端に長い1行のスニペット・参照文脈・抜粋を、関連箇所の周辺だけに切り詰める(`0` でクランプ解除、デフォルト: 512、最大: 4096) | -| `--fts` | `search` | リテラル安全な引用ではなく生のFTS5クエリ構文を使う。`fts_chunks` が公開する列は `content` のみのため、列修飾子は `content:` だけが有効 | -| `--max-line-width ` | `search`, `references`, `find`, `excerpt`, `inspect` | 極端に長い1行のスニペット・参照文脈・抜粋を、関連箇所の周辺だけに切り詰める(`0` でクランプ解除、デフォルト: 512、最大: 4096) | +| `--max-line-width ` | `search`, `references`, `callers`, `callees`, `find`, `excerpt`, `impact`, `inspect` | 極端に長い1行のスニペット・参照文脈・抜粋を、関連箇所の周辺だけに切り詰める(`0` でクランプ解除、デフォルト: 512、最大: 4096) | | `--fts` | `search` | リテラル安全な引用ではなく生のFTS5クエリ構文を使う。壊れた入力はヒント付きの使用エラーになり、列修飾子は `content:` だけが有効 | | `--exact` | `search`, `find`, `symbols`, `definition`, `references`, `callers`, `callees`, `inspect` | 後方互換の短縮形。`search` では `--exact-substring`、`find` では `--exact` を使い、symbol / graph 系コマンドと `inspect` では `--exact-name` を推奨。CLI JSON と MCP `structuredContent` は `exact_index_available` / `degraded_reason` を返し、MCP では後方互換の camelCase alias も維持する。 | | `--exact-substring` | `search` | `search` 用の推奨 explicit alias。大文字小文字を区別する完全部分一致(FTS5 バイパス)。 | @@ -2834,7 +2832,7 @@ cdidx report --output report.tgz --json | `--exact-name` | `symbols`, `definition`, `references`, `callers`, `callees`, `inspect` | symbol-name exactness 用の推奨 explicit alias。NFKC + Unicode CaseFold による完全一致(`Ä` / `ä`、全角 `Run` / `Run`、合字、sharp-S、Greek final sigma を畳み込む)。Unicode CaseFold は locale-invariant のため、トルコ語の dotted `İ` は plain `i` と同一視しない。C# では `this` / `explicit` のような source keyword ではなく、抽出済みの canonical name(`operator +`、`operator checked +`、`explicit operator Money`、`implicit operator decimal`、`Item`)を渡す。DB に stale な fold metadata が残る間は ASCII `COLLATE NOCASE` に fallback するため、まず `cdidx backfill-fold`、または stale row を全置換できる通常の `cdidx index .`、それが無理なら `--rebuild` を使う(`status --json` の `fold_ready` と `csharp_symbol_name_ready` で判定)。read-only な旧DBに fallback exact-match index が無い場合は、人間向け出力が WARN を表示し、CLI JSON と MCP `structuredContent` が縮退メタデータを返す。 | | `--lang ` | クエリ系 | 言語でフィルタ(大文字小文字を区別しない。`--lang Python` は `--lang python` と同じ扱い)。`c#`、`cs`、`kt`、`kts` のような一般的な別名も受け付ける。未知の値を指定すると、人間向け出力の 0 件応答に `Available: <言語一覧>` ヒントが付く。 | | `--kind ` | `definition`, `references`, `callers`, `callees`, `symbols`, `hotspots`, `unused`, `validate` | 種別でフィルタ(大文字小文字を区別しない。`--kind FUNCTION` は `--kind function` と同じ扱い)。`definition` / `symbols` / `hotspots` / `unused` は symbol kind(`function`、`lambda`、`async_function`、`generator`、`async_generator`、`test.method`、`class`、`struct`、`interface`、`protocol`、`enum`、`property`、`event`、`delegate`、`namespace`、`import`)、`references` は全ての reference kind(`call`、`instantiate`、`subscribe`、`attribute`、`annotation`、`type_reference`)を受け付ける。`callers` / `callees` は call-graph 種別のみ(`call`、`instantiate`、`subscribe`)を受け付け、非 call-graph 種別(`--kind attribute` / `--kind annotation` / `--kind type_reference`)は usage error で拒否する — metadata 行は注釈対象そのものではなく body-range 上の外側シンボルに帰属し、`type_reference` は宣言型・generic 制約・`is`/`as`/`instanceof`・XML-doc `cref` といった compile-time な型位置エッジであり実行時呼び出しではないため、`callers` / `callees` はいずれの kind にも正しく答えられない。metadata / 型位置参照の列挙は `references --kind attribute` / `references --kind annotation` / `references --kind type_reference` を使う。`references` の既定は全 reference kind を表示して metadata 参照も見えるままにするが、`callers` / `callees` / `hotspots` / `impact` の既定は call-graph kind(`call`、`instantiate`、`subscribe`)のみで、`attribute` / `annotation` / `type_reference` のような metadata edge は除外する。同じ物理位置にある constructor の `call` + `instantiate` 重複行は引き続き集約する。`validate` は `bom` などの issue kind を使う | -| `--body` | `definition`, `inspect` | 言語抽出器が本体範囲を推論できる場合に本体内容も含める | +| `--body` | `definition`, `references`, `callers`, `callees`, `impact`, `inspect` | 再構成した本文、または上限付きの graph 位置抜粋を含める | | `--count` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `impact`, `unused`, `hotspots` | 件数だけを返す。`search` / `definition` / `references` / `callers` / `callees` / `symbols` / `files` / `find` / `unused` は `--limit` を無視した総件数を返し、`impact` と `hotspots` は visible page count のままで `--limit` によって切り詰められることがある(`--json` 併用時は単一の count オブジェクト。files 件数を出すコマンドは `files` も返す) | | `--group-by ` | `hotspots` | hotspot の集計単位を選ぶ。既定は非 SQL scope では `symbol`、`--lang sql` では既存の statement-oriented grouping を保つため `statement`。JSON には `grouped_by` が入り、mixed-language 呼び出しでも現在の単位を確認できる。`file` は symbol hotspot の参照量を対象ファイル単位にまとめる。 | | `--group-by-name` | `hotspots` | ファイルをまたいで同じ `(name, kind)` を共有する行を代表1件に集約し、JSON では `definition_sites` / `paths` metadata を保持したまま返す。hotspot の順位付けは重み付き invocation score(`call` / `instantiate` = 1.0、`subscribe` = 0.3)を使い、生の reference count も引き続き表示する。`attribute` / `annotation` / `type_reference` のような metadata-only edge は既定の hotspots から除外されたまま。 | @@ -3373,7 +3371,7 @@ system では、同じ `cdidx index . --quiet` を step として追加してく - 検索結果の正しさが重要な場合は、まず `status --check --json` で鮮度を確認する。インデックスが workspace と一致していなければ、symbol / graph 結果を信頼する前に `cdidx .` を実行する。全体像の把握には `map` / `map --json` を使い、言語、モジュール、entrypoint 候補、活動量の高い領域を先に見る。 - `--lang` に渡す正式名と現在の symbol / graph 対応状況は `languages` を正とする。プロンプトやエージェント向け手順に言語別抽出仕様を細かく固定しない。対応範囲は更新されるため、必要な場所では CLI が返す `graph_supported`、`graph_support_reason`、関連する trust metadata を読む。 - 候補シンボル名がある場合は、まず `symbols` で候補を固める。対象が決まったら `--exact-name` を付ける。1ファイルの構造だけ見たいときは `outline`、定義・参照・caller・callee のまとまった文脈が欲しいときは `inspect` を使う。 -- 実装本文が必要なら `definition --body` を使い、その後に graph 対応言語では `references`、`callers`、`callees`、`impact` を使う。候補が解決済みなら `--exact` を付け、`Run` が `RunAsync` や `RunImpact` に広がらないようにする。graph fallback や degraded metadata は信頼度の情報として扱う。 +- 実装本文が必要なら `definition --body` を使い、graph 結果にもインライン抜粋が必要なら `references --body`、`callers --body`、`callees --body`、`impact --body` を使う。候補が解決済みなら `--exact` を付け、`Run` が `RunAsync` や `RunImpact` に広がらないようにする。graph fallback や degraded metadata は信頼度の情報として扱う。 - 生テキスト、コメント、文字列、オプション名、生成コード、または現在の `languages` 出力で構造化 graph 対応がない言語には `search` を使う。記号を多く含む literal には `--exact-substring`、`NEAR` や `OR` などの FTS5 構文を意図して使う場合だけ `--fts` を使う。 - 広い検索は早い段階で `--path `、繰り返し指定できる `--exclude-path `、テストが目的でない場合の `--exclude-tests` で絞る。生成・minified・transpiled などノイズの大きいファイルでは `--snippet-lines ` と `--max-line-width ` で payload を小さくする。 - 候補パスの把握には `files`、既知ファイル内の再探索には `find`、必要行だけ読むときは `excerpt` を使い、ファイル全体を開かない。 diff --git a/changelog.d/unreleased/1594.added.md b/changelog.d/unreleased/1594.added.md new file mode 100644 index 0000000000..6eabf7dc73 --- /dev/null +++ b/changelog.d/unreleased/1594.added.md @@ -0,0 +1,20 @@ +--- +category: added +issues: + - 1594 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - src/CodeIndex/Cli/CliFlagSchema.cs + - src/CodeIndex/Cli/ConsoleUi.cs + - src/CodeIndex/Models/QueryResults.cs + - tests/CodeIndex.Tests/QueryCommandRunnerTests.cs + - USER_GUIDE.md +--- + +## English + +- **Graph queries can inline body excerpts (#1594)** — `references`, `callers`, `callees`, and `impact` now accept `--body` with `--snippet-lines` and `--max-line-width` to include capped body excerpts beside each matched location. + +## 日本語 + +- **グラフ系クエリで本文抜粋をインライン表示できるようになりました (#1594)** — `references`、`callers`、`callees`、`impact` は `--body`、`--snippet-lines`、`--max-line-width` を受け付け、各一致位置に上限付きの本文抜粋を付与します。 diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index d29fa52cb8..6a26b72db6 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -134,11 +134,11 @@ internal static class CliFlagSchema "definition", "references", "callers", "callees", "symbols", "inspect", ]; - private static readonly string[] BodyCommands = ["definition", "inspect"]; + private static readonly string[] BodyCommands = ["definition", "references", "callers", "callees", "impact", "inspect"]; private static readonly string[] MaxLineWidthCommands = [ - "search", "references", "find", "excerpt", "inspect", + "search", "references", "callers", "callees", "find", "excerpt", "impact", "inspect", ]; private static readonly string[] DbPathCommands = @@ -216,7 +216,7 @@ private static IReadOnlyList BuildAll() new() { Name = "--prefix", Description = "Trailing-asterisk prefix shorthand", Commands = Set("search") }, new() { Name = "--name", ValuePlaceholder = "", Description = "Exact symbol name", Commands = Set("symbols") }, new() { Name = "--max-line-width", ValuePlaceholder = "", Description = "Clamp long single-line payloads (0 disables clamping)", Commands = Set(MaxLineWidthCommands) }, - new() { Name = "--snippet-lines", ValuePlaceholder = "", Description = "Snippet length", Commands = Set("search") }, + new() { Name = "--snippet-lines", ValuePlaceholder = "", Description = "Snippet length", Commands = Set("search", "references", "callers", "callees", "impact") }, new() { Name = "--snippet-focus", ValuePlaceholder = "", Description = "Search snippet long-line focus mode", Commands = Set("search") }, new() { Name = "--fts", Description = "Raw FTS5 syntax", Commands = Set("search") }, new() { Name = "--no-dedup", Description = "Show duplicate chunks", Commands = Set("search") }, diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 0bb2ccc746..a4cd0b082c 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -70,9 +70,9 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("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 ] [--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 ] [--lang ] [--kind ] [--visibility ] [--exclude-visibility ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--exact|--exact-name] [--count] [--since ]"), - ("references", "cdidx references |--query |-- [--db ] [--json] [--verbose] [--limit ] [--lang ] [--kind ] [--path ] [--exclude-path ] [--exclude-tests] [--max-line-width ] [--exact|--exact-name] [--count]"), - ("callers", "cdidx callers |--query |-- [--db ] [--json] [--verbose] [--limit ] [--lang ] [--kind ] [--rank-by ] [--raw-kinds] [--path ] [--exclude-path ] [--exclude-tests] [--exact|--exact-name] [--count]"), - ("callees", "cdidx callees |--query |-- [--db ] [--json] [--verbose] [--limit ] [--lang ] [--kind ] [--rank-by ] [--raw-kinds] [--path ] [--exclude-path ] [--exclude-tests] [--exact|--exact-name] [--count]"), + ("references", "cdidx references |--query |-- [--db ] [--json] [--verbose] [--limit ] [--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 ] [--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 ] [--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 ] [--lang ] [--kind ] [--visibility ] [--exclude-visibility ] [--path ] [--exclude-path ] [--exclude-tests] [--exact|--exact-name] [--count] [--since ]"), ("files", "cdidx files [query|--query |-- ] [--db ] [--json] [--verbose] [--limit ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count] [--since ] [--bytes]"), ("find", "cdidx find --path [--db ] [--json] [--verbose] [--limit ] [--lang ] [--exclude-path ] [--exclude-tests] [--before ] [--after ] [--max-line-width ] [--exact] [--count]"), @@ -85,7 +85,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("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 ]"), - ("impact", "cdidx impact |--query |-- [--db ] [--json] [--verbose] [--limit ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--max-hops ] [--count] [--with-paths]"), + ("impact", "cdidx impact |--query |-- [--db ] [--json] [--verbose] [--limit ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--snippet-lines ] [--max-line-width ] [--max-hops ] [--count] [--with-paths]"), ("deps", "cdidx deps [--db ] [--json] [--verbose] [--limit ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--reverse]"), ("unused", "cdidx unused [--db ] [--json] [--verbose] [--limit ] [--kind ] [--visibility ] [--exclude-visibility ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count]"), ("hotspots", "cdidx hotspots [--db ] [--json] [--verbose] [--limit ] [--kind ] [--visibility ] [--exclude-visibility ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--count] [--group-by ] [--group-by-name]"), @@ -715,7 +715,7 @@ void WriteHelpLine(string line = "") Console.WriteLine(" --include-generated Include generated files in query results"); Console.WriteLine(" --snippet-lines Search snippet length (1-20, default: 8)"); Console.WriteLine(" --snippet-focus search only: long-line focus mode (leftmost|quality|proximity, default: quality)"); - WriteHelpLine($" --max-line-width search/references/find/excerpt/inspect only: clamp very long single-line snippet/context/excerpt payloads (`0` disables clamping; default: {LineWidthFormatter.DefaultMaxLineWidth})"); + WriteHelpLine($" --max-line-width search/references/callers/callees/find/excerpt/impact/inspect only: clamp very long single-line snippet/context/excerpt payloads (`0` disables clamping; default: {LineWidthFormatter.DefaultMaxLineWidth})"); Console.WriteLine(" --focus-line excerpt: line whose focused column should stay visible (requires --focus-column)"); Console.WriteLine(" --focus-column excerpt: column to keep centered when clamping (must be within the focused line)"); Console.WriteLine(" --focus-length excerpt: width of the focused span (default: 1, requires --focus-column)"); diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 93fd68a74b..04e174c26c 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -689,6 +689,8 @@ public static int RunReferences(string[] cmdArgs, JsonSerializerOptions jsonOpti } var results = reader.SearchReferences(options.Query, options.Limit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact, options.MaxLineWidth); + if (options.IncludeBody) + AttachBodyExcerpts(reader, results, options.SnippetLines, options.MaxLineWidth); var sqlGraphSignal = NarrowSqlGraphContractSignalByLanguages(baseSqlGraphSignal, results.Select(result => result.Lang), options.Lang, exactGraphLanguage); var exactSignal = reader.GetReferencesExactQuerySignal(options.Lang, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, includeSqlGraphContractSignal: sqlGraphSignal.Relevant); var exactZeroHint = BuildExactZeroHint( @@ -731,6 +733,7 @@ public static int RunReferences(string[] cmdArgs, JsonSerializerOptions jsonOpti var owner = r.ContainerName != null ? $" in {r.ContainerName}" : ""; Console.WriteLine($"{r.ReferenceKind,-12} {r.SymbolName,-32} {r.Path}:{r.Line}:{r.Column}{owner}"); Console.WriteLine($" {r.Context}"); + WriteOptionalBodyExcerpt(r.BodyStartLine, r.BodyContent); } var refFileCount = results.Select(r => r.Path).Distinct().Count(); Console.Error.WriteLine($"({results.Count} references in {refFileCount} files)"); @@ -741,7 +744,7 @@ public static int RunReferences(string[] cmdArgs, JsonSerializerOptions jsonOpti public static int RunCallers(string[] cmdArgs, JsonSerializerOptions jsonOptions) { - var previewOptionError = ValidatePreviewOptions("callers", cmdArgs, allowMaxLineWidth: false, allowFocusOptions: false); + var previewOptionError = ValidatePreviewOptions("callers", cmdArgs, allowMaxLineWidth: true, allowFocusOptions: false); if (previewOptionError != null) { Console.Error.WriteLine(previewOptionError); @@ -815,6 +818,8 @@ public static int RunCallers(string[] cmdArgs, JsonSerializerOptions jsonOptions } var results = reader.GetCallers(options.Query, options.Limit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact, options.RawKinds, options.RankMode); + if (options.IncludeBody) + AttachBodyExcerpts(reader, results, options.SnippetLines, options.MaxLineWidth); var sqlGraphSignal = NarrowSqlGraphContractSignalByLanguages(baseSqlGraphSignal, results.Select(result => result.Lang), options.Lang, exactGraphLanguage); var exactSignal = reader.GetCallersExactQuerySignal(options.Lang, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, includeSqlGraphContractSignal: sqlGraphSignal.Relevant); var exactZeroHint = BuildExactZeroHint( @@ -857,6 +862,7 @@ public static int RunCallers(string[] cmdArgs, JsonSerializerOptions jsonOptions { var kindLabel = FormatReferenceKindLabel(r.ReferenceKind, r.ReferenceKinds, r.HasMixedReferenceKinds, r.ReferenceKindCounts); Console.WriteLine($"{kindLabel.PadRight(kindColumnWidth)} {r.CallerKind ?? "?",-10} {r.CallerName ?? "",-32} {r.Path}:{r.FirstLine} -> {r.CalleeName} ({r.ReferenceCount} refs)"); + WriteOptionalBodyExcerpt(r.BodyStartLine, r.BodyContent); } var callerFileCount = results.Select(r => r.Path).Distinct().Count(); Console.Error.WriteLine($"({results.Count} callers in {callerFileCount} files)"); @@ -867,7 +873,7 @@ public static int RunCallers(string[] cmdArgs, JsonSerializerOptions jsonOptions public static int RunCallees(string[] cmdArgs, JsonSerializerOptions jsonOptions) { - var previewOptionError = ValidatePreviewOptions("callees", cmdArgs, allowMaxLineWidth: false, allowFocusOptions: false); + var previewOptionError = ValidatePreviewOptions("callees", cmdArgs, allowMaxLineWidth: true, allowFocusOptions: false); if (previewOptionError != null) { Console.Error.WriteLine(previewOptionError); @@ -941,6 +947,8 @@ public static int RunCallees(string[] cmdArgs, JsonSerializerOptions jsonOptions } var results = reader.GetCallees(options.Query, options.Limit, options.Lang, options.Kind, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, exact, options.RawKinds, options.RankMode); + if (options.IncludeBody) + AttachBodyExcerpts(reader, results, options.SnippetLines, options.MaxLineWidth); var sqlGraphSignal = NarrowSqlGraphContractSignalByLanguages(baseSqlGraphSignal, results.Select(result => result.Lang), options.Lang, exactGraphLanguage); var exactSignal = reader.GetCalleesExactQuerySignal(options.Lang, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, includeSqlGraphContractSignal: sqlGraphSignal.Relevant); var exactZeroHint = BuildExactZeroHint( @@ -983,6 +991,7 @@ public static int RunCallees(string[] cmdArgs, JsonSerializerOptions jsonOptions { var kindLabel = FormatReferenceKindLabel(r.ReferenceKind, r.ReferenceKinds, r.HasMixedReferenceKinds, r.ReferenceKindCounts); Console.WriteLine($"{kindLabel.PadRight(kindColumnWidth)} {r.CalleeName,-32} {r.Path}:{r.FirstLine} <- {r.CallerName ?? ""} ({r.ReferenceCount} refs)"); + WriteOptionalBodyExcerpt(r.BodyStartLine, r.BodyContent); } var calleeFileCount = results.Select(r => r.Path).Distinct().Count(); Console.Error.WriteLine($"({results.Count} callees in {calleeFileCount} files)"); @@ -991,6 +1000,138 @@ public static int RunCallees(string[] cmdArgs, JsonSerializerOptions jsonOptions }); } + private static void AttachBodyExcerpts(DbReader reader, IEnumerable results, int snippetLines, int maxLineWidth) + { + foreach (var result in results) + { + var excerpt = result.ContainerName != null + ? BuildSymbolBodyExcerpt(reader, result.Path, result.Lang, result.ContainerName, snippetLines, maxLineWidth) + : null; + excerpt ??= BuildBodyExcerpt(reader, result.Path, result.Line, snippetLines, maxLineWidth, focusColumn: result.Column, focusLength: Math.Max(1, result.SymbolName.Length)); + ApplyBodyExcerpt(result, excerpt); + } + } + + private static void AttachBodyExcerpts(DbReader reader, IEnumerable results, int snippetLines, int maxLineWidth) + { + foreach (var result in results) + { + var excerpt = result.CallerName != null + ? BuildSymbolBodyExcerpt(reader, result.Path, result.Lang, result.CallerName, snippetLines, maxLineWidth) + : null; + excerpt ??= BuildBodyExcerpt(reader, result.Path, result.FirstLine, snippetLines, maxLineWidth); + ApplyBodyExcerpt(result, excerpt); + } + } + + private static void AttachBodyExcerpts(DbReader reader, IEnumerable results, int snippetLines, int maxLineWidth) + { + foreach (var result in results) + { + var excerpt = BuildSymbolBodyExcerpt(reader, result.Path, result.Lang, result.CalleeName, snippetLines, maxLineWidth) + ?? BuildBodyExcerpt(reader, result.Path, result.FirstLine, snippetLines, maxLineWidth); + ApplyBodyExcerpt(result, excerpt); + } + } + + private static void AttachBodyExcerpts(DbReader reader, IEnumerable results, int snippetLines, int maxLineWidth) + { + foreach (var result in results) + { + var excerpt = result.CallerName != null + ? BuildSymbolBodyExcerpt(reader, result.Path, result.Lang, result.CallerName, snippetLines, maxLineWidth) + : null; + excerpt ??= BuildBodyExcerpt(reader, result.Path, result.FirstLine, snippetLines, maxLineWidth); + ApplyBodyExcerpt(result, excerpt); + } + } + + private static FileExcerptResult? BuildSymbolBodyExcerpt(DbReader reader, string path, string? lang, string symbolName, int snippetLines, int maxLineWidth) + { + var definitions = reader.GetDefinitions( + symbolName, + limit: 1, + kind: null, + lang: lang, + includeBody: true, + pathPatterns: [path], + excludePathPatterns: null, + excludeTests: false, + since: null, + exact: true); + var definition = definitions.FirstOrDefault(); + if (definition == null) + return null; + + var startLine = definition.StartLine; + var naturalEndLine = definition.BodyEndLine ?? definition.EndLine; + var cappedEndLine = (int)Math.Min(naturalEndLine, (long)startLine + SearchSnippetFormatter.ClampSnippetLines(snippetLines) - 1); + return reader.GetExcerpt(path, startLine, cappedEndLine, maxLineWidth: maxLineWidth, focusLine: startLine); + } + + private static FileExcerptResult? BuildBodyExcerpt(DbReader reader, string path, int line, int snippetLines, int maxLineWidth, int? focusColumn = null, int focusLength = 1) + { + var cappedLines = SearchSnippetFormatter.ClampSnippetLines(snippetLines); + var endLine = (int)Math.Min(int.MaxValue, (long)line + cappedLines - 1); + return reader.GetExcerpt( + path, + line, + endLine, + maxLineWidth: maxLineWidth, + focusLine: line, + focusColumn: focusColumn, + focusLength: focusLength); + } + + private static void ApplyBodyExcerpt(ReferenceResult result, FileExcerptResult? excerpt) + { + if (excerpt == null) + return; + result.BodyContent = excerpt.Content; + result.BodyStartLine = excerpt.StartLine; + result.BodyEndLine = excerpt.EndLine; + result.BodyContentTruncated = excerpt.ContentTruncated; + } + + private static void ApplyBodyExcerpt(CallerResult result, FileExcerptResult? excerpt) + { + if (excerpt == null) + return; + result.BodyContent = excerpt.Content; + result.BodyStartLine = excerpt.StartLine; + result.BodyEndLine = excerpt.EndLine; + result.BodyContentTruncated = excerpt.ContentTruncated; + } + + private static void ApplyBodyExcerpt(CalleeResult result, FileExcerptResult? excerpt) + { + if (excerpt == null) + return; + result.BodyContent = excerpt.Content; + result.BodyStartLine = excerpt.StartLine; + result.BodyEndLine = excerpt.EndLine; + result.BodyContentTruncated = excerpt.ContentTruncated; + } + + private static void ApplyBodyExcerpt(ImpactResult result, FileExcerptResult? excerpt) + { + if (excerpt == null) + return; + result.BodyContent = excerpt.Content; + result.BodyStartLine = excerpt.StartLine; + result.BodyEndLine = excerpt.EndLine; + result.BodyContentTruncated = excerpt.ContentTruncated; + } + + private static void WriteOptionalBodyExcerpt(int? startLine, string? content, string indent = "") + { + if (startLine == null || content == null) + return; + + Console.WriteLine($"{indent} Body:"); + WriteNumberedExcerpt(startLine.Value, content, indent + " "); + } + /// /// Build the OR-joined name list for `symbols`: first positional + extra positionals + --name values. /// Pipe characters are treated as literal name characters so operator symbols like `operator |` remain searchable. @@ -2281,7 +2422,7 @@ public static int RunVacuum(string[] cmdArgs, JsonSerializerOptions jsonOptions) public static int RunImpact(string[] cmdArgs, JsonSerializerOptions jsonOptions) { - var previewOptionError = ValidatePreviewOptions("impact", cmdArgs, allowMaxLineWidth: false, allowFocusOptions: false); + var previewOptionError = ValidatePreviewOptions("impact", cmdArgs, allowMaxLineWidth: true, allowFocusOptions: false); if (previewOptionError != null) { Console.Error.WriteLine(previewOptionError); @@ -2319,6 +2460,8 @@ public static int RunImpact(string[] cmdArgs, JsonSerializerOptions jsonOptions) if (!options.Json && options.ImpactDeprecatedDepthUsed) Console.Error.WriteLine("Warning: --depth is deprecated for impact; use --max-hops instead."); var analysis = reader.AnalyzeImpact(options.Query, maxDepth, options.Limit, options.Lang, options.PathPatterns, options.ExcludePaths, options.ExcludeTests, options.WithPaths); + if (options.IncludeBody) + AttachBodyExcerpts(reader, analysis.Callers, options.SnippetLines, options.MaxLineWidth); var sqlGraphSignal = NarrowSqlGraphContractSignal( reader.GetSqlGraphContractSignal(options.Lang, options.PathPatterns, options.ExcludePaths, options.ExcludeTests), DbReader.IsSqlLanguage(options.Lang) @@ -2578,6 +2721,7 @@ public static int RunImpact(string[] cmdArgs, JsonSerializerOptions jsonOptions) { var indent = new string(' ', (r.Depth - 1) * 2); Console.WriteLine($" {indent}{r.CallerKind ?? "?",-10} {r.CallerName ?? "",-32} {r.Path}:{r.FirstLine} -> {r.CalleeName} ({r.ReferenceCount} refs)"); + WriteOptionalBodyExcerpt(r.BodyStartLine, r.BodyContent, $" {indent}"); if (options.WithPaths && r.Paths != null) { foreach (var p in r.Paths) @@ -4903,11 +5047,11 @@ private static void WriteVerboseQueryDebug(QueryCommandOptions options, IReadOnl }.ToJsonString(jsonOptions)); } - private static void WriteNumberedExcerpt(int startLine, string content) + private static void WriteNumberedExcerpt(int startLine, string content, string indent = "") { var lines = content.Split('\n'); for (int i = 0; i < lines.Length; i++) - Console.WriteLine($" {startLine + i,4}: {lines[i]}"); + Console.WriteLine($"{indent} {startLine + i,4}: {lines[i]}"); } private static bool TryWriteParseError(QueryCommandOptions options, string commandName) diff --git a/src/CodeIndex/Models/QueryResults.cs b/src/CodeIndex/Models/QueryResults.cs index 339089cd26..fbc738c58d 100644 --- a/src/CodeIndex/Models/QueryResults.cs +++ b/src/CodeIndex/Models/QueryResults.cs @@ -204,6 +204,14 @@ public class ReferenceResult public string? ContainerName { get; set; } public bool IsSelfReference { get; set; } public bool IsMutualRecursion { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? BodyContent { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? BodyStartLine { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? BodyEndLine { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool BodyContentTruncated { get; set; } } public class CallerResult @@ -231,6 +239,14 @@ public class CallerResult public int ReferenceCount { get; set; } public bool HasSelfReference { get; set; } public bool HasMutualRecursion { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? BodyContent { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? BodyStartLine { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? BodyEndLine { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool BodyContentTruncated { get; set; } } public class CalleeResult @@ -249,6 +265,14 @@ public class CalleeResult public double ReferenceWeightScore { get; set; } public int FirstLine { get; set; } public int ReferenceCount { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? BodyContent { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? BodyStartLine { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? BodyEndLine { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool BodyContentTruncated { get; set; } } public class ImpactResult @@ -279,6 +303,14 @@ public class ImpactResult // 同一 caller に対して保持上限を超える別経路が存在する場合に true。 [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public bool PathsTruncated { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? BodyContent { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? BodyStartLine { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? BodyEndLine { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool BodyContentTruncated { get; set; } } public static class ImpactResultKinds diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index a0d5b4cb6b..62724d8faa 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -71,11 +71,11 @@ public void PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags() Assert.Contains("cdidx callees |--query |-- ", output); Assert.Contains("cdidx search |--query |-- [--db ] [--json[=ndjson|array]] [--verbose] [--limit ] [--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 ] [--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 ] [--lang ] [--kind ] [--path ] [--exclude-path ] [--exclude-tests] [--max-line-width ] [--exact|--exact-name] [--count]", output); + Assert.Contains("cdidx references |--query |-- [--db ] [--json] [--verbose] [--limit ] [--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 ] [--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); - Assert.Contains("--max-line-width search/references/find/excerpt/inspect only: clamp very long single-line snippet/context/excerpt payloads (`0` disables clamping; default: 512)", output); + Assert.Contains("--max-line-width search/references/callers/callees/find/excerpt/impact/inspect only: clamp very long single-line snippet/context/excerpt payloads (`0` disables clamping; default: 512)", output); Assert.Contains("cdidx find --path ", output); Assert.Contains("--fts Use raw FTS5 query syntax for search (search query max 1000 chars; raw FTS parser max 2000 chars, 64 boolean ops, 16 NEAR ops", output); Assert.Contains("--exact Backward-compatible shorthand.", output); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index 9a27d3aa2d..8925ee1fd2 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -4152,6 +4152,95 @@ public void RunSymbols_EmptyAfterNormalizationFailsClosed() } } + [Fact] + public void GraphCommands_BodyOptionAddsCappedBodyExcerpt_Issue1594() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_graph_body"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + TestProjectHelper.InsertIndexedFile(dbPath, "src/Session.cs", "csharp", """ + class Session + { + int Run(int user) + { + var value = user; + return value; + } + + int Login(int user) + { + return Run(user); + } + } + """); + using (var db = new DbContext(dbPath)) + { + using var select = db.Connection.CreateCommand(); + select.CommandText = "SELECT id FROM files WHERE path = 'src/Session.cs'"; + var fileId = Convert.ToInt32(select.ExecuteScalar()); + var writer = new DbWriter(db.Connection); + writer.InsertReferences([ + new ReferenceRecord + { + FileId = fileId, + SymbolName = "Run", + ReferenceKind = "call", + Line = 11, + Column = 16, + Context = " return Run(user);", + ContainerKind = "function", + ContainerName = "Login", + } + ]); + writer.MarkGraphReady(); + } + + AssertBodyExcerpt( + QueryCommandRunner.RunReferences, + ["Run", "--db", dbPath, "--json", "--body", "--snippet-lines", "1"], + "int Login(int user)"); + AssertBodyExcerpt( + QueryCommandRunner.RunCallers, + ["Run", "--db", dbPath, "--json", "--body", "--snippet-lines", "2"], + "int Login(int user)"); + AssertBodyExcerpt( + QueryCommandRunner.RunCallees, + ["Login", "--db", dbPath, "--json", "--body", "--snippet-lines", "1"], + "int Run(int user)"); + + var (impactExitCode, impactStdout, impactStderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["Run", "--db", dbPath, "--json", "--body", "--snippet-lines", "2"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.Success, impactExitCode); + Assert.Equal(string.Empty, impactStderr); + using var impactDocument = ParseJsonOutput(impactStdout); + var impactCaller = impactDocument.RootElement.GetProperty("callers")[0]; + Assert.Contains("int Login(int user)", impactCaller.GetProperty("body_content").GetString()); + Assert.Equal(2, CountLines(impactCaller.GetProperty("body_content").GetString()!)); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + private void AssertBodyExcerpt( + Func command, + string[] args, + string expectedContent) + { + var (exitCode, stdout, stderr) = CaptureConsole(() => command(args, _jsonOptions)); + + Assert.True(exitCode == CommandExitCodes.Success, $"exit={exitCode}\nstdout={stdout}\nstderr={stderr}"); + Assert.Equal(string.Empty, stderr); + using var document = ParseJsonOutput(stdout); + Assert.Contains(expectedContent, document.RootElement.GetProperty("body_content").GetString()); + } + + private static int CountLines(string text) => text.Split('\n').Length; + [Fact] public void RunSymbols_ExactNameFindsXamlTargetType() {