codelocal 1.5.0-beta.1
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/README.md +27 -0
- package/dist/approval-memory.js +105 -0
- package/dist/audit.js +34 -0
- package/dist/chat-approval.js +77 -0
- package/dist/cli-saas.js +311 -0
- package/dist/cli.js +344 -0
- package/dist/client-entry-v2.js +22 -0
- package/dist/client-v2.js +910 -0
- package/dist/cloud-client-sync.js +6 -0
- package/dist/context-engine.js +295 -0
- package/dist/editing-engine.js +205 -0
- package/dist/identity.js +30 -0
- package/dist/log.js +235 -0
- package/dist/lsp.js +288 -0
- package/dist/mcp-cloud-sync.js +3 -0
- package/dist/mcp-hub.js +508 -0
- package/dist/native-watcher.js +148 -0
- package/dist/process-manager.js +261 -0
- package/dist/protocol.js +52 -0
- package/dist/runtime-daemon.js +162 -0
- package/dist/security-policy.js +293 -0
- package/dist/semantic-router.js +378 -0
- package/dist/semantic.js +263 -0
- package/dist/state.js +110 -0
- package/dist/terminal-history.js +102 -0
- package/dist/verification.js +66 -0
- package/dist/workspace-index.js +457 -0
- package/dist/workspace-registry.js +86 -0
- package/package.json +31 -0
package/dist/log.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
const LOG_LEVEL = (process.env.CODELOCAL_LOG_LEVEL ?? "info").toLowerCase();
|
|
2
|
+
const LOG_FORMAT = (process.env.CODELOCAL_LOG_FORMAT ?? "json").toLowerCase();
|
|
3
|
+
const ranks = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
4
|
+
const configuredRank = ranks[(LOG_LEVEL in ranks ? LOG_LEVEL : "info")];
|
|
5
|
+
const ansi = {
|
|
6
|
+
reset: "\u001b[0m",
|
|
7
|
+
bold: "\u001b[1m",
|
|
8
|
+
dim: "\u001b[2m",
|
|
9
|
+
gray: "\u001b[90m",
|
|
10
|
+
cyan: "\u001b[36m",
|
|
11
|
+
green: "\u001b[32m",
|
|
12
|
+
yellow: "\u001b[33m",
|
|
13
|
+
red: "\u001b[31m",
|
|
14
|
+
magenta: "\u001b[35m",
|
|
15
|
+
};
|
|
16
|
+
function colorEnabled() {
|
|
17
|
+
return process.env.NO_COLOR == null && process.env.TERM !== "dumb" && !!process.stdout.isTTY;
|
|
18
|
+
}
|
|
19
|
+
function paint(code, value) {
|
|
20
|
+
const text = String(value ?? "");
|
|
21
|
+
return colorEnabled() ? `${code}${text}${ansi.reset}` : text;
|
|
22
|
+
}
|
|
23
|
+
function bold(value) { return paint(ansi.bold, value); }
|
|
24
|
+
function dim(value) { return paint(ansi.gray, value); }
|
|
25
|
+
function redactText(input) {
|
|
26
|
+
let value = input;
|
|
27
|
+
value = value.replace(/(authorization\s*:\s*bearer\s+)[^\s\"']+/gi, "$1[REDACTED]");
|
|
28
|
+
value = value.replace(/((?:api[_-]?key|access[_-]?token|auth[_-]?token|password|secret)\s*[=:]\s*)[^\s\"']+/gi, "$1[REDACTED]");
|
|
29
|
+
value = value.replace(/(--(?:token|password|secret|api-key|apikey)\s+)([^\s]+)/gi, "$1[REDACTED]");
|
|
30
|
+
value = value.replace(/((?:OPENAI_API_KEY|CODEX_API_KEY|AWS_SECRET_ACCESS_KEY|GITHUB_TOKEN|GH_TOKEN)=)([^\s]+)/gi, "$1[REDACTED]");
|
|
31
|
+
value = value.replace(/(https?:\/\/[^\s:@/]+:)[^\s@/]+@/gi, "$1[REDACTED]@");
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
function sanitize(value) {
|
|
35
|
+
if (value instanceof Error)
|
|
36
|
+
return { name: value.name, message: redactText(value.message) };
|
|
37
|
+
if (Array.isArray(value))
|
|
38
|
+
return value.map(sanitize);
|
|
39
|
+
if (!value || typeof value !== "object")
|
|
40
|
+
return typeof value === "string" ? redactText(value) : value;
|
|
41
|
+
const out = {};
|
|
42
|
+
for (const [key, item] of Object.entries(value)) {
|
|
43
|
+
if (/token|secret|password|authorization|cookie/i.test(key))
|
|
44
|
+
out[key] = "[redacted]";
|
|
45
|
+
else if (/content|patch|oldText|newText|input/i.test(key) && typeof item === "string")
|
|
46
|
+
out[key] = `[${Buffer.byteLength(item, "utf8")} bytes]`;
|
|
47
|
+
else if (typeof item === "string")
|
|
48
|
+
out[key] = redactText(item);
|
|
49
|
+
else
|
|
50
|
+
out[key] = sanitize(item);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
function duration(value) {
|
|
55
|
+
const ms = Number(value);
|
|
56
|
+
if (!Number.isFinite(ms))
|
|
57
|
+
return "";
|
|
58
|
+
if (ms < 1000)
|
|
59
|
+
return `${Math.max(0, Math.round(ms))}ms`;
|
|
60
|
+
return `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)}s`;
|
|
61
|
+
}
|
|
62
|
+
function compact(value, max = 76) {
|
|
63
|
+
const text = redactText(String(value ?? "")).replace(/\s+/g, " ").trim();
|
|
64
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
65
|
+
}
|
|
66
|
+
function toolDetail(args) {
|
|
67
|
+
if (!args || typeof args !== "object")
|
|
68
|
+
return "";
|
|
69
|
+
const value = args;
|
|
70
|
+
if (typeof value.command === "string")
|
|
71
|
+
return `$ ${compact(value.command, 64)}`;
|
|
72
|
+
if (typeof value.path === "string") {
|
|
73
|
+
const lines = value.startLine != null ? ` L${value.startLine}${value.endLine != null ? `–${value.endLine}` : ""}` : "";
|
|
74
|
+
return `${compact(value.path, 58)}${lines}`;
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(value.paths))
|
|
77
|
+
return `${value.paths.length} file${value.paths.length === 1 ? "" : "s"}`;
|
|
78
|
+
if (typeof value.query === "string")
|
|
79
|
+
return `“${compact(value.query, 58)}”`;
|
|
80
|
+
if (typeof value.name === "string")
|
|
81
|
+
return compact(value.name, 58);
|
|
82
|
+
if (typeof value.processId === "string")
|
|
83
|
+
return `process ${value.processId.slice(0, 8)}`;
|
|
84
|
+
if (typeof value.cwd === "string" && value.cwd !== ".")
|
|
85
|
+
return `in ${compact(value.cwd, 58)}`;
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
function terminalIcon(tone) {
|
|
89
|
+
if (tone === "success")
|
|
90
|
+
return paint(ansi.green, "●");
|
|
91
|
+
if (tone === "accent")
|
|
92
|
+
return paint(ansi.magenta, "◆");
|
|
93
|
+
if (tone === "warn")
|
|
94
|
+
return paint(ansi.yellow, "▲");
|
|
95
|
+
if (tone === "error")
|
|
96
|
+
return paint(ansi.red, "✕");
|
|
97
|
+
if (tone === "muted")
|
|
98
|
+
return paint(ansi.gray, "◌");
|
|
99
|
+
return paint(ansi.cyan, "◇");
|
|
100
|
+
}
|
|
101
|
+
export function terminalStatus(tone, label, detail = "") {
|
|
102
|
+
const padded = `${label}`.padEnd(12, " ");
|
|
103
|
+
console.log(` ${terminalIcon(tone)} ${bold(padded)} ${detail}`.trimEnd());
|
|
104
|
+
}
|
|
105
|
+
export function terminalHeader(title = "CodeLocal", subtitle = "Local runtime for ChatGPT") {
|
|
106
|
+
const width = 48;
|
|
107
|
+
const top = `╭${"─".repeat(width - 2)}╮`;
|
|
108
|
+
const bottom = `╰${"─".repeat(width - 2)}╯`;
|
|
109
|
+
const row = (text) => `│ ${text}${" ".repeat(Math.max(0, width - 4 - text.length))}│`;
|
|
110
|
+
console.log("");
|
|
111
|
+
console.log(paint(ansi.magenta, top));
|
|
112
|
+
console.log(paint(ansi.magenta, row(`◆ ${title}`)));
|
|
113
|
+
console.log(paint(ansi.gray, row(subtitle)));
|
|
114
|
+
console.log(paint(ansi.magenta, bottom));
|
|
115
|
+
console.log("");
|
|
116
|
+
}
|
|
117
|
+
export function mirrorProcessOutput(stream, text) {
|
|
118
|
+
if (LOG_FORMAT !== "pretty") {
|
|
119
|
+
process.stdout.write(text);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const marker = stream === "stderr" ? paint(ansi.yellow, "│") : paint(ansi.gray, "│");
|
|
123
|
+
const lines = text.replace(/\r/g, "").split("\n");
|
|
124
|
+
const trailingNewline = text.endsWith("\n");
|
|
125
|
+
const rendered = lines
|
|
126
|
+
.filter((line, index) => line.length > 0 || index < lines.length - 1)
|
|
127
|
+
.map((line) => ` ${marker} ${line}`)
|
|
128
|
+
.join("\n");
|
|
129
|
+
if (rendered)
|
|
130
|
+
process.stdout.write(rendered + (trailingNewline ? "\n" : ""));
|
|
131
|
+
}
|
|
132
|
+
function prettyLog(level, event, fields) {
|
|
133
|
+
const tool = compact(fields.tool ?? "", 42);
|
|
134
|
+
const elapsed = duration(fields.durationMs);
|
|
135
|
+
if (event === "workspace.watcher_configured") {
|
|
136
|
+
terminalStatus("info", "Watcher", `${fields.backend ?? "native"} ${dim(`· fallback ${fields.fallbackBackend ?? "bounded"}`)}`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (event === "mcp.cloud_sync") {
|
|
140
|
+
const reason = fields.reason ? ` · ${fields.reason}` : "";
|
|
141
|
+
terminalStatus(fields.skipped ? "muted" : "success", "MCP sync", `${fields.skipped ? "Skipped" : "Ready"}${dim(reason)}`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (event === "mcp.cloud_sync_failed") {
|
|
145
|
+
terminalStatus("warn", "MCP sync", compact(fields.error ?? "Cloud sync unavailable"));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (event === "client.started") {
|
|
149
|
+
terminalStatus("accent", "Workspace", `${fields.workspaceId ?? "workspace"} ${dim(`· approval ${fields.approvalMode ?? "prompt"}`)}`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (event === "client.connecting") {
|
|
153
|
+
terminalStatus("info", "Cloud", "Connecting…");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (event === "client.registered") {
|
|
157
|
+
terminalStatus("success", "Cloud", `Connected ${dim(`· protocol v${fields.protocolVersion ?? "?"}`)}`);
|
|
158
|
+
console.log("");
|
|
159
|
+
terminalStatus("muted", "Status", "Waiting for ChatGPT…");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (event === "client.disconnected") {
|
|
163
|
+
terminalStatus("warn", "Cloud", `Disconnected ${dim(`· retry ${duration(fields.reconnectInMs)}`)}`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (event === "client.socket_error") {
|
|
167
|
+
const error = fields.error;
|
|
168
|
+
terminalStatus("error", "Cloud", compact(error?.message ?? "Connection error"));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (event === "tool.received") {
|
|
172
|
+
const detail = toolDetail(fields.args);
|
|
173
|
+
console.log(` ${paint(ansi.magenta, "◆")} ${paint(ansi.cyan, bold(tool || "tool"))}${detail ? ` ${dim(detail)}` : ""}`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (event === "tool.completed") {
|
|
177
|
+
console.log(` ${paint(ansi.green, "✓")} ${bold(tool || "tool")} ${dim(elapsed || "done")}`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (event === "tool.failed") {
|
|
181
|
+
const error = fields.error;
|
|
182
|
+
console.error(` ${paint(ansi.red, "✕")} ${bold(tool || "tool")} ${dim(elapsed)}${error?.message ? ` ${paint(ansi.red, compact(error.message, 72))}` : ""}`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const detail = Object.entries(fields)
|
|
186
|
+
.filter(([key]) => !["requestId", "tool"].includes(key))
|
|
187
|
+
.slice(0, 3)
|
|
188
|
+
.map(([key, value]) => `${key}=${compact(typeof value === "object" ? JSON.stringify(value) : value, 38)}`)
|
|
189
|
+
.join(" ");
|
|
190
|
+
const tone = level === "error" ? "error" : level === "warn" ? "warn" : level === "debug" ? "muted" : "info";
|
|
191
|
+
terminalStatus(tone, event.replace(/^client\.|^workspace\./, ""), detail);
|
|
192
|
+
}
|
|
193
|
+
export function log(level, event, fields = {}) {
|
|
194
|
+
if (ranks[level] < configuredRank)
|
|
195
|
+
return;
|
|
196
|
+
const cleanFields = sanitize(fields);
|
|
197
|
+
if (LOG_FORMAT === "pretty") {
|
|
198
|
+
prettyLog(level, event, cleanFields);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const entry = {
|
|
202
|
+
ts: new Date().toISOString(),
|
|
203
|
+
level,
|
|
204
|
+
event,
|
|
205
|
+
...cleanFields,
|
|
206
|
+
};
|
|
207
|
+
const line = JSON.stringify(entry);
|
|
208
|
+
if (level === "error")
|
|
209
|
+
console.error(line);
|
|
210
|
+
else if (level === "warn")
|
|
211
|
+
console.warn(line);
|
|
212
|
+
else
|
|
213
|
+
console.log(line);
|
|
214
|
+
}
|
|
215
|
+
export function summarizeToolArgs(tool, args) {
|
|
216
|
+
if (!args || typeof args !== "object")
|
|
217
|
+
return {};
|
|
218
|
+
const source = args;
|
|
219
|
+
const summary = {};
|
|
220
|
+
for (const key of ["path", "paths", "cwd", "processId", "cursor", "cached", "maxDepth", "maxResults", "fixedStrings", "yieldMs", "timeoutMs", "signal", "includeIgnored", "expectedHash", "startLine", "endLine", "limit", "name", "ref"]) {
|
|
221
|
+
if (key in source)
|
|
222
|
+
summary[key] = source[key];
|
|
223
|
+
}
|
|
224
|
+
if (typeof source.command === "string")
|
|
225
|
+
summary.command = redactText(source.command.slice(0, 500));
|
|
226
|
+
if (typeof source.query === "string")
|
|
227
|
+
summary.query = redactText(source.query.slice(0, 300));
|
|
228
|
+
if (typeof source.content === "string")
|
|
229
|
+
summary.contentBytes = Buffer.byteLength(source.content, "utf8");
|
|
230
|
+
if (typeof source.patch === "string")
|
|
231
|
+
summary.patchBytes = Buffer.byteLength(source.patch, "utf8");
|
|
232
|
+
if (typeof source.input === "string")
|
|
233
|
+
summary.inputBytes = Buffer.byteLength(source.input, "utf8");
|
|
234
|
+
return summary;
|
|
235
|
+
}
|
package/dist/lsp.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { promises as fs } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
function uri(file) {
|
|
6
|
+
return pathToFileURL(file).toString();
|
|
7
|
+
}
|
|
8
|
+
function languageId(file) {
|
|
9
|
+
const ext = path.extname(file).toLowerCase();
|
|
10
|
+
const map = {
|
|
11
|
+
".ts": "typescript", ".tsx": "typescriptreact", ".js": "javascript", ".jsx": "javascriptreact",
|
|
12
|
+
".mts": "typescript", ".cts": "typescript", ".mjs": "javascript", ".cjs": "javascript",
|
|
13
|
+
".py": "python", ".rs": "rust", ".go": "go", ".c": "c", ".h": "c", ".cc": "cpp", ".cpp": "cpp", ".cxx": "cpp", ".hpp": "cpp",
|
|
14
|
+
".java": "java", ".kt": "kotlin", ".kts": "kotlin", ".cs": "csharp", ".php": "php", ".rb": "ruby", ".lua": "lua",
|
|
15
|
+
".swift": "swift", ".dart": "dart", ".ex": "elixir", ".exs": "elixir", ".zig": "zig", ".sol": "solidity",
|
|
16
|
+
};
|
|
17
|
+
return map[ext] ?? (ext.replace(/^\./, "") || "plaintext");
|
|
18
|
+
}
|
|
19
|
+
function inside(root, candidate) {
|
|
20
|
+
const relative = path.relative(root, candidate);
|
|
21
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
22
|
+
}
|
|
23
|
+
export async function commandExists(command) {
|
|
24
|
+
if (path.isAbsolute(command) || command.includes(path.sep)) {
|
|
25
|
+
try {
|
|
26
|
+
await fs.access(command);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const pathEntries = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
|
|
34
|
+
const suffixes = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
|
|
35
|
+
for (const dir of pathEntries) {
|
|
36
|
+
for (const suffix of suffixes) {
|
|
37
|
+
try {
|
|
38
|
+
await fs.access(path.join(dir, `${command}${suffix}`));
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
export async function resolveLspRoot(workspaceRoot, file, spec) {
|
|
47
|
+
const root = path.resolve(workspaceRoot);
|
|
48
|
+
const absolute = path.resolve(file);
|
|
49
|
+
let current = path.dirname(absolute);
|
|
50
|
+
const markers = spec.rootMarkers ?? [];
|
|
51
|
+
if (!markers.length)
|
|
52
|
+
return root;
|
|
53
|
+
while (inside(root, current)) {
|
|
54
|
+
for (const marker of markers) {
|
|
55
|
+
try {
|
|
56
|
+
await fs.access(path.join(current, marker));
|
|
57
|
+
return current;
|
|
58
|
+
}
|
|
59
|
+
catch { }
|
|
60
|
+
}
|
|
61
|
+
if (current === root)
|
|
62
|
+
break;
|
|
63
|
+
const parent = path.dirname(current);
|
|
64
|
+
if (parent === current)
|
|
65
|
+
break;
|
|
66
|
+
current = parent;
|
|
67
|
+
}
|
|
68
|
+
return root;
|
|
69
|
+
}
|
|
70
|
+
export class LspClient {
|
|
71
|
+
root;
|
|
72
|
+
spec;
|
|
73
|
+
child = null;
|
|
74
|
+
sequence = 1;
|
|
75
|
+
buffer = Buffer.alloc(0);
|
|
76
|
+
pending = new Map();
|
|
77
|
+
opened = new Map();
|
|
78
|
+
publishedDiagnostics = new Map();
|
|
79
|
+
initialized = false;
|
|
80
|
+
startPromise = null;
|
|
81
|
+
constructor(root, spec) {
|
|
82
|
+
this.root = root;
|
|
83
|
+
this.spec = spec;
|
|
84
|
+
}
|
|
85
|
+
async available() {
|
|
86
|
+
return commandExists(this.spec.command);
|
|
87
|
+
}
|
|
88
|
+
async start() {
|
|
89
|
+
if (this.initialized && this.child)
|
|
90
|
+
return;
|
|
91
|
+
if (this.startPromise)
|
|
92
|
+
return this.startPromise;
|
|
93
|
+
this.startPromise = this.startInternal().finally(() => { this.startPromise = null; });
|
|
94
|
+
return this.startPromise;
|
|
95
|
+
}
|
|
96
|
+
async startInternal() {
|
|
97
|
+
if (!(await this.available()))
|
|
98
|
+
throw new Error(`${this.spec.id} is not installed.`);
|
|
99
|
+
const child = spawn(this.spec.command, this.spec.args, {
|
|
100
|
+
cwd: this.root,
|
|
101
|
+
env: { ...process.env },
|
|
102
|
+
stdio: "pipe",
|
|
103
|
+
});
|
|
104
|
+
this.child = child;
|
|
105
|
+
this.buffer = Buffer.alloc(0);
|
|
106
|
+
this.opened.clear();
|
|
107
|
+
this.publishedDiagnostics.clear();
|
|
108
|
+
child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
109
|
+
child.stderr.on("data", () => undefined);
|
|
110
|
+
child.on("error", (error) => this.failAll(error));
|
|
111
|
+
child.on("close", () => this.failAll(new Error(`${this.spec.id} exited.`)));
|
|
112
|
+
const rootUri = uri(this.root);
|
|
113
|
+
try {
|
|
114
|
+
await this.request("initialize", {
|
|
115
|
+
processId: process.pid,
|
|
116
|
+
clientInfo: { name: "CodeLocal", version: "1.2" },
|
|
117
|
+
rootUri,
|
|
118
|
+
workspaceFolders: [{ uri: rootUri, name: path.basename(this.root) }],
|
|
119
|
+
capabilities: {
|
|
120
|
+
workspace: { symbol: {}, workspaceFolders: true },
|
|
121
|
+
textDocument: {
|
|
122
|
+
synchronization: { didSave: true, dynamicRegistration: false },
|
|
123
|
+
definition: {}, references: {}, implementation: {}, hover: {}, documentSymbol: {}, publishDiagnostics: {},
|
|
124
|
+
callHierarchy: {},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
initializationOptions: this.spec.initializationOptions,
|
|
128
|
+
}, 25_000);
|
|
129
|
+
this.notify("initialized", {});
|
|
130
|
+
this.initialized = true;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
try {
|
|
134
|
+
child.kill("SIGTERM");
|
|
135
|
+
}
|
|
136
|
+
catch { }
|
|
137
|
+
if (this.child === child)
|
|
138
|
+
this.child = null;
|
|
139
|
+
this.opened.clear();
|
|
140
|
+
this.publishedDiagnostics.clear();
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
failAll(error) {
|
|
145
|
+
for (const [id, pending] of this.pending) {
|
|
146
|
+
clearTimeout(pending.timer);
|
|
147
|
+
pending.reject(error);
|
|
148
|
+
this.pending.delete(id);
|
|
149
|
+
}
|
|
150
|
+
this.initialized = false;
|
|
151
|
+
this.child = null;
|
|
152
|
+
this.opened.clear();
|
|
153
|
+
this.publishedDiagnostics.clear();
|
|
154
|
+
this.buffer = Buffer.alloc(0);
|
|
155
|
+
}
|
|
156
|
+
send(payload) {
|
|
157
|
+
if (!this.child)
|
|
158
|
+
throw new Error(`${this.spec.id} is not running.`);
|
|
159
|
+
const json = JSON.stringify(payload);
|
|
160
|
+
this.child.stdin.write(`Content-Length: ${Buffer.byteLength(json, "utf8")}\r\n\r\n${json}`);
|
|
161
|
+
}
|
|
162
|
+
request(method, params, timeoutMs = 15_000) {
|
|
163
|
+
const id = this.sequence++;
|
|
164
|
+
const promise = new Promise((resolve, reject) => {
|
|
165
|
+
const timer = setTimeout(() => {
|
|
166
|
+
this.pending.delete(id);
|
|
167
|
+
reject(new Error(`${this.spec.id} LSP request timed out: ${method}`));
|
|
168
|
+
}, timeoutMs);
|
|
169
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
170
|
+
});
|
|
171
|
+
this.send({ jsonrpc: "2.0", id, method, params });
|
|
172
|
+
return promise;
|
|
173
|
+
}
|
|
174
|
+
notify(method, params) {
|
|
175
|
+
this.send({ jsonrpc: "2.0", method, params });
|
|
176
|
+
}
|
|
177
|
+
onData(chunk) {
|
|
178
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
179
|
+
while (true) {
|
|
180
|
+
const headerEnd = this.buffer.indexOf("\r\n\r\n");
|
|
181
|
+
if (headerEnd < 0)
|
|
182
|
+
return;
|
|
183
|
+
const header = this.buffer.subarray(0, headerEnd).toString("ascii");
|
|
184
|
+
const match = /Content-Length:\s*(\d+)/i.exec(header);
|
|
185
|
+
if (!match) {
|
|
186
|
+
this.buffer = this.buffer.subarray(headerEnd + 4);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const length = Number(match[1]);
|
|
190
|
+
const bodyStart = headerEnd + 4;
|
|
191
|
+
if (this.buffer.length < bodyStart + length)
|
|
192
|
+
return;
|
|
193
|
+
const body = this.buffer.subarray(bodyStart, bodyStart + length).toString("utf8");
|
|
194
|
+
this.buffer = this.buffer.subarray(bodyStart + length);
|
|
195
|
+
try {
|
|
196
|
+
this.onMessage(JSON.parse(body));
|
|
197
|
+
}
|
|
198
|
+
catch { }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
onMessage(message) {
|
|
202
|
+
if (typeof message?.id === "number" && this.pending.has(message.id)) {
|
|
203
|
+
const pending = this.pending.get(message.id);
|
|
204
|
+
this.pending.delete(message.id);
|
|
205
|
+
clearTimeout(pending.timer);
|
|
206
|
+
if (message.error)
|
|
207
|
+
pending.reject(new Error(message.error.message ?? "LSP error"));
|
|
208
|
+
else
|
|
209
|
+
pending.resolve(message.result);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (message?.method === "textDocument/publishDiagnostics") {
|
|
213
|
+
this.publishedDiagnostics.set(String(message.params?.uri ?? ""), Array.isArray(message.params?.diagnostics) ? message.params.diagnostics : []);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async openDocument(file) {
|
|
217
|
+
await this.start();
|
|
218
|
+
const absolute = path.resolve(file);
|
|
219
|
+
const text = await fs.readFile(absolute, "utf8");
|
|
220
|
+
const version = (this.opened.get(absolute) ?? 0) + 1;
|
|
221
|
+
this.opened.set(absolute, version);
|
|
222
|
+
const textDocument = { uri: uri(absolute), languageId: languageId(absolute), version, text };
|
|
223
|
+
if (version === 1)
|
|
224
|
+
this.notify("textDocument/didOpen", { textDocument });
|
|
225
|
+
else
|
|
226
|
+
this.notify("textDocument/didChange", { textDocument: { uri: textDocument.uri, version }, contentChanges: [{ text }] });
|
|
227
|
+
return { absolute, version };
|
|
228
|
+
}
|
|
229
|
+
async positionRequest(method, file, line, column, extra = {}) {
|
|
230
|
+
await this.openDocument(file);
|
|
231
|
+
return this.request(method, {
|
|
232
|
+
textDocument: { uri: uri(path.resolve(file)) },
|
|
233
|
+
position: { line: Math.max(0, line - 1), character: Math.max(0, column - 1) },
|
|
234
|
+
...extra,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
async workspaceSymbols(query) {
|
|
238
|
+
await this.start();
|
|
239
|
+
return this.request("workspace/symbol", { query });
|
|
240
|
+
}
|
|
241
|
+
async documentSymbols(file) {
|
|
242
|
+
await this.openDocument(file);
|
|
243
|
+
return this.request("textDocument/documentSymbol", { textDocument: { uri: uri(path.resolve(file)) } });
|
|
244
|
+
}
|
|
245
|
+
async diagnostics(file, waitMs = 350) {
|
|
246
|
+
await this.openDocument(file);
|
|
247
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
248
|
+
return this.publishedDiagnostics.get(uri(path.resolve(file))) ?? [];
|
|
249
|
+
}
|
|
250
|
+
async callHierarchy(file, line, column, direction) {
|
|
251
|
+
await this.openDocument(file);
|
|
252
|
+
const textDocument = { uri: uri(path.resolve(file)) };
|
|
253
|
+
const position = { line: Math.max(0, line - 1), character: Math.max(0, column - 1) };
|
|
254
|
+
const items = await this.request("textDocument/prepareCallHierarchy", { textDocument, position }).catch(() => []);
|
|
255
|
+
const prepared = Array.isArray(items) ? items.slice(0, 4) : [];
|
|
256
|
+
if (!prepared.length)
|
|
257
|
+
return [];
|
|
258
|
+
const method = direction === "incoming" ? "callHierarchy/incomingCalls" : "callHierarchy/outgoingCalls";
|
|
259
|
+
const results = await Promise.all(prepared.map((item) => this.request(method, { item }).catch(() => [])));
|
|
260
|
+
return results.flat().filter(Boolean);
|
|
261
|
+
}
|
|
262
|
+
status() {
|
|
263
|
+
return { id: this.spec.id, root: this.root, initialized: this.initialized, pid: this.child?.pid ?? null };
|
|
264
|
+
}
|
|
265
|
+
async stop() {
|
|
266
|
+
const child = this.child;
|
|
267
|
+
if (!child)
|
|
268
|
+
return;
|
|
269
|
+
try {
|
|
270
|
+
await this.request("shutdown", null, 2000);
|
|
271
|
+
}
|
|
272
|
+
catch { }
|
|
273
|
+
try {
|
|
274
|
+
this.notify("exit", null);
|
|
275
|
+
}
|
|
276
|
+
catch { }
|
|
277
|
+
try {
|
|
278
|
+
child.kill("SIGTERM");
|
|
279
|
+
}
|
|
280
|
+
catch { }
|
|
281
|
+
if (this.child === child)
|
|
282
|
+
this.child = null;
|
|
283
|
+
this.initialized = false;
|
|
284
|
+
this.opened.clear();
|
|
285
|
+
this.publishedDiagnostics.clear();
|
|
286
|
+
this.buffer = Buffer.alloc(0);
|
|
287
|
+
}
|
|
288
|
+
}
|