Skip to content

Commit e944570

Browse files
committed
feat: implement missing workspace op and uri issue when opening
1 parent a7785c5 commit e944570

File tree

4 files changed

+137
-12
lines changed

4 files changed

+137
-12
lines changed

src/cm/lsp/clientManager.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,8 @@ export class LspClientManager {
303303

304304
const workspaceOptions = {
305305
displayFile: this.options.displayFile,
306+
openFile: this.options.openFile,
307+
resolveLanguageId: this.options.resolveLanguageId,
306308
};
307309

308310
const clientConfig = { ...(server.clientConfig ?? {}) };
@@ -789,31 +791,32 @@ const defaultManager = new LspClientManager();
789791

790792
export default defaultManager;
791793

792-
const FILE_SCHEME_REQUIRED_SERVERS = new Set(["typescript"]);
793-
794794
function normalizeRootUriForServer(
795-
server: LspServerDefinition,
795+
_server: LspServerDefinition,
796796
rootUri: string | null,
797797
): NormalizedRootUri {
798798
if (!rootUri || typeof rootUri !== "string") {
799799
return { normalizedRootUri: null, originalRootUri: null };
800800
}
801801
const schemeMatch = /^([a-zA-Z][\w+\-.]*):/.exec(rootUri);
802802
const scheme = schemeMatch ? schemeMatch[1].toLowerCase() : null;
803+
804+
// Already a file:// URI - use as-is
803805
if (scheme === "file") {
804806
return { normalizedRootUri: rootUri, originalRootUri: rootUri };
805807
}
806808

809+
// Try to convert content:// URIs to file:// URIs
807810
if (scheme === "content") {
808811
const fileUri = contentUriToFileUri(rootUri);
809812
if (fileUri) {
810813
return { normalizedRootUri: fileUri, originalRootUri: rootUri };
811814
}
812-
if (FILE_SCHEME_REQUIRED_SERVERS.has(server.id)) {
813-
return { normalizedRootUri: null, originalRootUri: rootUri };
814-
}
815+
// Can't convert to file:// - server won't work properly
816+
return { normalizedRootUri: null, originalRootUri: rootUri };
815817
}
816818

819+
// Unknown scheme - try to use as-is
817820
return { normalizedRootUri: rootUri, originalRootUri: rootUri };
818821
}
819822

@@ -839,6 +842,7 @@ function contentUriToFileUri(uri: string): string | null {
839842

840843
switch (providerId) {
841844
case "foxdebug.acode":
845+
case "foxdebug.acodefree":
842846
normalized = normalized.replace(/:+$/, "");
843847
if (!normalized) return null;
844848
if (normalized.startsWith("raw:/")) {

src/cm/lsp/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ export interface ClientManagerOptions {
188188
clientExtensions?: Extension | Extension[];
189189
resolveRoot?: (context: RootUriContext) => Promise<string | null>;
190190
displayFile?: (uri: string) => Promise<EditorView | null>;
191+
openFile?: (uri: string) => Promise<EditorView | null>;
192+
resolveLanguageId?: (uri: string) => string | null;
191193
onClientIdle?: (info: ClientIdleInfo) => void;
192194
}
193195

@@ -236,6 +238,8 @@ export interface WaitOptions {
236238

237239
export interface WorkspaceOptions {
238240
displayFile?: (uri: string) => Promise<EditorView | null>;
241+
openFile?: (uri: string) => Promise<EditorView | null>;
242+
resolveLanguageId?: (uri: string) => string | null;
239243
}
240244

241245
// ============================================================================

src/cm/lsp/workspace.ts

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type { WorkspaceFile } from "@codemirror/lsp-client";
22
import { LSPPlugin, Workspace } from "@codemirror/lsp-client";
3-
import type { Text } from "@codemirror/state";
3+
import type { Text, TransactionSpec } from "@codemirror/state";
44
import type { EditorView } from "@codemirror/view";
5+
import { getModeForPath } from "cm/modelist";
56
import type { WorkspaceFileUpdate, WorkspaceOptions } from "./types";
67

78
class AcodeWorkspaceFile implements WorkspaceFile {
@@ -96,6 +97,30 @@ export default class AcodeWorkspace extends Workspace {
9697
return next;
9798
}
9899

100+
#resolveLanguageIdForUri(uri: string): string {
101+
if (typeof this.options.resolveLanguageId === "function") {
102+
const resolved = this.options.resolveLanguageId(uri);
103+
if (resolved) return resolved;
104+
}
105+
try {
106+
const mode = getModeForPath(uri);
107+
if (mode?.name) {
108+
return String(mode.name).toLowerCase();
109+
}
110+
} catch (_) {}
111+
return "plaintext";
112+
}
113+
114+
#getOpenFileCallback(): ((uri: string) => Promise<EditorView | null>) | null {
115+
if (typeof this.options.openFile === "function") {
116+
return this.options.openFile;
117+
}
118+
if (typeof this.options.displayFile === "function") {
119+
return this.options.displayFile;
120+
}
121+
return null;
122+
}
123+
99124
syncFiles(): readonly WorkspaceFileUpdate[] {
100125
const updates: WorkspaceFileUpdate[] = [];
101126
for (const file of this.files) {
@@ -137,12 +162,73 @@ export default class AcodeWorkspace extends Workspace {
137162
return this.#getFileEntry(uri);
138163
}
139164

165+
async requestFile(uri: string): Promise<AcodeWorkspaceFile | null> {
166+
const existing = this.#getFileEntry(uri);
167+
if (existing) return existing;
168+
169+
const openFileCallback = this.#getOpenFileCallback();
170+
if (!openFileCallback) return null;
171+
172+
try {
173+
const view = await openFileCallback(uri);
174+
if (!view?.state?.doc) return null;
175+
const languageId = this.#resolveLanguageIdForUri(uri);
176+
return this.#getOrCreateFile(uri, languageId, view);
177+
} catch (error) {
178+
console.error(`[LSP:Workspace] Failed to open file: ${uri}`, error);
179+
return null;
180+
}
181+
}
182+
183+
connected(): void {
184+
for (const file of this.files) {
185+
this.client.didOpen(file);
186+
}
187+
}
188+
189+
updateFile(uri: string, update: TransactionSpec): void {
190+
const file = this.#getFileEntry(uri);
191+
192+
if (file) {
193+
const view = file.getView();
194+
if (view) {
195+
view.dispatch(update);
196+
return;
197+
}
198+
}
199+
200+
// File is not open - try to open it and apply the update
201+
this.#applyUpdateToClosedFile(uri, update).catch((error) => {
202+
console.warn(`[LSP:Workspace] Failed to apply update: ${uri}`, error);
203+
});
204+
}
205+
206+
async #applyUpdateToClosedFile(
207+
uri: string,
208+
update: TransactionSpec,
209+
): Promise<void> {
210+
const openFileCallback = this.#getOpenFileCallback();
211+
if (!openFileCallback) return;
212+
213+
try {
214+
const file = await this.requestFile(uri);
215+
if (!file) return;
216+
217+
const view = file.getView();
218+
if (view) {
219+
view.dispatch(update);
220+
}
221+
} catch (error) {
222+
console.error(`[LSP:Workspace] Failed to apply update: ${uri}`, error);
223+
}
224+
}
225+
140226
async displayFile(uri: string): Promise<EditorView | null> {
141227
if (typeof this.options.displayFile === "function") {
142228
try {
143229
return await this.options.displayFile(uri);
144230
} catch (error) {
145-
console.error("Failed to display file via workspace", error);
231+
console.error("[LSP:Workspace] Failed to display file", error);
146232
}
147233
}
148234
return null;

src/lib/editorManager.js

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,23 +1155,54 @@ async function EditorManager($header, $body) {
11551155
},
11561156
displayFile: async (targetUri) => {
11571157
if (!targetUri) return null;
1158-
const existing = manager.getFile(targetUri, "uri");
1158+
// Decode URI components (e.g., %40 -> @) since LSP returns encoded URIs
1159+
const decodedUri = decodeURIComponent(targetUri);
1160+
const existing = manager.getFile(decodedUri, "uri");
11591161
if (existing?.type === "editor") {
11601162
existing.makeActive();
11611163
return editor;
11621164
}
11631165
try {
1164-
await openFile(targetUri, { render: true });
1165-
const opened = manager.getFile(targetUri, "uri");
1166+
await openFile(decodedUri, { render: true });
1167+
const opened = manager.getFile(decodedUri, "uri");
11661168
if (opened?.type === "editor") {
11671169
opened.makeActive();
11681170
return editor;
11691171
}
11701172
} catch (error) {
1171-
console.error("Failed to open file for LSP navigation", error);
1173+
console.error("[LSP] Failed to open file", decodedUri, error);
11721174
}
11731175
return null;
11741176
},
1177+
openFile: async (targetUri) => {
1178+
if (!targetUri) return null;
1179+
// Decode URI components (e.g., %40 -> @)
1180+
const decodedUri = decodeURIComponent(targetUri);
1181+
const existing = manager.getFile(decodedUri, "uri");
1182+
if (existing?.type === "editor") {
1183+
existing.makeActive();
1184+
return editor;
1185+
}
1186+
try {
1187+
await openFile(decodedUri, { render: true });
1188+
const opened = manager.getFile(decodedUri, "uri");
1189+
if (opened?.type === "editor") {
1190+
opened.makeActive();
1191+
return editor;
1192+
}
1193+
} catch (error) {
1194+
console.error("[LSP] Failed to open file", decodedUri, error);
1195+
}
1196+
return null;
1197+
},
1198+
resolveLanguageId: (uri) => {
1199+
if (!uri) return "plaintext";
1200+
try {
1201+
const mode = getModeForPath(uri);
1202+
if (mode?.name) return String(mode.name).toLowerCase();
1203+
} catch (_) {}
1204+
return "plaintext";
1205+
},
11751206
clientExtensions: [diagnosticsClientExt],
11761207
diagnosticsUiExtension: buildDiagnosticsUiExt(),
11771208
});

0 commit comments

Comments
 (0)