Skip to content

Commit 350d319

Browse files
committed
fix: typing and launch things
1 parent 05cabd3 commit 350d319

5 files changed

Lines changed: 146 additions & 91 deletions

File tree

src/cm/lsp/diagnostics.ts

Lines changed: 7 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
} from "@codemirror/state";
1111
import type { EditorView } from "@codemirror/view";
1212
import type {
13+
LSPClientWithWorkspace,
14+
LSPPluginAPI,
1315
LspDiagnostic,
1416
PublishDiagnosticsParams,
1517
RawDiagnostic,
@@ -70,24 +72,8 @@ const severities: DiagnosticSeverity[] = [
7072
"hint",
7173
];
7274

73-
interface LSPPluginInstance {
74-
fromPosition: (
75-
pos: { line: number; character: number },
76-
doc: unknown,
77-
) => number;
78-
syncedDoc: { length: number };
79-
unsyncedChanges: {
80-
mapPos: (pos: number, assoc?: number, mode?: MapMode) => number | null;
81-
empty: boolean;
82-
};
83-
client: {
84-
sync: () => void;
85-
};
86-
clear: () => void;
87-
}
88-
8975
function storeLspDiagnostics(
90-
plugin: LSPPluginInstance,
76+
plugin: LSPPluginAPI,
9177
diagnostics: RawDiagnostic[],
9278
): StateEffect<LspDiagnostic[]> {
9379
const items: LspDiagnostic[] = [];
@@ -133,7 +119,7 @@ function storeLspDiagnostics(
133119
}
134120

135121
function mapDiagnostics(
136-
plugin: LSPPluginInstance,
122+
plugin: LSPPluginAPI,
137123
state: EditorState,
138124
): Diagnostic[] {
139125
plugin.client.sync();
@@ -159,24 +145,11 @@ function mapDiagnostics(
159145
}
160146

161147
function lspLinterSource(view: EditorView): Diagnostic[] {
162-
const plugin = LSPPlugin.get(view) as LSPPluginInstance | null;
148+
const plugin = LSPPlugin.get(view) as LSPPluginAPI | null;
163149
if (!plugin) return [];
164150
return mapDiagnostics(plugin, view.state);
165151
}
166152

167-
interface WorkspaceFile {
168-
version: number;
169-
getView: () => EditorView | null;
170-
}
171-
172-
interface WorkspaceWithGetFile {
173-
getFile: (uri: string) => WorkspaceFile | null;
174-
}
175-
176-
interface LSPClientWithWorkspace {
177-
workspace: WorkspaceWithGetFile;
178-
}
179-
180153
export function lspDiagnosticsClientExtension(): {
181154
clientCapabilities: Record<string, unknown>;
182155
notificationHandlers: Record<
@@ -200,7 +173,7 @@ export function lspDiagnosticsClientExtension(): {
200173
client: LSPClient,
201174
params: PublishDiagnosticsParams,
202175
): boolean => {
203-
const clientWithWorkspace = client as LSPClientWithWorkspace;
176+
const clientWithWorkspace = client as unknown as LSPClientWithWorkspace;
204177
const file = clientWithWorkspace.workspace.getFile(params.uri);
205178
if (
206179
!file ||
@@ -210,7 +183,7 @@ export function lspDiagnosticsClientExtension(): {
210183
}
211184
const view = file.getView();
212185
if (!view) return false;
213-
const plugin = LSPPlugin.get(view) as LSPPluginInstance | null;
186+
const plugin = LSPPlugin.get(view) as LSPPluginAPI | null;
214187
if (!plugin) return false;
215188

216189
view.dispatch({

src/cm/lsp/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@ export type {
2626
BuiltinExtensionsConfig,
2727
ClientManagerOptions,
2828
ClientState,
29+
DiagnosticRelatedInformation,
2930
FileMetadata,
3031
FormattingOptions,
32+
LSPClientWithWorkspace,
3133
LSPDiagnostic,
3234
LSPFormattingOptions,
35+
LSPPluginAPI,
3336
LspDiagnostic,
3437
LspServerDefinition,
3538
Position,

src/cm/lsp/inlayHints.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type {
2121
InlayHintLabelPart,
2222
Position,
2323
} from "vscode-languageserver-types";
24+
import type { LSPPluginAPI } from "./types";
2425

2526
// ============================================================================
2627
// Types
@@ -131,15 +132,6 @@ function buildDecos(hints: ProcessedHint[], docLen: number): DecorationSet {
131132
// Plugin
132133
// ============================================================================
133134

134-
interface PluginAPI {
135-
uri: string;
136-
client: LSPClient;
137-
toPosition: (offset: number) => Position;
138-
fromPosition: (pos: Position, doc?: unknown) => number;
139-
syncedDoc: { length: number };
140-
unsyncedChanges: { mapPos: (pos: number) => number | null };
141-
}
142-
143135
function createPlugin(config: InlayHintsConfig) {
144136
const delay = config.debounceMs ?? 200;
145137
const max = config.maxHints ?? 500;
@@ -179,7 +171,7 @@ function createPlugin(config: InlayHintsConfig) {
179171
}
180172

181173
async fetch(): Promise<void> {
182-
const lsp = LSPPlugin.get(this.view) as PluginAPI | null;
174+
const lsp = LSPPlugin.get(this.view) as LSPPluginAPI | null;
183175
if (!lsp?.client.connected) return;
184176

185177
const caps = lsp.client.serverCapabilities;
@@ -220,7 +212,7 @@ function createPlugin(config: InlayHintsConfig) {
220212
}
221213

222214
process(
223-
lsp: PluginAPI,
215+
lsp: LSPPluginAPI,
224216
hints: InlayHint[],
225217
docLen: number,
226218
): ProcessedHint[] {

src/cm/lsp/serverLauncher.ts

Lines changed: 58 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,10 @@ async function startInteractiveServer(
202202
console.warn(`[${serverId}] ${data}`);
203203
} else if (type === "stdout" && data && data.trim()) {
204204
console.info(`[${serverId}] ${data}`);
205+
// Detect when the axs proxy signals it's listening
206+
if (/listening on/i.test(data)) {
207+
signalServerReady(serverId);
208+
}
205209
}
206210
};
207211
const uuid = await executor.start(command, callback, true);
@@ -217,59 +221,68 @@ function sleep(ms: number): Promise<void> {
217221
return new Promise((resolve) => setTimeout(resolve, ms));
218222
}
219223

224+
/**
225+
* Tracks servers that have signaled they're ready (listening)
226+
* Key: serverId, Value: timestamp when ready
227+
*/
228+
const serverReadySignals = new Map<string, number>();
229+
230+
/**
231+
* Called when stdout contains a "listening" message from the axs proxy.
232+
* This signals that the server is ready to accept connections.
233+
*/
234+
export function signalServerReady(serverId: string): void {
235+
serverReadySignals.set(serverId, Date.now());
236+
}
237+
238+
/**
239+
* Wait for the LSP server to be ready.
240+
*
241+
* This function polls for a ready signal (set when stdout contains "listening")
242+
*/
220243
async function waitForWebSocket(
221244
url: string,
222245
options: WaitOptions = {},
223246
): Promise<void> {
224-
const { attempts = 20, delay = 200, probeTimeout = 2000 } = options;
247+
const {
248+
delay = 100, // Poll interval
249+
probeTimeout = 5000, // Max wait time
250+
} = options;
251+
252+
// Extract server ID from URL (e.g., "ws://127.0.0.1:2090" -> check by port)
253+
const portMatch = url.match(/:(\d+)/);
254+
const port = portMatch ? portMatch[1] : null;
255+
256+
// Find the server ID that's starting on this port
257+
let targetServerId: string | null = null;
258+
const entries = Array.from(managedServers.entries());
259+
for (const [serverId, entry] of entries) {
260+
if (
261+
entry.command.includes(`--port ${port}`) ||
262+
entry.command.includes(`:${port}`)
263+
) {
264+
targetServerId = serverId;
265+
break;
266+
}
267+
}
225268

226-
let lastError: Error | null = null;
227-
for (let i = 0; i < attempts; i++) {
228-
try {
229-
await new Promise<void>((resolve, reject) => {
230-
let socket: WebSocket | null = null;
231-
let timer: ReturnType<typeof setTimeout> | null = null;
232-
try {
233-
socket = new WebSocket(url);
234-
} catch (error) {
235-
reject(error);
236-
return;
237-
}
238-
239-
const cleanup = (cb?: () => void): void => {
240-
if (timer) clearTimeout(timer);
241-
if (socket) {
242-
socket.onopen = null;
243-
socket.onerror = null;
244-
try {
245-
socket.close();
246-
} catch (_) {
247-
// Ignore close errors
248-
}
249-
}
250-
if (cb) cb();
251-
};
252-
253-
socket.onopen = () => cleanup(resolve);
254-
socket.onerror = (event: Event) =>
255-
cleanup(() =>
256-
reject(
257-
event instanceof Error ? event : new Error("websocket error"),
258-
),
259-
);
260-
timer = setTimeout(
261-
() => cleanup(() => reject(new Error("timeout"))),
262-
probeTimeout,
263-
);
264-
});
269+
const deadline = Date.now() + probeTimeout;
270+
271+
while (Date.now() < deadline) {
272+
// Check if we got a ready signal
273+
if (targetServerId && serverReadySignals.has(targetServerId)) {
274+
// Server is ready, clear the signal and return
275+
serverReadySignals.delete(targetServerId);
265276
return;
266-
} catch (error) {
267-
lastError = error instanceof Error ? error : new Error(String(error));
268-
await sleep(delay);
269277
}
278+
279+
await sleep(delay);
270280
}
271-
const reason = lastError ? lastError.message || String(lastError) : "unknown";
272-
throw new Error(`WebSocket ${url} did not become ready (${reason})`);
281+
282+
// Timeout reached, proceed anyway (transport will retry if needed)
283+
console.debug(
284+
`[LSP] waitForWebSocket timed out for ${url}, proceeding anyway`,
285+
);
273286
}
274287

275288
interface LspError extends Error {

src/cm/lsp/types.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import type {
66
Workspace,
77
WorkspaceFile,
88
} from "@codemirror/lsp-client";
9-
import type { ChangeSet, Extension, Text } from "@codemirror/state";
9+
import type { ChangeSet, Extension, MapMode, Text } from "@codemirror/state";
1010
import type { EditorView } from "@codemirror/view";
11+
1112
import type {
1213
Diagnostic as LSPDiagnostic,
1314
FormattingOptions as LSPFormattingOptions,
@@ -247,6 +248,20 @@ export interface LspDiagnostic {
247248
severity: "error" | "warning" | "info" | "hint";
248249
message: string;
249250
source?: string;
251+
/** Related diagnostic information (e.g., location of declaration for 'unused' errors) */
252+
relatedInformation?: DiagnosticRelatedInformation[];
253+
}
254+
255+
/** Related information for a diagnostic (mapped to editor positions) */
256+
export interface DiagnosticRelatedInformation {
257+
/** Document URI */
258+
uri: string;
259+
/** Start position (offset in document) */
260+
from: number;
261+
/** End position (offset in document) */
262+
to: number;
263+
/** Message describing the relationship */
264+
message: string;
250265
}
251266

252267
export interface PublishDiagnosticsParams {
@@ -261,6 +276,17 @@ export interface RawDiagnostic {
261276
code?: number | string;
262277
source?: string;
263278
message: string;
279+
/** Related diagnostic locations from LSP (raw positions) */
280+
relatedInformation?: RawDiagnosticRelatedInformation[];
281+
}
282+
283+
/** Raw related information from LSP (before position mapping) */
284+
export interface RawDiagnosticRelatedInformation {
285+
location: {
286+
uri: string;
287+
range: Range;
288+
};
289+
message: string;
264290
}
265291

266292
// ============================================================================
@@ -285,6 +311,54 @@ export interface ParsedUri {
285311
isFileUri?: boolean;
286312
}
287313

314+
/**
315+
* Interface representing the LSPPlugin instance API.
316+
*/
317+
export interface LSPPluginAPI {
318+
/** The document URI this plugin is attached to */
319+
uri: string;
320+
/** The LSP client instance */
321+
client: LSPClient & { sync: () => void; connected?: boolean };
322+
/** Convert a document offset to an LSP Position */
323+
toPosition: (offset: number) => { line: number; character: number };
324+
/** Convert an LSP Position to a document offset */
325+
fromPosition: (
326+
pos: { line: number; character: number },
327+
doc?: unknown,
328+
) => number;
329+
/** The currently synced document state */
330+
syncedDoc: { length: number };
331+
/** Pending changes that haven't been synced yet */
332+
unsyncedChanges: {
333+
mapPos: (pos: number, assoc?: number, mode?: MapMode) => number | null;
334+
empty: boolean;
335+
};
336+
/** Clear pending changes */
337+
clear: () => void;
338+
}
339+
340+
/**
341+
* Interface for workspace file with view access
342+
*/
343+
export interface WorkspaceFileWithView {
344+
version: number;
345+
getView: () => EditorView | null;
346+
}
347+
348+
/**
349+
* Interface for workspace with file access
350+
*/
351+
export interface WorkspaceWithFileAccess {
352+
getFile: (uri: string) => WorkspaceFileWithView | null;
353+
}
354+
355+
/**
356+
* LSPClient with workspace access (for type casting in notification handlers)
357+
*/
358+
export interface LSPClientWithWorkspace {
359+
workspace: WorkspaceWithFileAccess;
360+
}
361+
288362
// Extend the LSPClient with Acode-specific properties
289363
declare module "@codemirror/lsp-client" {
290364
interface LSPClient {

0 commit comments

Comments
 (0)