Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ compact metadata. Keep this static path ahead of database access and runtime
diagnostic enrichment. See [MCP status explanations](docs/mcp-status-explanations.md#english)
for argument compatibility, output schemas, byte scopes and measured retry errors.

## LSP call hierarchy (#5351)

`LspServer.CallHierarchy.cs` adapts persisted call identities to standard LSP items;
`DbReader.CallHierarchy.cs` retains individual sites rather than grouped display rows.
Preserve generation checks, cancellation, checksum verification and explicit failure
when evidence or budgets cannot support the result. See [the contract](docs/lsp-call-hierarchy.md#english).

## C# scoped-update expansion (#5347)

See [C# update scope diagnostics](docs/csharp-update-expansion.md#english) for the
Expand Down Expand Up @@ -4580,6 +4587,13 @@ MCP の `status.explainField` は、CLI の serializer 由来フィールド説
引数の併用条件、出力スキーマ、バイト数の計測範囲、実測値付き再試行エラーについては
[MCP status の説明](docs/mcp-status-explanations.md#日本語)を参照してください。

### LSPコール階層 (#5351)

`LspServer.CallHierarchy.cs` は永続化済みの呼び出し識別情報を標準LSP項目へ変換し、
`DbReader.CallHierarchy.cs` は表示用の集約行ではなく個々の呼び出し箇所を返します。
世代確認、キャンセル、チェックサム検証を維持し、証拠や上限の制約で結果を保証できない
場合は明示的に失敗させてください。[契約](docs/lsp-call-hierarchy.md#日本語)を参照してください。

名前付きクエリの行選択(#5325)は、各クエリの固定 10,000 候補をフィルター・重複排除した後、
`ApplySearchOutputSelection` を独立に利用します。重複排除前の候補上限到達は
`SearchWithCandidateEvidence` で保持してください。first-per-file、sample、クエリ上限、
Expand Down
18 changes: 18 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ Keep tools/list schema validation and aggregate explain controls alongside these
Run `--filter 'FullyQualifiedName~Issue5352|FullyQualifiedName~Issue5093|FullyQualifiedName~RunStatus_Explain|FullyQualifiedName~ToolsCall_Status'`
on net8/net9 and verify initialized stdio MCP calls.

## LSP call hierarchy coverage (#5351)

`LspCallHierarchyTests` runs framed, initialized LSP sessions against real indexed
temporary projects on net8.0/net9.0. Keep exact overload/same-name identities,
recursive and repeated sites, UTF-16/file-URI ranges, existing navigation controls,
unsaved/stale/incomplete/ambiguous evidence, exact file identity, overlapping large
callable ranges, high-degree/response-size bounds and cancellation with session
reuse together. See [the protocol contract](docs/lsp-call-hierarchy.md#english).

MCP search parity coverage (#5349) lives in `McpServerIssue5349Tests` and
`HttpMcpTransportTests`. Run `--filter FullyQualifiedName~Issue5349` on net8/net9
with existing MCP schema/dispatch and CLI find/search-classification tests.
Expand Down Expand Up @@ -1452,6 +1461,15 @@ Check the following:
net8/net9 で `--filter 'FullyQualifiedName~Issue5352|FullyQualifiedName~Issue5093|FullyQualifiedName~RunStatus_Explain|FullyQualifiedName~ToolsCall_Status'`
を実行し、初期化済み stdio MCP の呼び出しも確認してください。

### LSPコール階層の検証 (#5351)

`LspCallHierarchyTests` は実際に索引を作成した一時プロジェクトを使い、初期化済みの
LSPセッションにフレーム形式で要求を送り、net8.0/net9.0で検証します。同名・
オーバーロードの正確な識別、再帰・複数の呼び出し箇所、UTF-16とファイルURIの範囲、
既存ナビゲーション、未保存・古い・未完成・曖昧な証拠、正確なファイル識別、重なり合う
大きな関数範囲、高次数・応答サイズの上限、キャンセル後のセッション継続を併せて
維持してください。[プロトコル契約](docs/lsp-call-hierarchy.md#日本語)を参照してください。

名前付き検索の行選択(#5325)は、重複チャンクを含む小さな共通 fixture で単一・複数・
共有ファイル・空クエリ、selector、全体/クエリ上限、rich / compact / 投影 JSON、text、
決定的な再実行、UTF-8 上限の境界、下限表記、非対応形式を検証します。
Expand Down
5 changes: 5 additions & 0 deletions USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3636,6 +3636,9 @@ MCP stdio is line protocol: send one compact UTF-8 JSON-RPC object per LF-delimi

### LSP Server (for LSP-native editors)

Indexed call hierarchy is available through the standard prepare/incoming/outgoing
methods. See [supported languages, error behavior and limits](docs/lsp-call-hierarchy.md#english).

`cdidx lsp --db .cdidx/codeindex.db` starts a read-only Language Server Protocol
server over stdio. It reuses the existing CodeIndex database and exposes
`initialize`, `workspace/symbol`, `textDocument/documentSymbol`,
Expand Down Expand Up @@ -7716,6 +7719,8 @@ request は JSON-RPC `-32002`(`Server not initialized`)を返します。重
`textDocument/inlayHint` は end position を含まない requested LSP range を尊重し、
indexed return type が symbol name の直前にすでに明記されている場合は type label を
省略するため、field / property / method の明示型を hint として重複表示しません。
標準の準備・呼び出し元・呼び出し先メソッドによるコール階層に対応しています。
[対象言語・エラー動作・上限](docs/lsp-call-hierarchy.md#日本語)を参照してください。
対応する provider は `initialize` が返す `capabilities` で確認してください。
未実装の optional LSP method は advertise しません。現在の対応状況では
`textDocument/typeDefinition`、`textDocument/implementation`、`textDocument/codeLens`、
Expand Down
17 changes: 17 additions & 0 deletions changelog.d/unreleased/5351.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
category: added
issues:
- 5351
affected:
- src/CodeIndex/Lsp/LspServer.CallHierarchy.cs
- src/CodeIndex/Database/DbReader.CallHierarchy.cs
- docs/lsp-call-hierarchy.md
---

## English

- **Added indexed LSP call hierarchy (#5351)** — Editors can prepare and expand incoming/outgoing calls using generation-bound symbol identities, UTF-16 ranges and repeated call sites. Unavailable, ambiguous, stale or over-budget evidence produces explicit LSP errors; successful results retain indexed-graph limitations.

## 日本語

- **索引に基づくLSPコール階層を追加しました (#5351)** — 世代に結び付いたシンボル識別情報、UTF-16範囲、複数の呼び出し箇所を使い、エディターから呼び出し元・先を展開できます。利用不能・曖昧・古い証拠や上限超過には明示的なLSPエラーを返し、成功時も索引グラフの制限を通知します。
88 changes: 88 additions & 0 deletions docs/lsp-call-hierarchy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Indexed LSP call hierarchy

## English

`cdidx lsp --db .cdidx/codeindex.db` advertises `callHierarchyProvider` and supports
`textDocument/prepareCallHierarchy`, `callHierarchy/incomingCalls`, and
`callHierarchy/outgoingCalls`. Prepare on a callable declaration or uniquely
resolved indexed reference, then send the returned item unchanged to expand it.
Recursive calls remain traversable. Repeated call sites are grouped by exact
endpoint identity and retained in `fromRanges`.

The initial language scope is C#, Java, JavaScript, TypeScript, Python, Go, Rust,
C, C++, Kotlin, Ruby, PHP and Swift. Only indexed `function`, `test.method` and
`lambda` symbols with usable ranges and current reference identities qualify.
Edges use the shared CLI/MCP persisted `call` references; this does not add language
binding, implicit constructor calls, subscriptions or member reads. Ambiguous
overloads and same-name symbols are never combined by name. Preparation uses the
existing LSP position and invocation-selection rules without a bare-name fallback.

This is heuristic indexed evidence. External calls, dynamic dispatch and unindexed
code can be absent even when the stored graph is complete. Each successful request
sends a standard `window/logMessage` reminder, including for an empty result;
item `detail` also labels the evidence as indexed. An empty expansion means no
eligible indexed calls were found, not that the callable has no possible callers
or callees. Non-callable declarations and positions without a token return `null`
from preparation.

Incomplete/unavailable graphs, unresolved or ambiguous contributing references,
unusable source ranges and exceeded budgets return LSP `RequestFailed` (`-32803`).
The server returns no successful partial hierarchy. Changed files, unsaved open
documents and stale items return `ContentModified` (`-32801`). Error `data.reason`
contains a bounded machine reason. Save documents, refresh the index, and prepare
again. If the live-document cache has discarded any text, save and reconnect too.
The opaque `data` string (at most 160 characters) binds the item to the server
session, indexed generation and exact symbol ID; never reuse it across sessions
or indexing runs. File URIs and all positions follow the existing LSP path policy
and zero-based UTF-16 coordinates.

Each request allows at most 1,000 raw call sites, 100 distinct neighbors, 512 KiB
of response JSON, 4 MiB per source file and a 16 MiB source set read twice, with a five-second
query deadline and SQLite cancellation. `$/cancelRequest` returns `-32800` and
leaves the session usable. Call hierarchy does not stream partial results or
advertise work-done progress. Use CLI/MCP graph diagnostics when a hierarchy
exceeds these bounds. All retained open buffers must match disk and the index;
participating source files are checksum checked before and after range assembly.
This does not perform a whole-workspace filesystem freshness scan per request.

## 日本語

`cdidx lsp --db .cdidx/codeindex.db` は `callHierarchyProvider` を通知し、
`textDocument/prepareCallHierarchy`、`callHierarchy/incomingCalls`、
`callHierarchy/outgoingCalls` に対応します。呼び出し可能な宣言、または索引内で
一意に解決できる参照位置で準備し、返された項目を変更せずに渡して展開します。
再帰呼び出しも辿れます。同じ相手への複数の呼び出し箇所は、正確なシンボルIDで
まとめ、すべての位置を `fromRanges` に保持します。

初期の対象言語は C#、Java、JavaScript、TypeScript、Python、Go、Rust、C、C++、
Kotlin、Ruby、PHP、Swift です。有効な範囲と最新の参照識別情報を持つ、索引内の
`function`、`test.method`、`lambda` シンボルが対象です。CLI/MCP と共通の永続化済み
`call` 参照を使い、言語の束縛解析、暗黙のコンストラクター呼び出し、購読、
メンバー読み取りは追加しません。曖昧なオーバーロードや同名シンボルを名前だけで
統合しません。準備処理は既存LSPの位置・引数に基づく選択規則を利用し、単純な
名前検索へのフォールバックは行いません。

結果は索引に保存されたヒューリスティックな証拠です。保存済みグラフが完全でも、
外部呼び出し、動的ディスパッチ、索引対象外のコードは含まれないことがあります。
成功した要求では空の結果も含めて標準の `window/logMessage` でこの制限を通知し、
項目の `detail` にも索引由来であることを表示します。空の展開結果は該当する索引内の
呼び出しが見つからなかったことを意味し、呼び出し元・先が存在しないことの証明には
なりません。呼び出せない宣言やトークンのない位置での準備は `null` を返します。

グラフが未完成・利用不能、関連する参照が未解決・曖昧、ソース範囲が利用不能、
または上限超過の場合は LSP `RequestFailed`(`-32803`)を返します。部分的な階層を
成功として返しません。ファイル変更、開いている文書の未保存編集、古い項目には
`ContentModified`(`-32801`)を返します。エラーの `data.reason` に上限付きの
機械判読用理由を含めます。保存・索引更新の後、準備からやり直してください。
文書キャッシュがテキストを破棄した場合は、保存後に再接続も必要です。
最大160文字の不透明な `data` はセッション・索引世代・正確なシンボルIDに結び付きます。
再接続や索引更新をまたいで再利用しないでください。ファイルURIは既存のLSPパス方針、
位置は0始まりのUTF-16座標に従います。

要求ごとの上限は生の呼び出し箇所1,000件、相手シンボル100件、応答JSON 512 KiB、
ソース1ファイル4 MiB、2回読み取るソース集合の合計16 MiBです。問い合わせの期限は5秒で、
SQLiteの処理にもキャンセルを適用します。`$/cancelRequest` は `-32800` を返し、
セッションは継続利用できます。コール階層の部分結果配信・進捗通知は提供しません。
上限に達した場合はCLI/MCPのグラフ診断を利用してください。保持中の全バッファは
ディスクと索引に一致する必要があり、結果に関わるソースのチェックサムを範囲の構築前後に
検証します。要求ごとのワークスペース全体のファイル鮮度走査は行いません。
90 changes: 90 additions & 0 deletions src/CodeIndex/Database/DbReader.CallHierarchy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System.Globalization;

namespace CodeIndex.Database;

internal sealed record IndexedCallSite(
long? SourceId, long? TargetId, string Path, int Line, int Column, int Length,
string Name, string? ResolutionState);

public partial class DbReader
{
// Position and endpoint lookups return metadata only. Reconstructing overlapping
// definition excerpts would bypass the LSP request's bounded source-file cache.
internal List<SymbolResult> GetCallHierarchyDeclarations(string path, int line, int limit) =>
GetSymbolsAtLine(path, line, limit, kind: null, lang: null);

internal SymbolResult? GetCallHierarchySymbol(long symbolId)
{
using var command = _conn.CreateCommand();
command.CommandText = $"""
SELECT f.path, f.lang, s.kind, s.name, s.line,
{GetSymbolColumnSql("start_line", "s.line")},
{GetSymbolColumnSql("end_line", "s.line")},
{GetSymbolColumnSql("body_start_line")},
{GetSymbolColumnSql("body_end_line")},
{GetSymbolColumnSql("signature")},
{GetSymbolColumnSql("container_kind")},
{GetSymbolColumnSql("container_name")},
{GetSymbolColumnSql("visibility")},
{GetSymbolColumnSql("return_type")}, s.id,
{GetSymbolColumnSql("container_qualified_name")},
{GetSymbolColumnSql("sub_kind")},
{GetSymbolColumnSql("start_column")}
FROM symbols s
JOIN files f ON f.id = s.file_id
WHERE s.id = @symbol
LIMIT 1
""";
SqliteCommandPolicy.Add(command, "@symbol", symbolId);
using var rows = command.ExecuteTrackedReader();
return rows.TrackedRead() ? ReadSymbolResult(rows) : null;
}

// LSP needs individual sites and both endpoints, rather than the grouped CLI rows.
// Use the same persisted reference identities and call kind; never bind by name.
internal List<IndexedCallSite> GetCallHierarchySites(long symbolId, bool incoming, int limit)
{
using var command = _conn.CreateCommand();
var identityFilter = incoming
? """
(r.target_symbol_id = @symbol OR EXISTS (
SELECT 1 FROM symbol_reference_candidates c
WHERE c.reference_id = r.id AND c.symbol_id = @symbol))
"""
: "r.source_symbol_id = @symbol";
command.CommandText = $"""
SELECT r.source_symbol_id, r.target_symbol_id, f.path, r.line,
r.column_number, r.span_length, r.symbol_name, r.resolution_state
FROM symbol_references r
JOIN files f ON f.id = r.file_id
WHERE {identityFilter} AND r.reference_kind = 'call'
ORDER BY r.id
LIMIT @limit
""";
SqliteCommandPolicy.Add(command, "@symbol", symbolId);
SqliteCommandPolicy.Add(command, "@limit", limit);
var sites = new List<IndexedCallSite>();
using var rows = command.ExecuteTrackedReader();
while (rows.TrackedRead())
{
Cancellation.ThrowIfCancellationRequested();
sites.Add(new IndexedCallSite(
rows.IsDBNull(0) ? null : rows.GetInt64(0),
rows.IsDBNull(1) ? null : rows.GetInt64(1),
rows.GetString(2), rows.GetInt32(3), rows.GetInt32(4),
rows.IsDBNull(5) ? 0 : rows.GetInt32(5), rows.GetString(6),
rows.IsDBNull(7) ? null : rows.GetString(7)));
}
return sites;
}

internal bool CallHierarchyIdentityAvailable =>
!_indexNewerThanReader && HasCurrentReferenceIdentityContractForRead()
&& _referenceColumns.Contains("source_symbol_id")
&& _referenceColumns.Contains("span_length")
&& HasTable("symbol_reference_candidates");

internal string GetCallHierarchyGeneration() => string.Create(
CultureInfo.InvariantCulture,
$"{GetSymbolSelectorGenerationIdentity()}\n{ExecuteScalar("PRAGMA data_version")}\n{ExecuteScalar("SELECT total_changes()")}");
}
8 changes: 8 additions & 0 deletions src/CodeIndex/Lsp/LspLiveDocumentStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ internal LspLiveDocumentStore(StringComparer comparer, StringComparison keyCompa

internal long EvictedBytes => _evictedBytes;

internal IEnumerable<KeyValuePair<string, string>> Documents => _documents;

// Once text has been discarded, indexed graph navigation cannot prove that all
// open buffers agree with the index. Reconnect after saving to recover.
internal bool HasDiscardedText { get; private set; }

internal int VersionTombstoneCount =>
_documentVersions.Keys.Count(key => !_documents.ContainsKey(key));

Expand All @@ -49,6 +55,7 @@ internal bool SetText(string key, string text, int? version = null)
var textBytes = Encoding.UTF8.GetByteCount(text);
if (textBytes > _maxDocumentBytes || textBytes > _maxLiveBytes)
{
HasDiscardedText = true;
RememberVersion(key, version);
Remove(key, preserveVersion: true);
TrimVersionTombstones();
Expand Down Expand Up @@ -78,6 +85,7 @@ internal void Remove(string key, bool recordEviction = false, bool preserveVersi
_documentBytes = Math.Max(0, _documentBytes - bytes);
if (recordEviction)
{
HasDiscardedText = true;
_evictionCount++;
_evictedBytes += bytes;
}
Expand Down
Loading
Loading