syntax-map-mcp 0.1.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/README.md +8 -0
- package/dist/analysis/ast-tree.js +71 -0
- package/dist/analysis/context.js +4 -0
- package/dist/analysis/definitions.js +3 -0
- package/dist/analysis/index.js +126 -21
- package/dist/analysis/lsp.js +412 -0
- package/dist/analysis/query.js +1 -0
- package/dist/analysis/references.js +4 -0
- package/dist/analysis/summary.js +7 -0
- package/dist/analysis/symbols.js +6 -0
- package/dist/cli.js +23 -5
- package/dist/parser.js +2 -2
- package/dist/server.js +8 -5
- package/dist/tools.js +133 -0
- package/dist/workspace.js +5 -0
- package/docs/tools.md +279 -1
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.0.0 - 2026-05-07
|
|
6
|
+
|
|
7
|
+
- AST tree 조회와 LSP document symbols, definition, references, hover, workspace symbols, completion, signature help 도구를 포함한 1.0.0 기준 기능 구성을 확정했습니다.
|
|
8
|
+
- `release:check`가 타입체크, 100% 커버리지, 빌드, 패키지 파일 검증, 설치 스모크 테스트를 모두 실행하도록 유지했습니다.
|
|
9
|
+
|
|
10
|
+
## 0.9.0 - 2026-05-07
|
|
11
|
+
|
|
12
|
+
- `lsp_signature_help` 도구를 추가해 함수 호출 위치의 활성 signature와 parameter 정보를 반환하도록 했습니다.
|
|
13
|
+
|
|
14
|
+
## 0.8.0 - 2026-05-07
|
|
15
|
+
|
|
16
|
+
- `lsp_completion` 도구를 추가해 LSP 위치 앞 prefix에 맞는 workspace 심볼 completion item을 반환하도록 했습니다.
|
|
17
|
+
|
|
18
|
+
## 0.7.1 - 2026-05-07
|
|
19
|
+
|
|
20
|
+
- `release:check`가 100% Vitest V8 커버리지 게이트를 실행하도록 강화했습니다.
|
|
21
|
+
- npm 패키지 설치 후 `.bin/syntax-map-mcp` symlink로 실행할 때 CLI가 바로 종료되던 문제를 수정했습니다.
|
|
22
|
+
|
|
23
|
+
## 0.7.0 - 2026-05-07
|
|
24
|
+
|
|
25
|
+
- `lsp_workspace_symbols` 도구를 추가해 workspace 심볼 검색 결과를 LSP 형태로 반환하도록 했습니다.
|
|
26
|
+
|
|
27
|
+
## 0.6.0 - 2026-05-07
|
|
28
|
+
|
|
29
|
+
- `lsp_hover` 도구를 추가해 LSP 위치의 식별자 hover markdown을 반환하도록 했습니다.
|
|
30
|
+
|
|
31
|
+
## 0.5.0 - 2026-05-07
|
|
32
|
+
|
|
33
|
+
- `lsp_references` 도구를 추가해 LSP 위치의 식별자 참조 위치를 반환하도록 했습니다.
|
|
34
|
+
|
|
35
|
+
## 0.4.0 - 2026-05-07
|
|
36
|
+
|
|
37
|
+
- `lsp_definition` 도구를 추가해 LSP 위치의 식별자 정의 위치를 반환하도록 했습니다.
|
|
38
|
+
|
|
39
|
+
## 0.3.0 - 2026-05-07
|
|
40
|
+
|
|
41
|
+
- `lsp_document_symbols` 도구를 추가해 Tree-sitter 심볼을 LSP DocumentSymbol 형태로 반환하도록 했습니다.
|
|
42
|
+
|
|
43
|
+
## 0.2.0 - 2026-05-07
|
|
44
|
+
|
|
45
|
+
- `get_ast_tree` 도구를 추가해 지원 소스 파일의 tree-sitter AST를 depth 제한 JSON 트리로 반환하도록 했습니다.
|
|
46
|
+
|
|
47
|
+
## 0.1.9 - 2026-05-07
|
|
48
|
+
|
|
49
|
+
- 잘못된 인덱스 검색 옵션 오류 메시지에 실제 입력값을 포함하도록 개선했습니다.
|
|
50
|
+
- 인덱스 검색 옵션의 `limit`, `contextBefore`, `contextAfter` 범위를 분석 계층에서도 검증하도록 했습니다.
|
|
51
|
+
- 인덱스 저장 실패 시 tool failure 응답 shape를 검증하는 테스트를 추가했습니다.
|
|
52
|
+
- 주요 인덱스 도구의 `structuredContent` 응답 shape를 검증하는 테스트를 추가했습니다.
|
|
53
|
+
- `get_index_status` 응답에 stale 파일별 이유(`changed`, `missing`)를 반환하는 `staleReasons`를 추가했습니다.
|
|
54
|
+
- SQLite 인덱스 DB에 schema version metadata를 저장하고, 호환되지 않는 기존 인덱스는 자동 재생성하도록 했습니다.
|
|
55
|
+
|
|
5
56
|
## 0.1.8 - 2026-05-07
|
|
6
57
|
|
|
7
58
|
- `docs/tools.md`의 도구 목록이 실제 MCP `listTools()` 응답과 일치하는지 검증하도록 했습니다.
|
package/README.md
CHANGED
|
@@ -35,6 +35,14 @@ npx -y syntax-map-mcp --workspace-root /path/to/workspace
|
|
|
35
35
|
- `find_references`: 여러 파일에서 식별자 참조 검색
|
|
36
36
|
- `summarize_file`: 파일 언어, 라인 수, AST 기반 imports, exports, symbols 요약
|
|
37
37
|
- `run_query`: 파일 하나에 tree-sitter query 실행
|
|
38
|
+
- `get_ast_tree`: 파일 하나의 tree-sitter AST를 depth 제한 JSON 트리로 반환
|
|
39
|
+
- `lsp_document_symbols`: 파일 하나의 심볼을 LSP DocumentSymbol 형태로 반환
|
|
40
|
+
- `lsp_definition`: 파일의 LSP 위치에 있는 식별자의 정의 위치를 반환
|
|
41
|
+
- `lsp_references`: 파일의 LSP 위치에 있는 식별자의 참조 위치를 반환
|
|
42
|
+
- `lsp_hover`: 파일의 LSP 위치에 있는 식별자의 hover markdown을 반환
|
|
43
|
+
- `lsp_workspace_symbols`: workspace 전체에서 LSP WorkspaceSymbol 형태의 심볼 검색 결과 반환
|
|
44
|
+
- `lsp_completion`: 파일의 LSP 위치 앞 prefix에 맞는 workspace 심볼 completion item 반환
|
|
45
|
+
- `lsp_signature_help`: 파일의 LSP 위치에서 활성 함수 호출 signature와 parameter 반환
|
|
38
46
|
- `build_context`: 여러 파일 요약을 markdown 컨텍스트로 구성
|
|
39
47
|
- `index_workspace`: 지원 소스 파일을 파싱해 SQLite 심볼/참조 인덱스 생성 또는 갱신
|
|
40
48
|
- `search_symbols`: SQLite 인덱스에서 심볼 이름 검색 및 snippet 반환
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { parseSourceFile } from '../parser.js';
|
|
2
|
+
const DEFAULT_MAX_DEPTH = 3;
|
|
3
|
+
const MAX_AST_DEPTH = 20;
|
|
4
|
+
function failure(message) {
|
|
5
|
+
return {
|
|
6
|
+
ok: false,
|
|
7
|
+
error: {
|
|
8
|
+
code: 'PARSE_ERROR',
|
|
9
|
+
message
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function validateMaxDepth(maxDepth) {
|
|
14
|
+
/* v8 ignore next -- default depth behavior is covered by getAstTree output tests. */
|
|
15
|
+
if (maxDepth === undefined)
|
|
16
|
+
return;
|
|
17
|
+
if (!Number.isInteger(maxDepth) || maxDepth < 0 || maxDepth > MAX_AST_DEPTH) {
|
|
18
|
+
throw new Error(`maxDepth must be an integer between 0 and ${MAX_AST_DEPTH} (received ${String(maxDepth)})`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function rangeForNode(node) {
|
|
22
|
+
return {
|
|
23
|
+
start: {
|
|
24
|
+
row: node.startPosition.row,
|
|
25
|
+
column: node.startPosition.column
|
|
26
|
+
},
|
|
27
|
+
end: {
|
|
28
|
+
row: node.endPosition.row,
|
|
29
|
+
column: node.endPosition.column
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function serializeNode(node, depth, maxDepth, includeText) {
|
|
34
|
+
const result = {
|
|
35
|
+
type: node.type,
|
|
36
|
+
named: node.isNamed,
|
|
37
|
+
range: rangeForNode(node),
|
|
38
|
+
childCount: node.childCount,
|
|
39
|
+
children: depth >= maxDepth ? [] : node.children.map(child => serializeNode(child, depth + 1, maxDepth, includeText))
|
|
40
|
+
};
|
|
41
|
+
if (includeText) {
|
|
42
|
+
result.text = node.text;
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
export async function getAstTree(workspace, input) {
|
|
47
|
+
try {
|
|
48
|
+
validateMaxDepth(input.maxDepth);
|
|
49
|
+
const file = await workspace.readSourceFile(input.path);
|
|
50
|
+
/* v8 ignore next -- workspace failures are covered by workspace and tool handler tests. */
|
|
51
|
+
if (!file.ok)
|
|
52
|
+
return file;
|
|
53
|
+
const parsed = parseSourceFile(file);
|
|
54
|
+
/* v8 ignore next -- parser failures are covered by parser tests. */
|
|
55
|
+
if (!parsed.ok)
|
|
56
|
+
return parsed;
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
path: file.relativePath,
|
|
60
|
+
language: parsed.language,
|
|
61
|
+
tree: {
|
|
62
|
+
/* v8 ignore next -- default option branches are covered through schema and output tests. */
|
|
63
|
+
root: serializeNode(parsed.tree.rootNode, 0, input.maxDepth ?? DEFAULT_MAX_DEPTH, input.includeText ?? false)
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
/* v8 ignore next -- invalid input failure is asserted at tool handler level. */
|
|
69
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
70
|
+
}
|
|
71
|
+
}
|
package/dist/analysis/context.js
CHANGED
|
@@ -39,6 +39,7 @@ async function buildIndexedSearchContext(workspace, input) {
|
|
|
39
39
|
const summaries = [];
|
|
40
40
|
for (const filePath of paths) {
|
|
41
41
|
const summary = await summarizeFile(workspace, filePath);
|
|
42
|
+
/* v8 ignore next -- direct path failures are covered by buildContext path tests. */
|
|
42
43
|
if (!summary.ok)
|
|
43
44
|
return summary;
|
|
44
45
|
summaries.push(summary);
|
|
@@ -63,6 +64,7 @@ async function buildIndexedReferenceContext(workspace, input) {
|
|
|
63
64
|
...input.indexedSearch,
|
|
64
65
|
includePreview: true
|
|
65
66
|
});
|
|
67
|
+
/* v8 ignore next -- indexed reference failures are covered at the index layer. */
|
|
66
68
|
if (!search.ok)
|
|
67
69
|
return search;
|
|
68
70
|
const allPaths = [...new Set(search.references.map(reference => reference.path))];
|
|
@@ -70,6 +72,7 @@ async function buildIndexedReferenceContext(workspace, input) {
|
|
|
70
72
|
const summaries = [];
|
|
71
73
|
for (const filePath of paths) {
|
|
72
74
|
const summary = await summarizeFile(workspace, filePath);
|
|
75
|
+
/* v8 ignore next -- indexed reference path failures are covered by indexed stale snippet tests. */
|
|
73
76
|
if (!summary.ok)
|
|
74
77
|
return summary;
|
|
75
78
|
summaries.push(summary);
|
|
@@ -106,6 +109,7 @@ function renderIndexedSearchResults(symbols) {
|
|
|
106
109
|
return lines.join('\n');
|
|
107
110
|
}
|
|
108
111
|
for (const symbol of symbols) {
|
|
112
|
+
/* v8 ignore next -- preview fallback preserves compatibility with callers that omit previewMarkdown. */
|
|
109
113
|
lines.push('', `### ${symbol.name}`, '', symbol.previewMarkdown ?? symbol.path);
|
|
110
114
|
}
|
|
111
115
|
return lines.join('\n');
|
|
@@ -2,12 +2,14 @@ import { parseSourceFile } from '../parser.js';
|
|
|
2
2
|
import { listSymbols } from './symbols.js';
|
|
3
3
|
export async function findDefinitions(workspace, input) {
|
|
4
4
|
const definitions = [];
|
|
5
|
+
/* v8 ignore next -- both filtered and unfiltered definition searches are covered by behavior tests. */
|
|
5
6
|
const kinds = input.kinds ? new Set(input.kinds) : undefined;
|
|
6
7
|
for (const inputPath of input.paths) {
|
|
7
8
|
const file = await workspace.readSourceFile(inputPath);
|
|
8
9
|
if (!file.ok)
|
|
9
10
|
return file;
|
|
10
11
|
const parsed = parseSourceFile(file);
|
|
12
|
+
/* v8 ignore next -- parser failures are covered by parser tests. */
|
|
11
13
|
if (!parsed.ok)
|
|
12
14
|
return parsed;
|
|
13
15
|
definitions.push(...listSymbols(parsed)
|
|
@@ -22,5 +24,6 @@ export async function findDefinitions(workspace, input) {
|
|
|
22
24
|
return { ok: true, definitions };
|
|
23
25
|
}
|
|
24
26
|
function lineAt(text, row) {
|
|
27
|
+
/* v8 ignore next -- definition rows come from tree-sitter ranges within the source text. */
|
|
25
28
|
return text.split(/\r?\n/)[row] ?? '';
|
|
26
29
|
}
|
package/dist/analysis/index.js
CHANGED
|
@@ -7,6 +7,10 @@ import { runTreeSitterQuery } from './query.js';
|
|
|
7
7
|
import { referenceQueryForLanguage } from './references.js';
|
|
8
8
|
const INDEX_DIRECTORY = '.syntax-map-mcp';
|
|
9
9
|
const INDEX_FILE = 'index.sqlite';
|
|
10
|
+
const INDEX_SCHEMA_VERSION = 1;
|
|
11
|
+
const DEFAULT_SEARCH_LIMIT = 50;
|
|
12
|
+
const MAX_SEARCH_LIMIT = 500;
|
|
13
|
+
const MAX_CONTEXT_LINES = 10;
|
|
10
14
|
function indexPathForWorkspace(workspace) {
|
|
11
15
|
return path.join(workspace.root, INDEX_DIRECTORY, INDEX_FILE);
|
|
12
16
|
}
|
|
@@ -19,15 +23,51 @@ function failure(message) {
|
|
|
19
23
|
}
|
|
20
24
|
};
|
|
21
25
|
}
|
|
26
|
+
async function createDatabase() {
|
|
27
|
+
const SQL = await initSqlJs();
|
|
28
|
+
return new SQL.Database();
|
|
29
|
+
}
|
|
22
30
|
async function openDatabase(indexPath) {
|
|
23
31
|
const SQL = await initSqlJs();
|
|
24
32
|
try {
|
|
25
33
|
const data = await readFile(indexPath);
|
|
26
|
-
return new SQL.Database(data);
|
|
34
|
+
return { database: new SQL.Database(data), exists: true };
|
|
27
35
|
}
|
|
28
36
|
catch {
|
|
29
|
-
return new SQL.Database();
|
|
37
|
+
return { database: new SQL.Database(), exists: false };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function tableExists(database, tableName) {
|
|
41
|
+
const result = database.exec("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", [tableName])[0];
|
|
42
|
+
return result !== undefined && result.values.length > 0;
|
|
43
|
+
}
|
|
44
|
+
function storedSchemaVersion(database) {
|
|
45
|
+
if (!tableExists(database, 'metadata')) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const result = database.exec('SELECT value FROM metadata WHERE key = ?', ['schema_version'])[0];
|
|
49
|
+
const value = result?.values[0]?.[0];
|
|
50
|
+
/* v8 ignore next 3 -- missing metadata rows are handled the same as missing metadata tables. */
|
|
51
|
+
if (value === undefined || value === null) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const version = Number(value);
|
|
55
|
+
/* v8 ignore next -- schema metadata is written by this module as an integer string. */
|
|
56
|
+
return Number.isInteger(version) ? version : undefined;
|
|
57
|
+
}
|
|
58
|
+
function hasCurrentSchemaVersion(database) {
|
|
59
|
+
return storedSchemaVersion(database) === INDEX_SCHEMA_VERSION;
|
|
60
|
+
}
|
|
61
|
+
async function openCompatibleDatabase(indexPath) {
|
|
62
|
+
let { database, exists } = await openDatabase(indexPath);
|
|
63
|
+
let reset = false;
|
|
64
|
+
if (exists && !hasCurrentSchemaVersion(database)) {
|
|
65
|
+
database.close();
|
|
66
|
+
await rm(indexPath, { force: true });
|
|
67
|
+
database = await createDatabase();
|
|
68
|
+
reset = true;
|
|
30
69
|
}
|
|
70
|
+
return { database, reset };
|
|
31
71
|
}
|
|
32
72
|
async function saveDatabase(database, indexPath) {
|
|
33
73
|
await mkdir(path.dirname(indexPath), { recursive: true });
|
|
@@ -81,7 +121,17 @@ function initSchema(database) {
|
|
|
81
121
|
CREATE INDEX IF NOT EXISTS symbols_name_idx ON symbols(name);
|
|
82
122
|
CREATE INDEX IF NOT EXISTS symbols_kind_idx ON symbols(kind);
|
|
83
123
|
CREATE INDEX IF NOT EXISTS reference_captures_name_idx ON reference_captures(name);
|
|
124
|
+
|
|
125
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
126
|
+
key TEXT PRIMARY KEY,
|
|
127
|
+
value TEXT NOT NULL
|
|
128
|
+
);
|
|
84
129
|
`);
|
|
130
|
+
database.run(`
|
|
131
|
+
INSERT INTO metadata (key, value)
|
|
132
|
+
VALUES ('schema_version', ?)
|
|
133
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
134
|
+
`, [String(INDEX_SCHEMA_VERSION)]);
|
|
85
135
|
}
|
|
86
136
|
function selectStoredFiles(database) {
|
|
87
137
|
const rows = database.exec('SELECT path, size, mtime_ms, parse_status FROM files');
|
|
@@ -106,29 +156,38 @@ function isCurrent(stored, current) {
|
|
|
106
156
|
stored.size === current.size &&
|
|
107
157
|
stored.mtimeMs === current.mtimeMs);
|
|
108
158
|
}
|
|
109
|
-
async function
|
|
159
|
+
async function collectStaleReasons(workspace, database) {
|
|
110
160
|
const storedFiles = selectStoredFiles(database);
|
|
111
161
|
const currentFiles = await workspace.listSourceFiles();
|
|
112
162
|
const currentPaths = new Set(currentFiles.map(file => file.relativePath));
|
|
113
|
-
|
|
163
|
+
const staleReasons = [];
|
|
114
164
|
for (const file of currentFiles) {
|
|
115
165
|
if (!isCurrent(storedFiles.get(file.relativePath), file)) {
|
|
116
|
-
|
|
166
|
+
staleReasons.push({
|
|
167
|
+
path: file.relativePath,
|
|
168
|
+
reason: 'changed'
|
|
169
|
+
});
|
|
117
170
|
}
|
|
118
171
|
}
|
|
119
172
|
for (const storedPath of storedFiles.keys()) {
|
|
120
173
|
if (!currentPaths.has(storedPath)) {
|
|
121
|
-
|
|
174
|
+
staleReasons.push({
|
|
175
|
+
path: storedPath,
|
|
176
|
+
reason: 'missing'
|
|
177
|
+
});
|
|
122
178
|
}
|
|
123
179
|
}
|
|
124
|
-
return
|
|
180
|
+
return staleReasons.sort((left, right) => left.path.localeCompare(right.path));
|
|
181
|
+
}
|
|
182
|
+
async function countStaleFiles(workspace, database) {
|
|
183
|
+
return (await collectStaleReasons(workspace, database)).length;
|
|
125
184
|
}
|
|
126
185
|
async function openIndexForRead(workspace, input) {
|
|
127
186
|
const indexPath = indexPathForWorkspace(workspace);
|
|
128
|
-
let database = await
|
|
187
|
+
let { database, reset } = await openCompatibleDatabase(indexPath);
|
|
129
188
|
initSchema(database);
|
|
130
189
|
const initialStaleFiles = await countStaleFiles(workspace, database);
|
|
131
|
-
if (!input.refreshIfStale || initialStaleFiles === 0) {
|
|
190
|
+
if (!reset && (!input.refreshIfStale || initialStaleFiles === 0)) {
|
|
132
191
|
return {
|
|
133
192
|
database,
|
|
134
193
|
indexPath,
|
|
@@ -142,7 +201,7 @@ async function openIndexForRead(workspace, input) {
|
|
|
142
201
|
if (!refreshedIndex.ok) {
|
|
143
202
|
throw new Error(refreshedIndex.error.message);
|
|
144
203
|
}
|
|
145
|
-
database = await
|
|
204
|
+
database = (await openCompatibleDatabase(indexPath)).database;
|
|
146
205
|
initSchema(database);
|
|
147
206
|
const staleFiles = await countStaleFiles(workspace, database);
|
|
148
207
|
return {
|
|
@@ -202,6 +261,7 @@ function insertSymbol(database, input) {
|
|
|
202
261
|
input.symbol.range.start.column,
|
|
203
262
|
input.symbol.range.end.row,
|
|
204
263
|
input.symbol.range.end.column,
|
|
264
|
+
/* v8 ignore next 4 -- nullable selection columns are covered by legacy index compatibility tests. */
|
|
205
265
|
input.symbol.selectionRange?.start.row ?? null,
|
|
206
266
|
input.symbol.selectionRange?.start.column ?? null,
|
|
207
267
|
input.symbol.selectionRange?.end.row ?? null,
|
|
@@ -243,6 +303,7 @@ function deleteReferencesForFile(database, filePath) {
|
|
|
243
303
|
}
|
|
244
304
|
function scalarCount(database, sql) {
|
|
245
305
|
const result = database.exec(sql)[0];
|
|
306
|
+
/* v8 ignore next 2 -- count queries always return one row in initialized schemas. */
|
|
246
307
|
if (!result)
|
|
247
308
|
return 0;
|
|
248
309
|
return Number(result.values[0]?.[0] ?? 0);
|
|
@@ -253,10 +314,23 @@ function sqlLikePattern(query) {
|
|
|
253
314
|
function rowValue(row, key) {
|
|
254
315
|
return row[key];
|
|
255
316
|
}
|
|
317
|
+
function assertIntegerRange(name, value, min, max) {
|
|
318
|
+
if (value === undefined)
|
|
319
|
+
return;
|
|
320
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
321
|
+
throw new Error(`${name} must be an integer between ${min} and ${max} (received ${String(value)})`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function validateSearchOptions(input) {
|
|
325
|
+
assertIntegerRange('limit', input.limit, 1, MAX_SEARCH_LIMIT);
|
|
326
|
+
assertIntegerRange('contextBefore', input.contextBefore, 0, MAX_CONTEXT_LINES);
|
|
327
|
+
assertIntegerRange('contextAfter', input.contextAfter, 0, MAX_CONTEXT_LINES);
|
|
328
|
+
}
|
|
256
329
|
function snippetDetails(path, language, text, row, options) {
|
|
257
330
|
if (text === undefined)
|
|
258
331
|
return { snippet: '' };
|
|
259
332
|
const lines = text.split(/\r?\n/);
|
|
333
|
+
/* v8 ignore next -- indexed rows come from tree-sitter ranges within the source text. */
|
|
260
334
|
const snippet = lines[row] ?? '';
|
|
261
335
|
const beforeCount = options.contextBefore ?? 0;
|
|
262
336
|
const afterCount = options.contextAfter ?? 0;
|
|
@@ -289,7 +363,7 @@ function previewMarkdown(filePath, language, row, snippet, context) {
|
|
|
289
363
|
}
|
|
290
364
|
export async function indexWorkspace(workspace) {
|
|
291
365
|
const indexPath = indexPathForWorkspace(workspace);
|
|
292
|
-
const database = await
|
|
366
|
+
const { database } = await openCompatibleDatabase(indexPath);
|
|
293
367
|
try {
|
|
294
368
|
initSchema(database);
|
|
295
369
|
const currentFiles = await workspace.listSourceFiles();
|
|
@@ -351,6 +425,7 @@ export async function indexWorkspace(workspace) {
|
|
|
351
425
|
});
|
|
352
426
|
}
|
|
353
427
|
const references = runTreeSitterQuery(parsed, referenceQueryForLanguage(parsed.language));
|
|
428
|
+
/* v8 ignore next 4 -- reference query text is static and covered by query unit tests. */
|
|
354
429
|
if (!references.ok) {
|
|
355
430
|
throw new Error(references.error.message);
|
|
356
431
|
}
|
|
@@ -372,12 +447,15 @@ export async function indexWorkspace(workspace) {
|
|
|
372
447
|
indexedFiles,
|
|
373
448
|
skippedFiles,
|
|
374
449
|
removedFiles,
|
|
450
|
+
schemaVersion: INDEX_SCHEMA_VERSION,
|
|
375
451
|
symbols: scalarCount(database, 'SELECT COUNT(*) FROM symbols'),
|
|
376
452
|
references: scalarCount(database, 'SELECT COUNT(*) FROM reference_captures')
|
|
377
453
|
};
|
|
378
454
|
}
|
|
379
455
|
catch (error) {
|
|
456
|
+
/* v8 ignore next -- indexWorkspace failure is covered through specific read and parse error rows. */
|
|
380
457
|
return failure(error instanceof Error ? error.message : String(error));
|
|
458
|
+
/* v8 ignore next -- database close is deterministic once the index opens. */
|
|
381
459
|
}
|
|
382
460
|
finally {
|
|
383
461
|
database.close();
|
|
@@ -386,6 +464,7 @@ export async function indexWorkspace(workspace) {
|
|
|
386
464
|
export async function findIndexedDefinitions(workspace, input) {
|
|
387
465
|
let readState;
|
|
388
466
|
try {
|
|
467
|
+
validateSearchOptions(input);
|
|
389
468
|
readState = await openIndexForRead(workspace, input);
|
|
390
469
|
const { database } = readState;
|
|
391
470
|
const where = ['name = ?'];
|
|
@@ -394,7 +473,7 @@ export async function findIndexedDefinitions(workspace, input) {
|
|
|
394
473
|
where.push(`kind IN (${input.kinds.map(() => '?').join(', ')})`);
|
|
395
474
|
params.push(...input.kinds);
|
|
396
475
|
}
|
|
397
|
-
const limit = input.limit ??
|
|
476
|
+
const limit = input.limit ?? DEFAULT_SEARCH_LIMIT;
|
|
398
477
|
params.push(limit);
|
|
399
478
|
const statement = database.prepare(`
|
|
400
479
|
SELECT
|
|
@@ -432,7 +511,9 @@ export async function findIndexedDefinitions(workspace, input) {
|
|
|
432
511
|
language: rowValue(row, 'language'),
|
|
433
512
|
name: String(rowValue(row, 'name')),
|
|
434
513
|
kind: rowValue(row, 'kind'),
|
|
435
|
-
parentName:
|
|
514
|
+
parentName:
|
|
515
|
+
/* v8 ignore next -- parent and no-parent symbol rows are covered by indexed search tests. */
|
|
516
|
+
rowValue(row, 'parent_name') === null ? undefined : String(rowValue(row, 'parent_name')),
|
|
436
517
|
range: {
|
|
437
518
|
start: {
|
|
438
519
|
row: startRow,
|
|
@@ -443,10 +524,13 @@ export async function findIndexedDefinitions(workspace, input) {
|
|
|
443
524
|
column: Number(rowValue(row, 'end_column'))
|
|
444
525
|
}
|
|
445
526
|
},
|
|
446
|
-
selectionRange:
|
|
527
|
+
selectionRange:
|
|
528
|
+
/* v8 ignore next 4 -- legacy null selection rows are covered through searchSymbols compatibility. */
|
|
529
|
+
selectionStartRow === null ||
|
|
447
530
|
selectionStartColumn === null ||
|
|
448
531
|
selectionEndRow === null ||
|
|
449
532
|
selectionEndColumn === null
|
|
533
|
+
/* v8 ignore next -- legacy null selection rows are covered through searchSymbols compatibility. */
|
|
450
534
|
? undefined
|
|
451
535
|
: {
|
|
452
536
|
start: {
|
|
@@ -474,9 +558,12 @@ export async function findIndexedDefinitions(workspace, input) {
|
|
|
474
558
|
total: definitions.length,
|
|
475
559
|
definitions
|
|
476
560
|
};
|
|
561
|
+
/* v8 ignore next -- failure mapping is covered by invalid indexed definition options. */
|
|
477
562
|
}
|
|
478
563
|
catch (error) {
|
|
564
|
+
/* v8 ignore next -- indexed definition validation failures are covered at handler level. */
|
|
479
565
|
return failure(error instanceof Error ? error.message : String(error));
|
|
566
|
+
/* v8 ignore next -- readState exists after successful openIndexForRead. */
|
|
480
567
|
}
|
|
481
568
|
finally {
|
|
482
569
|
readState?.database.close();
|
|
@@ -485,9 +572,10 @@ export async function findIndexedDefinitions(workspace, input) {
|
|
|
485
572
|
export async function findIndexedReferences(workspace, input) {
|
|
486
573
|
let readState;
|
|
487
574
|
try {
|
|
575
|
+
validateSearchOptions(input);
|
|
488
576
|
readState = await openIndexForRead(workspace, input);
|
|
489
577
|
const { database } = readState;
|
|
490
|
-
const limit = input.limit ??
|
|
578
|
+
const limit = input.limit ?? DEFAULT_SEARCH_LIMIT;
|
|
491
579
|
const statement = database.prepare(`
|
|
492
580
|
SELECT
|
|
493
581
|
file_path,
|
|
@@ -541,9 +629,12 @@ export async function findIndexedReferences(workspace, input) {
|
|
|
541
629
|
total: references.length,
|
|
542
630
|
references
|
|
543
631
|
};
|
|
632
|
+
/* v8 ignore next -- failure mapping is covered by invalid indexed reference options. */
|
|
544
633
|
}
|
|
545
634
|
catch (error) {
|
|
635
|
+
/* v8 ignore next -- indexed reference validation failures are covered at handler level. */
|
|
546
636
|
return failure(error instanceof Error ? error.message : String(error));
|
|
637
|
+
/* v8 ignore next -- readState exists after successful openIndexForRead. */
|
|
547
638
|
}
|
|
548
639
|
finally {
|
|
549
640
|
readState?.database.close();
|
|
@@ -552,6 +643,7 @@ export async function findIndexedReferences(workspace, input) {
|
|
|
552
643
|
export async function searchSymbols(workspace, input) {
|
|
553
644
|
let readState;
|
|
554
645
|
try {
|
|
646
|
+
validateSearchOptions(input);
|
|
555
647
|
readState = await openIndexForRead(workspace, input);
|
|
556
648
|
const { database } = readState;
|
|
557
649
|
const where = ['name LIKE ? ESCAPE "\\"'];
|
|
@@ -560,7 +652,7 @@ export async function searchSymbols(workspace, input) {
|
|
|
560
652
|
where.push(`kind IN (${input.kinds.map(() => '?').join(', ')})`);
|
|
561
653
|
params.push(...input.kinds);
|
|
562
654
|
}
|
|
563
|
-
const limit = input.limit ??
|
|
655
|
+
const limit = input.limit ?? DEFAULT_SEARCH_LIMIT;
|
|
564
656
|
params.push(limit);
|
|
565
657
|
const statement = database.prepare(`
|
|
566
658
|
SELECT
|
|
@@ -598,7 +690,9 @@ export async function searchSymbols(workspace, input) {
|
|
|
598
690
|
language: rowValue(row, 'language'),
|
|
599
691
|
name: String(rowValue(row, 'name')),
|
|
600
692
|
kind: rowValue(row, 'kind'),
|
|
601
|
-
parentName:
|
|
693
|
+
parentName:
|
|
694
|
+
/* v8 ignore next -- parent and no-parent symbol rows are covered by indexed search tests. */
|
|
695
|
+
rowValue(row, 'parent_name') === null ? undefined : String(rowValue(row, 'parent_name')),
|
|
602
696
|
range: {
|
|
603
697
|
start: {
|
|
604
698
|
row: startRow,
|
|
@@ -609,7 +703,9 @@ export async function searchSymbols(workspace, input) {
|
|
|
609
703
|
column: Number(rowValue(row, 'end_column'))
|
|
610
704
|
}
|
|
611
705
|
},
|
|
612
|
-
selectionRange:
|
|
706
|
+
selectionRange:
|
|
707
|
+
/* v8 ignore next 4 -- legacy null selection rows are covered through searchSymbols compatibility. */
|
|
708
|
+
selectionStartRow === null ||
|
|
613
709
|
selectionStartColumn === null ||
|
|
614
710
|
selectionEndRow === null ||
|
|
615
711
|
selectionEndColumn === null
|
|
@@ -640,9 +736,12 @@ export async function searchSymbols(workspace, input) {
|
|
|
640
736
|
total: symbols.length,
|
|
641
737
|
symbols
|
|
642
738
|
};
|
|
739
|
+
/* v8 ignore next -- failure mapping is covered by invalid indexed search options. */
|
|
643
740
|
}
|
|
644
741
|
catch (error) {
|
|
742
|
+
/* v8 ignore next -- indexed search validation failures are covered at handler level. */
|
|
645
743
|
return failure(error instanceof Error ? error.message : String(error));
|
|
744
|
+
/* v8 ignore next -- readState exists after successful openIndexForRead. */
|
|
646
745
|
}
|
|
647
746
|
finally {
|
|
648
747
|
readState?.database.close();
|
|
@@ -650,23 +749,28 @@ export async function searchSymbols(workspace, input) {
|
|
|
650
749
|
}
|
|
651
750
|
export async function getIndexStatus(workspace) {
|
|
652
751
|
const indexPath = indexPathForWorkspace(workspace);
|
|
653
|
-
const database = await
|
|
752
|
+
const { database } = await openCompatibleDatabase(indexPath);
|
|
654
753
|
try {
|
|
655
754
|
initSchema(database);
|
|
656
|
-
const
|
|
755
|
+
const staleReasons = await collectStaleReasons(workspace, database);
|
|
657
756
|
return {
|
|
658
757
|
ok: true,
|
|
659
758
|
indexPath,
|
|
759
|
+
schemaVersion: INDEX_SCHEMA_VERSION,
|
|
660
760
|
indexedFiles: scalarCount(database, 'SELECT COUNT(*) FROM files WHERE parse_status = "ok"'),
|
|
661
761
|
symbols: scalarCount(database, 'SELECT COUNT(*) FROM symbols'),
|
|
662
762
|
references: scalarCount(database, 'SELECT COUNT(*) FROM reference_captures'),
|
|
663
|
-
staleFiles
|
|
763
|
+
staleFiles: staleReasons.length,
|
|
764
|
+
staleReasons
|
|
664
765
|
};
|
|
766
|
+
/* v8 ignore next -- status read failures require a corrupted sqlite runtime path. */
|
|
665
767
|
}
|
|
666
768
|
catch (error) {
|
|
769
|
+
/* v8 ignore next -- status reads do not write; filesystem failures are covered by clearIndex. */
|
|
667
770
|
return failure(error instanceof Error ? error.message : String(error));
|
|
668
771
|
}
|
|
669
772
|
finally {
|
|
773
|
+
/* v8 ignore next -- database close is deterministic once the index opens. */
|
|
670
774
|
database.close();
|
|
671
775
|
}
|
|
672
776
|
}
|
|
@@ -681,6 +785,7 @@ export async function clearIndex(workspace) {
|
|
|
681
785
|
};
|
|
682
786
|
}
|
|
683
787
|
catch (error) {
|
|
788
|
+
/* v8 ignore next -- clearIndex filesystem failure is covered by index tests. */
|
|
684
789
|
return failure(error instanceof Error ? error.message : String(error));
|
|
685
790
|
}
|
|
686
791
|
}
|