@crouton-kit/tsym 0.1.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/LICENSE +21 -0
- package/README.md +51 -0
- package/bin/tsym.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +89 -0
- package/dist/commands/handlers/check.d.ts +3 -0
- package/dist/commands/handlers/check.js +94 -0
- package/dist/commands/handlers/find.d.ts +3 -0
- package/dist/commands/handlers/find.js +56 -0
- package/dist/commands/handlers/inspect.d.ts +3 -0
- package/dist/commands/handlers/inspect.js +81 -0
- package/dist/commands/handlers/outline.d.ts +3 -0
- package/dist/commands/handlers/outline.js +101 -0
- package/dist/commands/handlers/query.d.ts +3 -0
- package/dist/commands/handlers/query.js +196 -0
- package/dist/commands/handlers/rename.d.ts +3 -0
- package/dist/commands/handlers/rename.js +53 -0
- package/dist/commands/handlers/search.d.ts +5 -0
- package/dist/commands/handlers/search.js +32 -0
- package/dist/commands/handlers/server-reload.d.ts +3 -0
- package/dist/commands/handlers/server-reload.js +7 -0
- package/dist/commands/handlers/server-status.d.ts +3 -0
- package/dist/commands/handlers/server-status.js +7 -0
- package/dist/commands/handlers/server-stop.d.ts +3 -0
- package/dist/commands/handlers/server-stop.js +7 -0
- package/dist/commands/handlers/show.d.ts +3 -0
- package/dist/commands/handlers/show.js +63 -0
- package/dist/commands/handlers/type.d.ts +3 -0
- package/dist/commands/handlers/type.js +45 -0
- package/dist/commands/handlers.d.ts +12 -0
- package/dist/commands/handlers.js +44 -0
- package/dist/commands/help.d.ts +2 -0
- package/dist/commands/help.js +27 -0
- package/dist/commands/rename-apply.d.ts +37 -0
- package/dist/commands/rename-apply.js +115 -0
- package/dist/commands/tree.d.ts +9 -0
- package/dist/commands/tree.js +277 -0
- package/dist/commands/types.d.ts +80 -0
- package/dist/commands/types.js +1 -0
- package/dist/core/declarations.d.ts +48 -0
- package/dist/core/declarations.js +153 -0
- package/dist/core/receipt.d.ts +7 -0
- package/dist/core/receipt.js +9 -0
- package/dist/core/search.d.ts +29 -0
- package/dist/core/search.js +47 -0
- package/dist/core/source.d.ts +32 -0
- package/dist/core/source.js +96 -0
- package/dist/core/workspace.d.ts +28 -0
- package/dist/core/workspace.js +137 -0
- package/dist/index/build.d.ts +22 -0
- package/dist/index/build.js +79 -0
- package/dist/index/classify.d.ts +20 -0
- package/dist/index/classify.js +60 -0
- package/dist/index/commits.d.ts +15 -0
- package/dist/index/commits.js +201 -0
- package/dist/index/descriptor.d.ts +19 -0
- package/dist/index/descriptor.js +295 -0
- package/dist/index/graph.d.ts +82 -0
- package/dist/index/graph.js +1 -0
- package/dist/index/heritage.d.ts +15 -0
- package/dist/index/heritage.js +122 -0
- package/dist/index/refresh.d.ts +28 -0
- package/dist/index/refresh.js +110 -0
- package/dist/index/scope.d.ts +4 -0
- package/dist/index/scope.js +21 -0
- package/dist/index/state.d.ts +22 -0
- package/dist/index/state.js +63 -0
- package/dist/index/walk.d.ts +23 -0
- package/dist/index/walk.js +550 -0
- package/dist/index/watch.d.ts +18 -0
- package/dist/index/watch.js +175 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/output.d.ts +12 -0
- package/dist/output.js +54 -0
- package/dist/read/read.d.ts +32 -0
- package/dist/read/read.js +89 -0
- package/dist/relations/graph.d.ts +23 -0
- package/dist/relations/graph.js +115 -0
- package/dist/relations/mermaid.d.ts +3 -0
- package/dist/relations/mermaid.js +37 -0
- package/dist/relations/store-relations.d.ts +20 -0
- package/dist/relations/store-relations.js +158 -0
- package/dist/relations/types.d.ts +58 -0
- package/dist/relations/types.js +12 -0
- package/dist/server/client.d.ts +5 -0
- package/dist/server/client.js +215 -0
- package/dist/server/context.d.ts +21 -0
- package/dist/server/context.js +12 -0
- package/dist/server/lifecycle.d.ts +28 -0
- package/dist/server/lifecycle.js +62 -0
- package/dist/server/main.d.ts +1 -0
- package/dist/server/main.js +541 -0
- package/dist/server/projects.d.ts +21 -0
- package/dist/server/projects.js +28 -0
- package/dist/server/wire.d.ts +39 -0
- package/dist/server/wire.js +19 -0
- package/dist/store/load.d.ts +67 -0
- package/dist/store/load.js +193 -0
- package/dist/store/meta.d.ts +28 -0
- package/dist/store/meta.js +137 -0
- package/dist/store/query.d.ts +5 -0
- package/dist/store/query.js +4 -0
- package/dist/store/schema.d.ts +4 -0
- package/dist/store/schema.js +19 -0
- package/dist/store/store.d.ts +36 -0
- package/dist/store/store.js +240 -0
- package/dist/ts7/api.d.ts +35 -0
- package/dist/ts7/api.js +164 -0
- package/dist/ts7/ast.d.ts +1 -0
- package/dist/ts7/ast.js +1 -0
- package/dist/ts7/checker.d.ts +55 -0
- package/dist/ts7/checker.js +383 -0
- package/dist/ts7/projects.d.ts +27 -0
- package/dist/ts7/projects.js +94 -0
- package/package.json +47 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { writeFileSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { pathsForRoot } from '../../server/lifecycle.js';
|
|
6
|
+
import { ingestCommits } from '../../index/commits.js';
|
|
7
|
+
import { query } from '../../store/query.js';
|
|
8
|
+
const address = /^(.*):(\d+):(\d+)$/;
|
|
9
|
+
const handler = async (context, invocation) => {
|
|
10
|
+
const cypher = invocation.input.args[0] ?? '';
|
|
11
|
+
if (cypher.trim() === '')
|
|
12
|
+
return rejected('cypher', 'an empty query', 'one non-empty Cypher statement', 'Pass a Cypher statement as the positional argument or pipe one to `tsym query -`.');
|
|
13
|
+
const bound = parametersFor(invocation);
|
|
14
|
+
if ('error' in bound)
|
|
15
|
+
return bound.error;
|
|
16
|
+
const parameters = bound.parameters;
|
|
17
|
+
if (invocation.input.flags['--since'] !== undefined) {
|
|
18
|
+
const commits = commitsSince(context.root, invocation.input.flags['--since']);
|
|
19
|
+
if (!Array.isArray(commits))
|
|
20
|
+
return commits;
|
|
21
|
+
if (!context.store)
|
|
22
|
+
throw new Error('The query leaf requires the resident store and index state.');
|
|
23
|
+
ingestCommits(context.root, context.store, commits);
|
|
24
|
+
parameters.commits = commits;
|
|
25
|
+
}
|
|
26
|
+
const { store, index } = context;
|
|
27
|
+
if (!store || !index)
|
|
28
|
+
throw new Error('The query leaf requires the resident store and index state.');
|
|
29
|
+
const started = performance.now();
|
|
30
|
+
let records;
|
|
31
|
+
try {
|
|
32
|
+
records = query(store, cypher, parameters);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error instanceof Error && error.message === 'Interrupted.')
|
|
36
|
+
return { category: 'tool', code: 'query-timeout', message: error.message };
|
|
37
|
+
return rejected('cypher', error instanceof Error ? error.message : String(error), 'a Cypher statement accepted by LadybugDB', 'Correct the query using the engine message and retry.');
|
|
38
|
+
}
|
|
39
|
+
const ordered = hasTopLevelOrderBy(cypher) ? records : orderDefault(records);
|
|
40
|
+
const rows = ordered.map(renderRow);
|
|
41
|
+
const limit = invocation.input.flags['--rows'];
|
|
42
|
+
const shown = limit === 0 ? rows : rows.slice(0, limit);
|
|
43
|
+
const output = invocation.input.flags['--out'];
|
|
44
|
+
const complete = rows.join('\n');
|
|
45
|
+
const resultPath = output === undefined
|
|
46
|
+
? shown.length < rows.length ? path.join(pathsForRoot(context.root).results, `${randomUUID()}.txt`) : undefined
|
|
47
|
+
: path.resolve(invocation.cwd, output);
|
|
48
|
+
if (resultPath !== undefined)
|
|
49
|
+
writeFileSync(resultPath, complete === '' ? '' : `${complete}\n`, { mode: 0o600 });
|
|
50
|
+
const unordered = !hasTopLevelOrderBy(cypher) && ordered.some((record) => addressFor(record) === undefined);
|
|
51
|
+
const elapsed = Math.round(performance.now() - started);
|
|
52
|
+
return {
|
|
53
|
+
data: {
|
|
54
|
+
element: 'query',
|
|
55
|
+
attributes: { rows: rows.length, shown: shown.length, indexedAt: index.indexedAt, state: index.state, ms: elapsed },
|
|
56
|
+
text: shown.join('\n'),
|
|
57
|
+
children: shown.length < rows.length && resultPath !== undefined
|
|
58
|
+
? [{ element: 'truncated', attributes: { rows: rows.length, shown: shown.length, file: resultPath }, text: 'Re-run with --rows 0 for every row on stdout.' }]
|
|
59
|
+
: undefined,
|
|
60
|
+
},
|
|
61
|
+
count: rows.length,
|
|
62
|
+
receipt: `${shown.length} of ${rows.length} query row${rows.length === 1 ? '' : 's'}${unordered ? ', unordered' : ''}, ${elapsed} ms`,
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
function parametersFor(invocation) {
|
|
66
|
+
const values = {};
|
|
67
|
+
const problems = [];
|
|
68
|
+
const problem = (received, expected, next) => problems.push({ field: '--param', received, expected, next });
|
|
69
|
+
for (const raw of invocation.input.flags['--param'] ?? []) {
|
|
70
|
+
const separator = raw.indexOf('=');
|
|
71
|
+
if (separator <= 0) {
|
|
72
|
+
problem(raw, 'NAME=VALUE', 'Name the Cypher parameter before `=`.');
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const name = raw.slice(0, separator);
|
|
76
|
+
if (!/^[_A-Za-z][_0-9A-Za-z]*$/.test(name)) {
|
|
77
|
+
problem(raw, 'NAME=VALUE with a Cypher parameter name', 'Use letters, digits, and underscores; the name cannot begin with a digit.');
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (name in values) {
|
|
81
|
+
problem(raw, `one binding for $${name}`, 'Bind each parameter once.');
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (name === 'commits' && invocation.input.flags['--since'] !== undefined) {
|
|
85
|
+
problem(raw, '$commits bound by --since', 'Remove --param commits or omit --since.');
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
values[name] = parseParameter(raw.slice(separator + 1));
|
|
89
|
+
}
|
|
90
|
+
if (problems.length > 0)
|
|
91
|
+
return { error: { category: 'invocation', code: 'query-rejected', problems } };
|
|
92
|
+
return { parameters: values };
|
|
93
|
+
}
|
|
94
|
+
function parseParameter(value) {
|
|
95
|
+
if (/^-?\d+$/.test(value))
|
|
96
|
+
return Number(value);
|
|
97
|
+
if (/^-?(?:\d+\.\d*|\d*\.\d+)(?:[eE][+-]?\d+)?$/.test(value))
|
|
98
|
+
return Number(value);
|
|
99
|
+
if (value === 'true')
|
|
100
|
+
return true;
|
|
101
|
+
if (value === 'false')
|
|
102
|
+
return false;
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
function commitsSince(root, ref) {
|
|
106
|
+
try {
|
|
107
|
+
const output = execFileSync('git', ['rev-list', `${ref}..HEAD`], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
108
|
+
return output.split('\n').filter(Boolean);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
112
|
+
return rejected('--since', ref, 'a git ref reachable from this workspace', `Git could not resolve that ref: ${message}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function renderRow(record) {
|
|
116
|
+
const entry = addressFor(record);
|
|
117
|
+
const values = Object.entries(record)
|
|
118
|
+
.filter(([name]) => !entry?.columns.includes(name))
|
|
119
|
+
.map(([, value]) => renderValue(value));
|
|
120
|
+
return [entry?.value, ...values].filter((value) => value !== undefined).join(' ');
|
|
121
|
+
}
|
|
122
|
+
function addressFor(record) {
|
|
123
|
+
if (typeof record.at === 'string' && address.test(record.at))
|
|
124
|
+
return { value: record.at, columns: ['at'] };
|
|
125
|
+
const file = record.file;
|
|
126
|
+
const line = record.line;
|
|
127
|
+
const col = record.col;
|
|
128
|
+
if (typeof file === 'string' && typeof line === 'number' && typeof col === 'number')
|
|
129
|
+
return { value: `${file}:${line}:${col}`, columns: ['file', 'line', 'col'] };
|
|
130
|
+
for (const [column, value] of Object.entries(record)) {
|
|
131
|
+
if (typeof value === 'string' && address.test(value))
|
|
132
|
+
return { value, columns: [column] };
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
function renderValue(value) {
|
|
137
|
+
if (value === null)
|
|
138
|
+
return 'null';
|
|
139
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint')
|
|
140
|
+
return String(value);
|
|
141
|
+
if (value instanceof Date)
|
|
142
|
+
return value.toISOString();
|
|
143
|
+
if (Array.isArray(value))
|
|
144
|
+
return `[${value.map(renderValue).join(', ')}]`;
|
|
145
|
+
if (value instanceof Map)
|
|
146
|
+
return `{${[...value.entries()].map(([key, entry]) => `${renderValue(key)}: ${renderValue(entry)}`).join(', ')}}`;
|
|
147
|
+
return `{${Object.entries(value).map(([key, entry]) => `${key}: ${renderValue(entry)}`).join(', ')}}`;
|
|
148
|
+
}
|
|
149
|
+
function orderDefault(records) {
|
|
150
|
+
const addressed = records.filter((record) => addressFor(record) !== undefined).sort((left, right) => compareAddresses(addressFor(left).value, addressFor(right).value));
|
|
151
|
+
const unaddressed = records.filter((record) => addressFor(record) === undefined);
|
|
152
|
+
return [...addressed, ...unaddressed];
|
|
153
|
+
}
|
|
154
|
+
function compareAddresses(left, right) {
|
|
155
|
+
const leftParts = address.exec(left);
|
|
156
|
+
const rightParts = address.exec(right);
|
|
157
|
+
return leftParts[1].localeCompare(rightParts[1]) || Number(leftParts[2]) - Number(rightParts[2]) || Number(leftParts[3]) - Number(rightParts[3]);
|
|
158
|
+
}
|
|
159
|
+
function hasTopLevelOrderBy(cypher) {
|
|
160
|
+
let depth = 0;
|
|
161
|
+
let quote;
|
|
162
|
+
for (let index = 0; index < cypher.length; index++) {
|
|
163
|
+
const character = cypher[index];
|
|
164
|
+
if (quote !== undefined) {
|
|
165
|
+
if (character === quote) {
|
|
166
|
+
if (cypher[index + 1] === quote)
|
|
167
|
+
index++;
|
|
168
|
+
else
|
|
169
|
+
quote = undefined;
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (character === "'" || character === '"' || character === '`') {
|
|
174
|
+
quote = character;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (character === '(' || character === '[' || character === '{') {
|
|
178
|
+
depth++;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (character === ')' || character === ']' || character === '}') {
|
|
182
|
+
depth--;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (depth !== 0 || !/[Oo]/.test(character))
|
|
186
|
+
continue;
|
|
187
|
+
const candidate = cypher.slice(index, index + 32);
|
|
188
|
+
if (/^order\s+by\b/i.test(candidate) && !/[A-Za-z0-9_]/.test(cypher[index - 1] ?? ''))
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
function rejected(field, received, expected, next) {
|
|
194
|
+
return { category: 'invocation', code: 'query-rejected', problems: [{ field, received, expected, next }] };
|
|
195
|
+
}
|
|
196
|
+
export default handler;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { matchesPathFilters } from '../../core/receipt.js';
|
|
3
|
+
import { RenamePreflightError, renameFromOccurrences } from '../../ts7/checker.js';
|
|
4
|
+
import { query } from '../../store/query.js';
|
|
5
|
+
const handler = async (context, invocation) => {
|
|
6
|
+
const [input, newName] = invocation.input.args;
|
|
7
|
+
if (!input || !newName)
|
|
8
|
+
return invalid('rename needs a symbol and a replacement identifier.', input ?? 'none', 'exactly two values: <symbol> <newName>');
|
|
9
|
+
await context.projects.bootstrap();
|
|
10
|
+
const declaration = await context.resolver.resolve(input);
|
|
11
|
+
try {
|
|
12
|
+
if (!context.store)
|
|
13
|
+
throw new Error('The TypeScript index is not available yet.');
|
|
14
|
+
const edits = renameFromOccurrences(context.program, context.root, context.projects.configured(), declaration, occurrences(context.store, declaration.location.path, declaration.location.line, declaration.location.col), newName);
|
|
15
|
+
const plan = buildPlan(context.root, declaration.address, declaration.name, newName, edits, context.source.read.bind(context.source));
|
|
16
|
+
const visible = plan.files.filter((file) => matchesPathFilters(file.path, { in: strings(invocation.input.flags['--in']), exclude: strings(invocation.input.flags['--exclude']) }));
|
|
17
|
+
const sites = visible.reduce((total, file) => total + file.edits.length, 0);
|
|
18
|
+
const answer = {
|
|
19
|
+
data: { element: 'rename', attributes: { symbol: declaration.address, from: declaration.name, to: newName, sites, files: visible.length, applied: false }, children: visible.map(renderFile) },
|
|
20
|
+
count: sites,
|
|
21
|
+
receipt: `${sites} rename site${sites === 1 ? '' : 's'} across ${visible.length} file${visible.length === 1 ? '' : 's'}, 0 ms`,
|
|
22
|
+
renamePlan: plan,
|
|
23
|
+
};
|
|
24
|
+
return answer;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error instanceof RenamePreflightError)
|
|
28
|
+
return refusal(error, input);
|
|
29
|
+
return { category: 'tool', code: 'typescript-error', message: `TypeScript could not prepare this rename: ${message(error)}.`, received: input, expected: 'checker-resolved occurrence locations', next: 'Read server status and retry after repairing the workspace.' };
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
function occurrences(store, file, line, col) {
|
|
33
|
+
return query(store, 'MATCH (owner:Declaration)-[r:REFERENCES]->(d:Declaration) WHERE d.file = $file AND d.line = $line AND d.col = $col RETURN owner.file AS file, r.line AS line, r.col AS col', { file, line, col }).map((row) => ({ file: String(row.file), line: Number(row.line), col: Number(row.col) }));
|
|
34
|
+
}
|
|
35
|
+
function buildPlan(root, symbol, from, to, locations, read) {
|
|
36
|
+
const grouped = new Map();
|
|
37
|
+
for (const location of locations) {
|
|
38
|
+
const file = path.resolve(root, location.file);
|
|
39
|
+
grouped.set(file, [...(grouped.get(file) ?? []), { startUtf16: location.startUtf16, endUtf16: location.endUtf16, line: location.line, col: location.col, oldText: location.oldText, replacement: location.replacement }]);
|
|
40
|
+
}
|
|
41
|
+
const files = [...grouped].map(([file, edits]) => ({ path: path.relative(root, file).split(path.sep).join('/'), snapshot: Buffer.from(read(file)).toString('base64'), edits: edits.sort((left, right) => left.startUtf16 - right.startUtf16 || left.endUtf16 - right.endUtf16) })).sort((left, right) => left.path.localeCompare(right.path));
|
|
42
|
+
return { root, symbol, from, to, files };
|
|
43
|
+
}
|
|
44
|
+
function refusal(error, received) {
|
|
45
|
+
return { category: 'invocation', code: error.code, problems: [{ field: '<symbol>', received: `${error.site}: ${error.message}`, expected: 'a rename without an unsupported site or name conflict', next: 'Choose a different symbol or replacement identifier, then retry rename.' }] };
|
|
46
|
+
}
|
|
47
|
+
function renderFile(file) {
|
|
48
|
+
return { element: 'file', attributes: { path: file.path, sites: file.edits.length }, text: file.edits.map((edit) => `${edit.line}:${edit.col} ${edit.oldText} → ${edit.replacement}`).join('\n') };
|
|
49
|
+
}
|
|
50
|
+
function strings(value) { return Array.isArray(value) ? value : undefined; }
|
|
51
|
+
function invalid(message, received, expected) { return { category: 'invocation', code: 'cannot-rename', problems: [{ field: '<symbol>', received, expected: `${expected}; ${message}`, next: 'Choose a symbol TypeScript can rename, then retry.' }] }; }
|
|
52
|
+
function message(error) { return error instanceof Error ? error.message : String(error); }
|
|
53
|
+
export default handler;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Answer, Invocation, InvocationError, ToolError } from '../types.js';
|
|
2
|
+
import type { SemanticHandler } from '../handlers.js';
|
|
3
|
+
export declare function localSearch(invocation: Invocation): Promise<Answer | InvocationError | ToolError>;
|
|
4
|
+
declare const handler: SemanticHandler;
|
|
5
|
+
export default handler;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { search } from '../../core/search.js';
|
|
2
|
+
import { openWorkspace } from '../../core/workspace.js';
|
|
3
|
+
export async function localSearch(invocation) {
|
|
4
|
+
const pattern = invocation.input.args[0];
|
|
5
|
+
let result;
|
|
6
|
+
try {
|
|
7
|
+
result = await search(openWorkspace(invocation.root), pattern, {
|
|
8
|
+
in: invocation.input.flags['--in'],
|
|
9
|
+
exclude: invocation.input.flags['--exclude'],
|
|
10
|
+
limit: invocation.input.flags['--limit'],
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (isInvalidPattern(error))
|
|
15
|
+
return { category: 'invocation', code: 'invalid-pattern', problems: [{ field: '<pattern>', received: pattern, expected: `one ast-grep pattern that parses as a single TypeScript node; ${error.message}`, next: 'Wrap the fragment so it is one node, for example a class body, or use --in with a simpler pattern.' }] };
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
data: {
|
|
20
|
+
element: 'search',
|
|
21
|
+
attributes: { pattern, count: result.count, shown: result.receipt.truncated ? result.items.length : undefined },
|
|
22
|
+
text: result.items.map((match) => `${match.path}:${match.start.line}:${match.start.col} ${match.text}`).join('\n'),
|
|
23
|
+
},
|
|
24
|
+
count: result.count,
|
|
25
|
+
receipt: `${result.receipt.truncated ? `${result.items.length} of ${result.count} search rows shown, limit ${result.receipt.limit}` : `${result.items.length} search row${result.items.length === 1 ? '' : 's'}`}${result.receipt.excluded > 0 ? `, ${result.receipt.excluded} excluded` : ''}, ${result.receipt.elapsed_ms} ms`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function isInvalidPattern(error) {
|
|
29
|
+
return error instanceof Error && error.code === 'InvalidArg' && error.message.includes('Rule contains invalid pattern matcher.');
|
|
30
|
+
}
|
|
31
|
+
const handler = async (_context, invocation) => localSearch(invocation);
|
|
32
|
+
export default handler;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { DeclarationError } from '../../core/declarations.js';
|
|
2
|
+
import { query } from '../../store/query.js';
|
|
3
|
+
const handler = async (context, invocation) => {
|
|
4
|
+
await context.projects.bootstrap();
|
|
5
|
+
const started = performance.now();
|
|
6
|
+
const resolved = await context.resolver.resolveAll(invocation.input.args);
|
|
7
|
+
const rendered = [];
|
|
8
|
+
for (const declaration of resolved) {
|
|
9
|
+
const source = context.source.read(declaration.file);
|
|
10
|
+
const span = storedSpan(context, declaration);
|
|
11
|
+
const sites = declaration.location.line === 0 ? [{ source, span }] : await declarationSites(context, declaration, span);
|
|
12
|
+
const text = declaration.location.line === 0 ? source : sites.map((site) => declarationText(site.source, site.span)).join('\n');
|
|
13
|
+
const lines = declaration.location.line === 0 ? sourceLines(source) : sites.reduce((total, site) => total + site.span.lines, 0);
|
|
14
|
+
rendered.push({ element: 'symbol', attributes: { at: declaration.address, name: declaration.name, kind: declaration.kind, lines }, text });
|
|
15
|
+
}
|
|
16
|
+
const limit = invocation.input.flags['--limit'];
|
|
17
|
+
const shown = limit === undefined ? rendered : rendered.slice(0, limit);
|
|
18
|
+
const capped = shown.length < rendered.length;
|
|
19
|
+
return {
|
|
20
|
+
data: { element: 'show', attributes: { count: rendered.length, shown: capped ? shown.length : undefined }, children: shown },
|
|
21
|
+
count: rendered.length,
|
|
22
|
+
receipt: capped ? `${shown.length} of ${rendered.length} show rows shown, limit ${limit}, ${Math.round(performance.now() - started)} ms` : `${shown.length} show declaration${shown.length === 1 ? '' : 's'}, ${Math.round(performance.now() - started)} ms`,
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
function storedSpan(context, declaration) {
|
|
26
|
+
if (!context.store)
|
|
27
|
+
throw new DeclarationError('not-in-program', 'The TypeScript index is not available yet.');
|
|
28
|
+
const row = query(context.store, 'MATCH (d:Declaration) WHERE d.file = $file AND d.line = $line AND d.col = $col RETURN d.endLine AS endLine, d.lines AS lines, d.declCount AS declCount', { file: declaration.location.path, line: declaration.location.line, col: declaration.location.col })[0];
|
|
29
|
+
if (!row)
|
|
30
|
+
throw new DeclarationError('unresolvable-symbol', `${declaration.address} is absent from the TypeScript index`);
|
|
31
|
+
return { endLine: Number(row.endLine), lines: Number(row.lines), declCount: Number(row.declCount) };
|
|
32
|
+
}
|
|
33
|
+
async function declarationSites(context, declaration, stored) {
|
|
34
|
+
if (stored.declCount === 1)
|
|
35
|
+
return [{ source: context.source.read(declaration.file), span: stored }];
|
|
36
|
+
const membership = await context.projects.membership(declaration.file);
|
|
37
|
+
if (!membership)
|
|
38
|
+
throw new DeclarationError('not-in-program', `${declaration.location.path} exists but is not part of a TypeScript project under ${context.root}`);
|
|
39
|
+
const sites = context.program.withSnapshot((snapshot) => {
|
|
40
|
+
const project = snapshot.getProject(membership.config);
|
|
41
|
+
if (!project)
|
|
42
|
+
throw new Error(`TypeScript project ${membership.config} is not loaded.`);
|
|
43
|
+
const symbol = project.checker.getSymbolAtPosition(declaration.file, declaration.nameSpan.start);
|
|
44
|
+
return (symbol?.declarations ?? []).flatMap((handle) => {
|
|
45
|
+
const node = handle.resolve(project);
|
|
46
|
+
if (!node)
|
|
47
|
+
return [];
|
|
48
|
+
const source = node.getSourceFile();
|
|
49
|
+
const start = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
50
|
+
const end = source.getLineAndCharacterOfPosition(node.getEnd());
|
|
51
|
+
return [{ source: context.source.read(source.fileName), span: { endLine: end.line + 1, lines: end.line - start.line + 1, declCount: stored.declCount } }];
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
if (sites.length === 0)
|
|
55
|
+
throw new DeclarationError('unresolvable-symbol', `${declaration.address} has no declaration sites in the TypeScript program`);
|
|
56
|
+
return sites;
|
|
57
|
+
}
|
|
58
|
+
function declarationText(source, span) {
|
|
59
|
+
const firstLine = span.endLine - span.lines + 1;
|
|
60
|
+
return source.split('\n').slice(firstLine - 1, span.endLine).join('\n');
|
|
61
|
+
}
|
|
62
|
+
function sourceLines(source) { return source.split('\n').length - (source.endsWith('\n') ? 1 : 0); }
|
|
63
|
+
export default handler;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { DeclarationError, DeclarationResolutionErrors } from '../../core/declarations.js';
|
|
2
|
+
import { normalKind } from '../../read/read.js';
|
|
3
|
+
import { typeAt } from '../../ts7/checker.js';
|
|
4
|
+
const handler = async (context, invocation) => {
|
|
5
|
+
try {
|
|
6
|
+
await context.projects.bootstrap();
|
|
7
|
+
const declarations = await context.resolver.resolveAll(invocation.input.args);
|
|
8
|
+
const includeDefinition = invocation.input.flags['--def'] === true;
|
|
9
|
+
const symbols = [];
|
|
10
|
+
let answers = 0;
|
|
11
|
+
for (const declaration of declarations) {
|
|
12
|
+
const project = await context.projects.membership(declaration.file);
|
|
13
|
+
if (!project)
|
|
14
|
+
throw new DeclarationError('not-in-program', `${declaration.location.path} exists but is not part of a TypeScript project under ${context.root}`);
|
|
15
|
+
const answer = typeAt(context.program, context.root, project.config, declaration.file, declaration.nameSpan.start, { ...declaration, kind: normalKind(declaration.kind) });
|
|
16
|
+
if (answer !== null)
|
|
17
|
+
answers++;
|
|
18
|
+
symbols.push(renderSymbol(declaration, answer, includeDefinition));
|
|
19
|
+
}
|
|
20
|
+
return { data: { element: 'type', children: symbols }, count: answers };
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error instanceof DeclarationError || error instanceof DeclarationResolutionErrors)
|
|
24
|
+
throw error;
|
|
25
|
+
return failure(error);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
function renderSymbol(declaration, answer, includeDefinition) {
|
|
29
|
+
const attributes = {
|
|
30
|
+
at: declaration.address,
|
|
31
|
+
name: declaration.name,
|
|
32
|
+
kind: normalKind(answer?.kind ?? declaration.kind),
|
|
33
|
+
modifiers: answer?.modifiers,
|
|
34
|
+
typeAt: includeDefinition && answer && answer.definitions.length > 0 ? answer.definitions.join(' ') : undefined,
|
|
35
|
+
};
|
|
36
|
+
if (answer === null)
|
|
37
|
+
return { element: 'symbol', attributes };
|
|
38
|
+
const children = [documentation(answer.documentation), ...answer.tags.map((tag) => ({ element: 'tag', attributes: { name: tag.name }, text: tag.text }))].filter((node) => node !== null);
|
|
39
|
+
return { element: 'symbol', attributes, text: answer.signature, children };
|
|
40
|
+
}
|
|
41
|
+
function documentation(text) { return text ? { element: 'doc', text } : null; }
|
|
42
|
+
function failure(error) {
|
|
43
|
+
return { category: 'tool', code: 'typescript-error', message: 'TypeScript could not answer the type query.', received: error instanceof Error ? error.message : String(error), expected: 'a successful checker type query', next: 'Read the server log, then retry.' };
|
|
44
|
+
}
|
|
45
|
+
export default handler;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DeclarationError } from '../core/declarations.js';
|
|
2
|
+
import type { SemanticContext } from '../server/context.js';
|
|
3
|
+
import type { Completion, Invocation, InvocationError } from './types.js';
|
|
4
|
+
/** A leaf handler answers, or returns an invocation error (exit 2) or a tool error (exit 3). It never writes output. */
|
|
5
|
+
export type SemanticHandler = (context: SemanticContext, invocation: Invocation) => Promise<Completion>;
|
|
6
|
+
export declare function executeSemantic(context: SemanticContext, invocation: Invocation): Promise<Completion>;
|
|
7
|
+
/**
|
|
8
|
+
* A resolver rejection is the caller's input being wrong, not the tool failing: exit 2 with the leaf's `-h` as
|
|
9
|
+
* the next step. Handlers may let `DeclarationError` propagate and rely on this mapping, or return an
|
|
10
|
+
* `InvocationError` themselves when they know a sharper field or next step.
|
|
11
|
+
*/
|
|
12
|
+
export declare function declarationError(error: DeclarationError, invocation: Invocation): InvocationError;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { DeclarationError, DeclarationResolutionErrors } from '../core/declarations.js';
|
|
2
|
+
const modules = {
|
|
3
|
+
outline: () => import('./handlers/outline.js'), find: () => import('./handlers/find.js'), show: () => import('./handlers/show.js'), type: () => import('./handlers/type.js'), inspect: () => import('./handlers/inspect.js'), query: () => import('./handlers/query.js'), search: () => import('./handlers/search.js'), check: () => import('./handlers/check.js'), rename: () => import('./handlers/rename.js'), 'server-status': () => import('./handlers/server-status.js'), 'server-stop': () => import('./handlers/server-stop.js'), 'server-reload': () => import('./handlers/server-reload.js'),
|
|
4
|
+
};
|
|
5
|
+
export async function executeSemantic(context, invocation) {
|
|
6
|
+
const handler = invocation.declaration.handler;
|
|
7
|
+
if (!handler)
|
|
8
|
+
return { category: 'tool', code: 'not-implemented', message: `tsym ${invocation.path.join(' ')} has no handler.`, next: 'Register an executable leaf handler.' };
|
|
9
|
+
try {
|
|
10
|
+
return await (await modules[handler]()).default(context, invocation);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error instanceof DeclarationError)
|
|
14
|
+
return declarationError(error, invocation);
|
|
15
|
+
if (error instanceof DeclarationResolutionErrors)
|
|
16
|
+
return declarationErrors(error, invocation);
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A resolver rejection is the caller's input being wrong, not the tool failing: exit 2 with the leaf's `-h` as
|
|
22
|
+
* the next step. Handlers may let `DeclarationError` propagate and rely on this mapping, or return an
|
|
23
|
+
* `InvocationError` themselves when they know a sharper field or next step.
|
|
24
|
+
*/
|
|
25
|
+
export function declarationError(error, invocation) {
|
|
26
|
+
return { category: 'invocation', code: error.code, problems: [declarationProblem(error, invocation)] };
|
|
27
|
+
}
|
|
28
|
+
function declarationErrors(errors, invocation) {
|
|
29
|
+
return { category: 'invocation', code: errors.errors[0].code, problems: errors.errors.map((error) => declarationProblem(error, invocation)) };
|
|
30
|
+
}
|
|
31
|
+
function declarationProblem(error, invocation) {
|
|
32
|
+
const leaf = `tsym ${invocation.path.join(' ')}`;
|
|
33
|
+
const expected = {
|
|
34
|
+
'no-such-path': 'an existing file or directory under --root',
|
|
35
|
+
'not-in-program': 'a file that a tsconfig under --root includes',
|
|
36
|
+
'unresolvable-symbol': 'a declaration name, qualified member name, or path:line:col address',
|
|
37
|
+
'ambiguous-symbol': 'one canonical address',
|
|
38
|
+
'invalid-address': 'path:line:col with both coordinates positive, or path:0:0 for a module',
|
|
39
|
+
};
|
|
40
|
+
const next = error.code === 'ambiguous-symbol' && error.candidates.length > 0
|
|
41
|
+
? `Pass one of: ${error.candidates.join(' ')}`
|
|
42
|
+
: `Run \`${leaf} -h\` for the accepted input forms.`;
|
|
43
|
+
return { field: invocation.declaration.argument?.name ?? 'argument', received: error.message, expected: expected[error.code], next };
|
|
44
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { xml, escapeAttribute } from '../output.js';
|
|
2
|
+
import { commandTree } from './tree.js';
|
|
3
|
+
export function renderHelp(node, serverState = '<server state="stopped"/>') {
|
|
4
|
+
const lines = [];
|
|
5
|
+
if (node.model)
|
|
6
|
+
lines.push(`<model>\n${node.model}${node === commandTree || node.state ? `\n\n${serverState}` : ''}\n</model>`);
|
|
7
|
+
if (serverState && node !== commandTree && !node.state)
|
|
8
|
+
lines.push(serverState);
|
|
9
|
+
if (node.children)
|
|
10
|
+
lines.push(...node.children.map((child) => `<subcommand name="${child.name}" description="${escapeAttribute(child.description)}" whenToUse="${escapeAttribute(child.whenToUse ?? '')}"/>`));
|
|
11
|
+
if (node.argument || node.flags?.length)
|
|
12
|
+
lines.push(`<input>\n${[node.argument ? argument(node.argument) : null, ...(node.flags ?? []).map(flag)].filter((line) => line !== null).join('\n')}\n</input>`);
|
|
13
|
+
if (node.output)
|
|
14
|
+
lines.push(`<output>${node.output}</output>`);
|
|
15
|
+
if (node.effects)
|
|
16
|
+
lines.push(`<effects>${node.effects}</effects>`);
|
|
17
|
+
if (node === commandTree) {
|
|
18
|
+
lines.push('<globals>\n <flag name="--root" type="path">Workspace root, overriding discovery from the current directory.</flag>\n <flag name="--json" type="bool">Raw JSON on stdout for programmatic consumers.</flag>\n</globals>');
|
|
19
|
+
lines.push('<io>Flags and positional arguments on input. Stdout is one named XML block per call. One receipt goes to stderr on every call. Exit 0 with results, 1 with none, 2 when the invocation was wrong, 3 when the tool could not answer.</io>');
|
|
20
|
+
}
|
|
21
|
+
return xml('command', { name: node.name, description: node.description }, lines.join('\n'));
|
|
22
|
+
}
|
|
23
|
+
function argument(declaration) { return ` <arg name="${declaration.name}" repeat="${declaration.repeat}">${declaration.description}</arg>`; }
|
|
24
|
+
function flag(declaration) {
|
|
25
|
+
const attrs = [`name="${declaration.name}"`, `type="${declaration.type}"`, declaration.repeat ? 'repeat="yes"' : null, declaration.default !== undefined ? `default="${declaration.default}"` : null].filter(Boolean).join(' ');
|
|
26
|
+
return ` <flag ${attrs}>${declaration.description}</flag>`;
|
|
27
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { chmod, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import type { ToolError } from './types.js';
|
|
3
|
+
export interface RenameEdit {
|
|
4
|
+
startUtf16: number;
|
|
5
|
+
endUtf16: number;
|
|
6
|
+
line: number;
|
|
7
|
+
col: number;
|
|
8
|
+
oldText: string;
|
|
9
|
+
replacement: string;
|
|
10
|
+
}
|
|
11
|
+
export interface RenameFilePlan {
|
|
12
|
+
path: string;
|
|
13
|
+
snapshot: string;
|
|
14
|
+
edits: RenameEdit[];
|
|
15
|
+
}
|
|
16
|
+
export interface RenamePlan {
|
|
17
|
+
root: string;
|
|
18
|
+
symbol: string;
|
|
19
|
+
from: string;
|
|
20
|
+
to: string;
|
|
21
|
+
files: RenameFilePlan[];
|
|
22
|
+
}
|
|
23
|
+
export type RenameAnswer = {
|
|
24
|
+
renamePlan: RenamePlan;
|
|
25
|
+
};
|
|
26
|
+
export interface RenameFileOperations {
|
|
27
|
+
readFile: typeof readFile;
|
|
28
|
+
writeFile: typeof writeFile;
|
|
29
|
+
rename: typeof rename;
|
|
30
|
+
rm: typeof rm;
|
|
31
|
+
stat: typeof stat;
|
|
32
|
+
chmod: typeof chmod;
|
|
33
|
+
}
|
|
34
|
+
/** Apply a complete compiler-produced plan without asking the resident to recompute it. */
|
|
35
|
+
export declare function applyRenamePlan(plan: RenamePlan, operations?: RenameFileOperations): Promise<{
|
|
36
|
+
applied: true;
|
|
37
|
+
} | ToolError>;
|