breakpoint-mcp 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/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/bridge.js +189 -0
- package/dist/config.js +71 -0
- package/dist/confirm.js +45 -0
- package/dist/csdap.js +331 -0
- package/dist/cslsp.js +217 -0
- package/dist/dap.js +341 -0
- package/dist/framing.js +125 -0
- package/dist/index.js +110 -0
- package/dist/logger.js +11 -0
- package/dist/lsp.js +219 -0
- package/dist/paths.js +28 -0
- package/dist/schemas.js +587 -0
- package/dist/stdio.js +101 -0
- package/dist/subscriptions.js +141 -0
- package/dist/tasks.js +146 -0
- package/dist/tools/assetgen.js +360 -0
- package/dist/tools/backend.js +532 -0
- package/dist/tools/cli.js +130 -0
- package/dist/tools/csdap.js +377 -0
- package/dist/tools/cslsp.js +285 -0
- package/dist/tools/dap.js +504 -0
- package/dist/tools/editor.js +1919 -0
- package/dist/tools/knowledge.js +517 -0
- package/dist/tools/lsp-common.js +121 -0
- package/dist/tools/lsp.js +576 -0
- package/dist/tools/netcode.js +411 -0
- package/dist/tools/processes.js +103 -0
- package/dist/tools/resources.js +27 -0
- package/dist/tools/runtime.js +125 -0
- package/package.json +57 -0
package/dist/csdap.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { DapError } from "./dap.js";
|
|
3
|
+
/**
|
|
4
|
+
* Minimal DAP client for the C#/.NET debugging plane (D4 C3) — the debugger
|
|
5
|
+
* analogue of the C2 `CsLspClient`. Transport-agnostic: it drives any
|
|
6
|
+
* `JsonRpcChannel`, so production spawns **netcoredbg** (Samsung, MIT) over
|
|
7
|
+
* stdio (`StdioChannel`) while unit tests point it at a loopback TCP mock
|
|
8
|
+
* (`FramedConnection`) — exactly the way the GDScript `DapClient` tests and the
|
|
9
|
+
* `CsLspClient` tests do.
|
|
10
|
+
*
|
|
11
|
+
* It is deliberately a sibling of `DapClient` rather than a shared base class —
|
|
12
|
+
* matching the codebase's one-client-per-protocol precedent (dap.ts / lsp.ts,
|
|
13
|
+
* and now cslsp.ts) — but reuses `DapError` / `DapState` and the framing
|
|
14
|
+
* primitives so the protocol plumbing isn't re-invented. The only C#-specific
|
|
15
|
+
* behaviors are the `coreclr` adapterID and pointing launch/attach at a .NET
|
|
16
|
+
* program/process; everything else is standard DAP the same way Godot's built-in
|
|
17
|
+
* debug adapter is. On top of read/inspect + a gated `setVariable`, it carries the
|
|
18
|
+
* GDScript extras netcoredbg actually backs — persistent watches and a `restart()`
|
|
19
|
+
* (terminate + relaunch, since netcoredbg advertises no `supportsRestartRequest`).
|
|
20
|
+
* The exception-breakpoint extra needs no client method (the tool drives `request`
|
|
21
|
+
* + capabilities directly). `goto` / data breakpoints are intentionally NOT ported:
|
|
22
|
+
* netcoredbg advertises neither `supportsGotoTargetsRequest` nor
|
|
23
|
+
* `supportsDataBreakpoints`, so those tools would be dead surface here.
|
|
24
|
+
*/
|
|
25
|
+
export class CsDapClient extends EventEmitter {
|
|
26
|
+
channel;
|
|
27
|
+
timeoutMs;
|
|
28
|
+
seq = 1;
|
|
29
|
+
pending = new Map();
|
|
30
|
+
breakpoints = new Map();
|
|
31
|
+
configured = false;
|
|
32
|
+
/** Persistent watch expressions, re-evaluated at each stop (see evaluateWatches). */
|
|
33
|
+
watches = [];
|
|
34
|
+
lastStartMode = null;
|
|
35
|
+
lastStartArgs = null;
|
|
36
|
+
capabilities = null;
|
|
37
|
+
state = "disconnected";
|
|
38
|
+
lastStoppedThreadId = null;
|
|
39
|
+
lastStoppedReason = null;
|
|
40
|
+
constructor(channel, timeoutMs) {
|
|
41
|
+
super();
|
|
42
|
+
this.channel = channel;
|
|
43
|
+
this.timeoutMs = timeoutMs;
|
|
44
|
+
this.channel.onMessage((m) => this.onMessage(m));
|
|
45
|
+
this.channel.onClose(() => {
|
|
46
|
+
this.state = "terminated";
|
|
47
|
+
this.configured = false;
|
|
48
|
+
for (const [, p] of this.pending) {
|
|
49
|
+
clearTimeout(p.timer);
|
|
50
|
+
p.reject(new DapError(p.command, "C# DAP connection closed"));
|
|
51
|
+
}
|
|
52
|
+
this.pending.clear();
|
|
53
|
+
this.emit("closed");
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
onMessage(msg) {
|
|
57
|
+
const type = msg["type"];
|
|
58
|
+
if (type === "response") {
|
|
59
|
+
const reqSeq = msg["request_seq"];
|
|
60
|
+
const p = this.pending.get(reqSeq);
|
|
61
|
+
if (!p)
|
|
62
|
+
return;
|
|
63
|
+
this.pending.delete(reqSeq);
|
|
64
|
+
clearTimeout(p.timer);
|
|
65
|
+
if (msg["success"]) {
|
|
66
|
+
p.resolve((msg["body"] ?? {}));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
p.reject(new DapError(String(msg["command"] ?? p.command), String(msg["message"] ?? "C# DAP request failed")));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (type === "event") {
|
|
73
|
+
this.onEvent(String(msg["event"]), (msg["body"] ?? {}));
|
|
74
|
+
}
|
|
75
|
+
else if (type === "request") {
|
|
76
|
+
// Reverse request (e.g. runInTerminal). Ack success so the adapter never stalls.
|
|
77
|
+
void this.channel.send({
|
|
78
|
+
seq: this.seq++,
|
|
79
|
+
type: "response",
|
|
80
|
+
request_seq: msg["seq"],
|
|
81
|
+
success: true,
|
|
82
|
+
command: msg["command"],
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
onEvent(event, body) {
|
|
87
|
+
switch (event) {
|
|
88
|
+
case "initialized":
|
|
89
|
+
this.emit("initialized");
|
|
90
|
+
break;
|
|
91
|
+
case "stopped":
|
|
92
|
+
this.state = "stopped";
|
|
93
|
+
this.lastStoppedThreadId = body["threadId"] ?? this.lastStoppedThreadId ?? 1;
|
|
94
|
+
this.lastStoppedReason = body["reason"] ?? null;
|
|
95
|
+
this.emit("stopped", body);
|
|
96
|
+
break;
|
|
97
|
+
case "continued":
|
|
98
|
+
this.state = "running";
|
|
99
|
+
break;
|
|
100
|
+
case "terminated":
|
|
101
|
+
case "exited":
|
|
102
|
+
this.state = "terminated";
|
|
103
|
+
this.emit("terminated", body);
|
|
104
|
+
break;
|
|
105
|
+
case "output":
|
|
106
|
+
this.emit("output", body);
|
|
107
|
+
break;
|
|
108
|
+
default:
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
request(command, args = {}, timeoutMs = this.timeoutMs) {
|
|
113
|
+
const seq = this.seq++;
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
const timer = setTimeout(() => {
|
|
116
|
+
this.pending.delete(seq);
|
|
117
|
+
reject(new DapError(command, `C# DAP '${command}' timed out after ${timeoutMs}ms`));
|
|
118
|
+
}, timeoutMs);
|
|
119
|
+
this.pending.set(seq, { command, resolve: resolve, reject, timer });
|
|
120
|
+
this.channel.send({ seq, type: "request", command, arguments: args }).catch((err) => {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
this.pending.delete(seq);
|
|
123
|
+
reject(err);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
waitEvent(name, timeoutMs) {
|
|
128
|
+
return new Promise((resolve) => {
|
|
129
|
+
const timer = setTimeout(() => {
|
|
130
|
+
this.removeListener(name, onEvent);
|
|
131
|
+
resolve();
|
|
132
|
+
}, timeoutMs);
|
|
133
|
+
const onEvent = () => {
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
resolve();
|
|
136
|
+
};
|
|
137
|
+
this.once(name, onEvent);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/** Store breakpoints; apply immediately if the session is already configured, else buffer until launch/attach. */
|
|
141
|
+
async setBreakpoints(path, lines, conditions) {
|
|
142
|
+
this.breakpoints.set(path, { path, lines, conditions });
|
|
143
|
+
if (this.configured)
|
|
144
|
+
return this.applyBreakpoints(path);
|
|
145
|
+
return { buffered: true, path, lines };
|
|
146
|
+
}
|
|
147
|
+
applyBreakpoints(path) {
|
|
148
|
+
const bp = this.breakpoints.get(path);
|
|
149
|
+
if (!bp)
|
|
150
|
+
return Promise.resolve({});
|
|
151
|
+
return this.request("setBreakpoints", {
|
|
152
|
+
source: { path },
|
|
153
|
+
breakpoints: bp.lines.map((line, i) => {
|
|
154
|
+
const b = { line };
|
|
155
|
+
const condition = bp.conditions?.[i];
|
|
156
|
+
if (condition)
|
|
157
|
+
b.condition = condition;
|
|
158
|
+
return b;
|
|
159
|
+
}),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
// ---- Watch expressions ---------------------------------------------------
|
|
163
|
+
/** Add expressions to the persistent watch set (deduped, order-preserving). */
|
|
164
|
+
addWatches(expressions) {
|
|
165
|
+
for (const e of expressions)
|
|
166
|
+
if (e && !this.watches.includes(e))
|
|
167
|
+
this.watches.push(e);
|
|
168
|
+
}
|
|
169
|
+
/** Remove specific expressions from the watch set. */
|
|
170
|
+
removeWatches(expressions) {
|
|
171
|
+
const drop = new Set(expressions);
|
|
172
|
+
this.watches = this.watches.filter((e) => !drop.has(e));
|
|
173
|
+
}
|
|
174
|
+
/** Clear all watch expressions. */
|
|
175
|
+
clearWatches() {
|
|
176
|
+
this.watches = [];
|
|
177
|
+
}
|
|
178
|
+
/** The current watch set (a copy). */
|
|
179
|
+
listWatches() {
|
|
180
|
+
return [...this.watches];
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Evaluate every watch expression in the context of a stopped frame and return
|
|
184
|
+
* the results. Each is evaluated with DAP `context: "watch"` (the side-effect-free
|
|
185
|
+
* context IDEs use for watch panels). A single bad expression yields an `error` on
|
|
186
|
+
* that entry instead of failing the whole call. `timeoutMs` bounds each individual
|
|
187
|
+
* `evaluate` (callers pass the shorter `csDapEvaluateTimeoutMs`) so a watch the
|
|
188
|
+
* adapter never answers fails fast on that entry rather than hanging the full DAP
|
|
189
|
+
* timeout at every stop — mirroring `cs_dbg_evaluate` and the GDScript `DapClient`.
|
|
190
|
+
*/
|
|
191
|
+
async evaluateWatches(frameId, timeoutMs = this.timeoutMs) {
|
|
192
|
+
const results = [];
|
|
193
|
+
for (const expression of this.watches) {
|
|
194
|
+
try {
|
|
195
|
+
const body = await this.request("evaluate", { expression, frameId, context: "watch" }, timeoutMs);
|
|
196
|
+
results.push({ expression, value: String(body["result"] ?? ""), type: String(body["type"] ?? ""), error: null });
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
const e = err;
|
|
200
|
+
results.push({ expression, value: "", type: "", error: e.message ?? String(err) });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return results;
|
|
204
|
+
}
|
|
205
|
+
async applyAllBreakpoints() {
|
|
206
|
+
for (const path of this.breakpoints.keys()) {
|
|
207
|
+
await this.applyBreakpoints(path).catch(() => undefined);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** Full handshake: initialize → launch/attach → (breakpoints) → configurationDone. */
|
|
211
|
+
async start(mode, args) {
|
|
212
|
+
this.lastStartMode = mode;
|
|
213
|
+
this.lastStartArgs = args;
|
|
214
|
+
// Listen for `initialized` before we ask, so we cannot miss it.
|
|
215
|
+
const onInit = this.waitEvent("initialized", Math.min(this.timeoutMs, 5000));
|
|
216
|
+
this.capabilities = await this.request("initialize", {
|
|
217
|
+
clientID: "breakpoint-mcp",
|
|
218
|
+
clientName: "Godot Breakpoint MCP",
|
|
219
|
+
adapterID: "coreclr",
|
|
220
|
+
pathFormat: "path",
|
|
221
|
+
linesStartAt1: true,
|
|
222
|
+
columnsStartAt1: true,
|
|
223
|
+
supportsRunInTerminalRequest: false,
|
|
224
|
+
});
|
|
225
|
+
this.state = "initialized";
|
|
226
|
+
// Send launch/attach but don't await it yet — many adapters (netcoredbg included)
|
|
227
|
+
// only resolve it after configurationDone.
|
|
228
|
+
const startReq = this.request(mode, args);
|
|
229
|
+
await onInit;
|
|
230
|
+
await this.applyAllBreakpoints();
|
|
231
|
+
await this.request("configurationDone", {}).catch(() => undefined);
|
|
232
|
+
this.configured = true;
|
|
233
|
+
this.state = "running";
|
|
234
|
+
// Surface a launch/attach failure if one occurs, but don't hang on success.
|
|
235
|
+
startReq.catch((err) => this.emit("error", err));
|
|
236
|
+
}
|
|
237
|
+
threadId() {
|
|
238
|
+
return this.lastStoppedThreadId ?? 1;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Issue a resume command (continue / next / stepIn / stepOut) and wait for the
|
|
242
|
+
* program to settle again — the next `stopped` (hit a breakpoint / step landed)
|
|
243
|
+
* or `terminated` event — before returning. The stop listener is armed BEFORE
|
|
244
|
+
* the command is sent so a fast stop can't be missed. If nothing settles within
|
|
245
|
+
* `waitMs` (e.g. `continue` runs on with no further breakpoint), it resolves with
|
|
246
|
+
* the current state ("running").
|
|
247
|
+
*/
|
|
248
|
+
async resume(command, args, waitMs) {
|
|
249
|
+
const settled = new Promise((resolve) => {
|
|
250
|
+
const finish = () => {
|
|
251
|
+
clearTimeout(timer);
|
|
252
|
+
this.removeListener("stopped", onStop);
|
|
253
|
+
this.removeListener("terminated", onTerm);
|
|
254
|
+
resolve({ state: this.state, reason: this.lastStoppedReason });
|
|
255
|
+
};
|
|
256
|
+
const onStop = () => finish();
|
|
257
|
+
const onTerm = () => finish();
|
|
258
|
+
const timer = setTimeout(() => {
|
|
259
|
+
this.removeListener("stopped", onStop);
|
|
260
|
+
this.removeListener("terminated", onTerm);
|
|
261
|
+
resolve({ state: this.state, reason: this.lastStoppedReason });
|
|
262
|
+
}, waitMs);
|
|
263
|
+
this.once("stopped", onStop);
|
|
264
|
+
this.once("terminated", onTerm);
|
|
265
|
+
});
|
|
266
|
+
// Optimistically mark running so a stale "stopped" isn't reported back.
|
|
267
|
+
this.state = "running";
|
|
268
|
+
await this.request(command, args);
|
|
269
|
+
return settled;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Resolve when the program next settles (`stopped`/`terminated`) or `waitMs`
|
|
273
|
+
* elapses — the shared wait used after a restart. Listeners are armed by the
|
|
274
|
+
* caller BEFORE the triggering request is sent so a fast settle can't be missed.
|
|
275
|
+
*/
|
|
276
|
+
settle(waitMs) {
|
|
277
|
+
return new Promise((resolve) => {
|
|
278
|
+
const finish = () => {
|
|
279
|
+
clearTimeout(timer);
|
|
280
|
+
this.removeListener("stopped", onStop);
|
|
281
|
+
this.removeListener("terminated", onTerm);
|
|
282
|
+
resolve({ state: this.state, reason: this.lastStoppedReason });
|
|
283
|
+
};
|
|
284
|
+
const onStop = () => finish();
|
|
285
|
+
const onTerm = () => finish();
|
|
286
|
+
const timer = setTimeout(() => {
|
|
287
|
+
this.removeListener("stopped", onStop);
|
|
288
|
+
this.removeListener("terminated", onTerm);
|
|
289
|
+
resolve({ state: this.state, reason: this.lastStoppedReason });
|
|
290
|
+
}, waitMs);
|
|
291
|
+
this.once("stopped", onStop);
|
|
292
|
+
this.once("terminated", onTerm);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Restart the debug session. If the adapter advertises `supportsRestartRequest`,
|
|
297
|
+
* issue a single DAP `restart` (carrying the launch/attach args); otherwise fall
|
|
298
|
+
* back to `terminate` + a fresh handshake, so restart works on every adapter.
|
|
299
|
+
* netcoredbg advertises no `supportsRestartRequest`, so in practice the relaunch
|
|
300
|
+
* path runs. Reuses the last cs_dbg_launch/cs_dbg_attach params; `overrideArgs`
|
|
301
|
+
* (e.g. a new `stopAtEntry`) are merged over them. `method` tells the caller which
|
|
302
|
+
* path ran. C# sessions have no scene, so — unlike the GDScript `DapClient` — this
|
|
303
|
+
* returns no `scene`.
|
|
304
|
+
*/
|
|
305
|
+
async restart(overrideArgs = {}, waitMs = 15000) {
|
|
306
|
+
if (!this.lastStartMode || !this.lastStartArgs) {
|
|
307
|
+
throw new DapError("restart", "no C# debug session to restart — call cs_dbg_launch or cs_dbg_attach first");
|
|
308
|
+
}
|
|
309
|
+
const args = { ...this.lastStartArgs, ...overrideArgs };
|
|
310
|
+
if (this.capabilities?.["supportsRestartRequest"] === true) {
|
|
311
|
+
// Arm the settle listener before issuing restart so a fast stop isn't missed.
|
|
312
|
+
const settled = this.settle(waitMs);
|
|
313
|
+
this.state = "running";
|
|
314
|
+
await this.request("restart", { arguments: args });
|
|
315
|
+
this.lastStartArgs = args;
|
|
316
|
+
const r = await settled;
|
|
317
|
+
return { method: "restart", state: r.state, reason: r.reason };
|
|
318
|
+
}
|
|
319
|
+
// Fallback: ask the debuggee to terminate (best-effort), then re-run the full
|
|
320
|
+
// initialize → launch/attach → configurationDone handshake with the same args.
|
|
321
|
+
await this.request("terminate", {}).catch(() => undefined);
|
|
322
|
+
await this.start(this.lastStartMode, args);
|
|
323
|
+
return { method: "relaunch", state: this.state, reason: this.lastStoppedReason };
|
|
324
|
+
}
|
|
325
|
+
close() {
|
|
326
|
+
this.channel.close();
|
|
327
|
+
this.state = "disconnected";
|
|
328
|
+
this.configured = false;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
//# sourceMappingURL=csdap.js.map
|
package/dist/cslsp.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { LspError } from "./lsp.js";
|
|
3
|
+
/**
|
|
4
|
+
* Minimal LSP client for the C#/.NET semantic plane (D4 C2). Transport-agnostic:
|
|
5
|
+
* it drives any `JsonRpcChannel`, so production spawns OmniSharp over stdio
|
|
6
|
+
* (`StdioChannel`) while unit tests point it at a loopback TCP mock (`FramedConnection`)
|
|
7
|
+
* exactly as the GDScript `LspClient` tests do.
|
|
8
|
+
*
|
|
9
|
+
* It is deliberately a sibling of `LspClient` rather than a shared base class —
|
|
10
|
+
* matching the codebase's one-client-per-protocol precedent (lsp.ts / dap.ts) —
|
|
11
|
+
* but reuses `LspError`, `Diagnostic`, and the framing primitives so the protocol
|
|
12
|
+
* plumbing isn't re-invented. The only C#-specific behaviors are the `csharp`
|
|
13
|
+
* `languageId` on didOpen and pointing at the C# project root; everything else is
|
|
14
|
+
* standard LSP the same way Godot's GDScript server is.
|
|
15
|
+
*/
|
|
16
|
+
export class CsLspClient {
|
|
17
|
+
channel;
|
|
18
|
+
rootUri;
|
|
19
|
+
timeoutMs;
|
|
20
|
+
nextId = 1;
|
|
21
|
+
pending = new Map();
|
|
22
|
+
initialized = null;
|
|
23
|
+
serverCapabilities = null;
|
|
24
|
+
opened = new Set();
|
|
25
|
+
diagnostics = new Map();
|
|
26
|
+
diagWaiters = new Map();
|
|
27
|
+
/** Absolute project root path (no trailing slash), used to canonicalize URIs. */
|
|
28
|
+
rootFsPath;
|
|
29
|
+
constructor(channel, rootUri, timeoutMs) {
|
|
30
|
+
this.channel = channel;
|
|
31
|
+
this.rootUri = rootUri;
|
|
32
|
+
this.timeoutMs = timeoutMs;
|
|
33
|
+
let root = "";
|
|
34
|
+
try {
|
|
35
|
+
root = fileURLToPath(rootUri);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
root = rootUri.replace(/^file:\/\//, "");
|
|
39
|
+
}
|
|
40
|
+
this.rootFsPath = root.replace(/[\\/]+$/, "");
|
|
41
|
+
this.channel.onMessage((m) => this.onMessage(m));
|
|
42
|
+
this.channel.onClose(() => this.onClose());
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Reduce any document URI to a stable, project-relative key (e.g. "Player.cs")
|
|
46
|
+
* so a published-diagnostics URI matches the one we opened the file with,
|
|
47
|
+
* regardless of how the server spells it (percent-encoded vs literal `file://`).
|
|
48
|
+
* Mirrors LspClient.diagKey; OmniSharp uses `file://` URIs, but keeping the
|
|
49
|
+
* `res://` branch costs nothing and keeps the two clients uniform.
|
|
50
|
+
*/
|
|
51
|
+
diagKey(uri) {
|
|
52
|
+
let s = uri;
|
|
53
|
+
try {
|
|
54
|
+
s = decodeURIComponent(uri);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
/* keep raw on malformed encoding */
|
|
58
|
+
}
|
|
59
|
+
if (s.startsWith("res://"))
|
|
60
|
+
return s.slice("res://".length).replace(/^[\\/]+/, "");
|
|
61
|
+
if (s.startsWith("file://"))
|
|
62
|
+
s = s.slice("file://".length);
|
|
63
|
+
if (this.rootFsPath && s.startsWith(this.rootFsPath))
|
|
64
|
+
s = s.slice(this.rootFsPath.length);
|
|
65
|
+
return s.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
66
|
+
}
|
|
67
|
+
onMessage(msg) {
|
|
68
|
+
const id = msg["id"];
|
|
69
|
+
const method = msg["method"];
|
|
70
|
+
// Response to one of our requests.
|
|
71
|
+
if (typeof id === "number" && method === undefined) {
|
|
72
|
+
const p = this.pending.get(id);
|
|
73
|
+
if (!p)
|
|
74
|
+
return;
|
|
75
|
+
this.pending.delete(id);
|
|
76
|
+
clearTimeout(p.timer);
|
|
77
|
+
if (msg["error"]) {
|
|
78
|
+
const e = msg["error"];
|
|
79
|
+
p.reject(new LspError(e.code ?? -1, e.message ?? "LSP error"));
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
p.resolve(msg["result"] ?? null);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Server -> client notification.
|
|
87
|
+
if (typeof method === "string" && id === undefined) {
|
|
88
|
+
if (method === "textDocument/publishDiagnostics") {
|
|
89
|
+
const params = (msg["params"] ?? {});
|
|
90
|
+
const uri = params.uri ?? "";
|
|
91
|
+
const diags = (params.diagnostics ?? []).map((d) => {
|
|
92
|
+
const dd = d;
|
|
93
|
+
return {
|
|
94
|
+
severity: dd.severity ?? 1,
|
|
95
|
+
message: dd.message ?? "",
|
|
96
|
+
line: dd.range?.start?.line ?? 0,
|
|
97
|
+
character: dd.range?.start?.character ?? 0,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
const key = this.diagKey(uri);
|
|
101
|
+
this.diagnostics.set(key, diags);
|
|
102
|
+
const waiters = this.diagWaiters.get(key);
|
|
103
|
+
if (waiters) {
|
|
104
|
+
this.diagWaiters.delete(key);
|
|
105
|
+
for (const w of waiters)
|
|
106
|
+
w();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Server -> client request (e.g. client/registerCapability, or OmniSharp's
|
|
112
|
+
// window/workDoneProgress/create): ack with null so the server never blocks
|
|
113
|
+
// waiting for us.
|
|
114
|
+
if (typeof method === "string" && typeof id === "number") {
|
|
115
|
+
void this.channel.send({ jsonrpc: "2.0", id, result: null });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
onClose() {
|
|
119
|
+
for (const [, p] of this.pending) {
|
|
120
|
+
clearTimeout(p.timer);
|
|
121
|
+
p.reject(new LspError("closed", "C# LSP connection closed"));
|
|
122
|
+
}
|
|
123
|
+
this.pending.clear();
|
|
124
|
+
this.initialized = null;
|
|
125
|
+
this.serverCapabilities = null;
|
|
126
|
+
this.opened.clear();
|
|
127
|
+
}
|
|
128
|
+
rawRequest(method, params, timeoutMs = this.timeoutMs) {
|
|
129
|
+
const id = this.nextId++;
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const timer = setTimeout(() => {
|
|
132
|
+
this.pending.delete(id);
|
|
133
|
+
reject(new LspError("timeout", `C# LSP '${method}' timed out after ${timeoutMs}ms`));
|
|
134
|
+
}, timeoutMs);
|
|
135
|
+
this.pending.set(id, { resolve: resolve, reject, timer });
|
|
136
|
+
this.channel.send({ jsonrpc: "2.0", id, method, params }).catch((err) => {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
this.pending.delete(id);
|
|
139
|
+
reject(err);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
notify(method, params) {
|
|
144
|
+
return this.channel.send({ jsonrpc: "2.0", method, params });
|
|
145
|
+
}
|
|
146
|
+
ensureInitialized() {
|
|
147
|
+
if (!this.initialized) {
|
|
148
|
+
this.initialized = (async () => {
|
|
149
|
+
const result = (await this.rawRequest("initialize", {
|
|
150
|
+
processId: process.pid,
|
|
151
|
+
rootUri: this.rootUri,
|
|
152
|
+
rootPath: decodeURIComponent(this.rootUri.replace(/^file:\/\//, "")),
|
|
153
|
+
capabilities: {
|
|
154
|
+
textDocument: {
|
|
155
|
+
synchronization: { didSave: true, dynamicRegistration: false },
|
|
156
|
+
completion: { completionItem: { snippetSupport: false } },
|
|
157
|
+
hover: { contentFormat: ["plaintext", "markdown"] },
|
|
158
|
+
definition: {},
|
|
159
|
+
references: {},
|
|
160
|
+
documentSymbol: { hierarchicalDocumentSymbolSupport: true },
|
|
161
|
+
signatureHelp: {},
|
|
162
|
+
publishDiagnostics: {},
|
|
163
|
+
},
|
|
164
|
+
workspace: { symbol: {}, workspaceFolders: true },
|
|
165
|
+
},
|
|
166
|
+
workspaceFolders: [{ uri: this.rootUri, name: "csharp-project" }],
|
|
167
|
+
clientInfo: { name: "breakpoint-mcp", version: "0.2.0" },
|
|
168
|
+
}));
|
|
169
|
+
this.serverCapabilities = result?.capabilities ?? {};
|
|
170
|
+
await this.notify("initialized", {});
|
|
171
|
+
return result;
|
|
172
|
+
})();
|
|
173
|
+
}
|
|
174
|
+
return this.initialized;
|
|
175
|
+
}
|
|
176
|
+
async request(method, params, timeoutMs) {
|
|
177
|
+
await this.ensureInitialized();
|
|
178
|
+
return this.rawRequest(method, params, timeoutMs);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The server's advertised capabilities from the `initialize` handshake (an
|
|
182
|
+
* empty object if none). Lets a cs_* tool feature-detect an optional LSP method
|
|
183
|
+
* before calling it, instead of surfacing a raw `-32601 Method not found`.
|
|
184
|
+
*/
|
|
185
|
+
async getServerCapabilities() {
|
|
186
|
+
await this.ensureInitialized();
|
|
187
|
+
return this.serverCapabilities ?? {};
|
|
188
|
+
}
|
|
189
|
+
async ensureOpen(uri, text) {
|
|
190
|
+
if (this.opened.has(uri))
|
|
191
|
+
return;
|
|
192
|
+
await this.ensureInitialized();
|
|
193
|
+
this.opened.add(uri);
|
|
194
|
+
await this.notify("textDocument/didOpen", {
|
|
195
|
+
textDocument: { uri, languageId: "csharp", version: 1, text },
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/** Return cached diagnostics for a URI, waiting up to timeoutMs for the first publish. */
|
|
199
|
+
waitForDiagnostics(uri, timeoutMs) {
|
|
200
|
+
const key = this.diagKey(uri);
|
|
201
|
+
if (this.diagnostics.has(key))
|
|
202
|
+
return Promise.resolve(this.diagnostics.get(key));
|
|
203
|
+
return new Promise((resolve) => {
|
|
204
|
+
const timer = setTimeout(() => resolve(this.diagnostics.get(key) ?? []), timeoutMs);
|
|
205
|
+
const arr = this.diagWaiters.get(key) ?? [];
|
|
206
|
+
arr.push(() => {
|
|
207
|
+
clearTimeout(timer);
|
|
208
|
+
resolve(this.diagnostics.get(key) ?? []);
|
|
209
|
+
});
|
|
210
|
+
this.diagWaiters.set(key, arr);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
close() {
|
|
214
|
+
this.channel.close();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=cslsp.js.map
|