Skip to content

Commit d5fc8ce

Browse files
authored
fix(lsp): allow plugins to override document and root uri handling (#1999)
1 parent 228a339 commit d5fc8ce

File tree

5 files changed

+123
-47
lines changed

5 files changed

+123
-47
lines changed

src/cm/lsp/clientManager.ts

Lines changed: 92 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import type {
2626
BuiltinExtensionsConfig,
2727
ClientManagerOptions,
2828
ClientState,
29-
EnsureServerResult,
29+
DocumentUriContext,
3030
FileMetadata,
3131
FormattingOptions,
3232
LspServerDefinition,
@@ -242,30 +242,23 @@ export class LspClientManager {
242242
const servers = serverRegistry.getServersForLanguage(effectiveLang);
243243
if (!servers.length) return [];
244244

245-
// Normalize the document URI for LSP (convert content:// to file://)
246-
let normalizedUri = normalizeDocumentUri(originalUri);
247-
if (!normalizedUri) {
248-
// Fall back to cache file path for unrecognized URIs
249-
// This allows LSP to work with any file system provider using the local cache
250-
const cacheFile = file?.cacheFile;
251-
if (cacheFile && typeof cacheFile === "string") {
252-
normalizedUri = buildFileUri(cacheFile.replace(/^file:\/\//, ""));
253-
if (normalizedUri) {
254-
console.info(
255-
`LSP using cache path for unrecognized URI: ${originalUri} -> ${normalizedUri}`,
256-
);
257-
}
258-
}
259-
if (!normalizedUri) {
260-
console.warn(`Cannot normalize document URI for LSP: ${originalUri}`);
261-
return [];
262-
}
263-
}
264-
265245
const lspExtensions: Extension[] = [];
266246
const diagnosticsUiExtension = this.options.diagnosticsUiExtension;
267247

268248
for (const server of servers) {
249+
const normalizedUri = await this.#resolveDocumentUri(server, {
250+
uri: originalUri,
251+
file,
252+
view,
253+
languageId: effectiveLang,
254+
rootUri,
255+
});
256+
if (!normalizedUri) {
257+
console.warn(
258+
`Cannot resolve document URI for LSP server ${server.id}: ${originalUri}`,
259+
);
260+
continue;
261+
}
269262
let targetLanguageId = effectiveLang;
270263
if (server.resolveLanguageId) {
271264
try {
@@ -296,7 +289,9 @@ export class LspClientManager {
296289
normalizedUri,
297290
targetLanguageId,
298291
);
299-
clientState.attach(normalizedUri, view as EditorView);
292+
const aliases =
293+
originalUri && originalUri !== normalizedUri ? [originalUri] : [];
294+
clientState.attach(normalizedUri, view as EditorView, aliases);
300295
lspExtensions.push(plugin);
301296
} catch (error) {
302297
const lspError = error as LSPError;
@@ -328,26 +323,25 @@ export class LspClientManager {
328323
const effectiveLang = safeString(languageId ?? languageName).toLowerCase();
329324
if (!effectiveLang || !view) return false;
330325

331-
let normalizedUri = normalizeDocumentUri(originalUri);
332-
if (!normalizedUri) {
333-
const cacheFile = file?.cacheFile;
334-
if (cacheFile && typeof cacheFile === "string") {
335-
normalizedUri = buildFileUri(cacheFile.replace(/^file:\/\//, ""));
336-
}
337-
if (!normalizedUri) {
338-
console.warn(
339-
`Cannot normalize document URI for formatting: ${originalUri}`,
340-
);
341-
return false;
342-
}
343-
}
344-
345326
const servers = serverRegistry.getServersForLanguage(effectiveLang);
346327
if (!servers.length) return false;
347328

348329
for (const server of servers) {
349330
if (!supportsBuiltinFormatting(server)) continue;
350331
try {
332+
const normalizedUri = await this.#resolveDocumentUri(server, {
333+
uri: originalUri,
334+
file,
335+
view,
336+
languageId: effectiveLang,
337+
rootUri: metadata.rootUri,
338+
});
339+
if (!normalizedUri) {
340+
console.warn(
341+
`Cannot resolve document URI for formatting with ${server.id}: ${originalUri}`,
342+
);
343+
continue;
344+
}
351345
const context: RootUriContext = {
352346
uri: normalizedUri,
353347
languageId: effectiveLang,
@@ -834,28 +828,44 @@ export class LspClientManager {
834828
originalRootUri,
835829
} = params;
836830
const fileRefs = new Map<string, Set<EditorView>>();
831+
const uriAliases = new Map<string, string>();
837832
const effectiveRoot = normalizedRootUri ?? originalRootUri ?? null;
838833

839-
const attach = (uri: string, view: EditorView): void => {
834+
const attach = (
835+
uri: string,
836+
view: EditorView,
837+
aliases: string[] = [],
838+
): void => {
840839
const existing = fileRefs.get(uri) ?? new Set();
841840
existing.add(view);
842841
fileRefs.set(uri, existing);
842+
uriAliases.set(uri, uri);
843+
for (const alias of aliases) {
844+
if (!alias || alias === uri) continue;
845+
uriAliases.set(alias, uri);
846+
}
843847
const suffix = effectiveRoot ? ` (root ${effectiveRoot})` : "";
844848
logLspInfo(`[LSP:${server.id}] attached to ${uri}${suffix}`);
845849
};
846850

847851
const detach = (uri: string, view?: EditorView): void => {
848-
const existing = fileRefs.get(uri);
852+
const actualUri = uriAliases.get(uri) ?? uri;
853+
const existing = fileRefs.get(actualUri);
849854
if (!existing) return;
850855
if (view) existing.delete(view);
851856
if (!view || !existing.size) {
852-
fileRefs.delete(uri);
857+
fileRefs.delete(actualUri);
858+
for (const [alias, target] of uriAliases.entries()) {
859+
if (target === actualUri) {
860+
uriAliases.delete(alias);
861+
}
862+
}
853863
try {
854864
// Only pass uri to closeFile - view is not needed for closing
855865
// and passing it may cause issues if the view is already disposed
856-
(client.workspace as AcodeWorkspace)?.closeFile?.(uri);
866+
(client.workspace as AcodeWorkspace)?.closeFile?.(actualUri);
857867
} catch (error) {
858-
console.warn(`Failed to close LSP file ${uri}`, error);
868+
console.warn(`Failed to close LSP file ${actualUri}`, error);
859869
}
860870
}
861871

@@ -897,8 +907,6 @@ export class LspClientManager {
897907
server: LspServerDefinition,
898908
context: RootUriContext,
899909
): Promise<string | null> {
900-
if (context?.rootUri) return context.rootUri;
901-
902910
if (typeof server.rootUri === "function") {
903911
try {
904912
const value = await server.rootUri(context?.uri ?? "", context);
@@ -908,6 +916,8 @@ export class LspClientManager {
908916
}
909917
}
910918

919+
if (context?.rootUri) return safeString(context.rootUri);
920+
911921
if (typeof this.options.resolveRoot === "function") {
912922
try {
913923
const value = await this.options.resolveRoot(context);
@@ -919,6 +929,45 @@ export class LspClientManager {
919929

920930
return null;
921931
}
932+
933+
async #resolveDocumentUri(
934+
server: LspServerDefinition,
935+
context: RootUriContext,
936+
): Promise<string | null> {
937+
const originalUri = context?.uri;
938+
if (!originalUri) return null;
939+
940+
let normalizedUri = normalizeDocumentUri(originalUri);
941+
if (!normalizedUri) {
942+
// Fall back to cache file path for providers that do not expose a file:// URI.
943+
const cacheFile = context.file?.cacheFile;
944+
if (cacheFile && typeof cacheFile === "string") {
945+
normalizedUri = buildFileUri(cacheFile.replace(/^file:\/\//, ""));
946+
if (normalizedUri) {
947+
console.info(
948+
`LSP using cache path for unrecognized URI: ${originalUri} -> ${normalizedUri}`,
949+
);
950+
}
951+
}
952+
}
953+
954+
if (typeof server.documentUri === "function") {
955+
try {
956+
const value = await server.documentUri(originalUri, {
957+
...context,
958+
normalizedUri,
959+
} as DocumentUriContext);
960+
if (value) return safeString(value);
961+
} catch (error) {
962+
console.warn(
963+
`Server document URI resolver failed for ${server.id}`,
964+
error,
965+
);
966+
}
967+
}
968+
969+
return normalizedUri;
970+
}
922971
}
923972

924973
interface Change {

src/cm/lsp/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export type {
7878
ClientManagerOptions,
7979
ClientState,
8080
DiagnosticRelatedInformation,
81+
DocumentUriContext,
8182
FileMetadata,
8283
FormattingOptions,
8384
LSPClientWithWorkspace,

src/cm/lsp/providerUtils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface ManagedServerOptions {
2727
clientConfig?: LspServerManifest["clientConfig"];
2828
resolveLanguageId?: LspServerManifest["resolveLanguageId"];
2929
rootUri?: LspServerManifest["rootUri"];
30+
documentUri?: LspServerManifest["documentUri"];
3031
capabilityOverrides?: Record<string, unknown>;
3132
}
3233

@@ -83,6 +84,7 @@ export function defineServer(options: ManagedServerOptions): LspServerManifest {
8384
clientConfig,
8485
resolveLanguageId,
8586
rootUri,
87+
documentUri,
8688
capabilityOverrides,
8789
} = options;
8890

@@ -118,6 +120,7 @@ export function defineServer(options: ManagedServerOptions): LspServerManifest {
118120
clientConfig,
119121
resolveLanguageId,
120122
rootUri,
123+
documentUri,
121124
capabilityOverrides,
122125
};
123126
}

src/cm/lsp/serverRegistry.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,10 @@ function sanitizeDefinition(
294294
capabilityOverrides: clone(definition.capabilityOverrides),
295295
rootUri:
296296
typeof definition.rootUri === "function" ? definition.rootUri : null,
297+
documentUri:
298+
typeof definition.documentUri === "function"
299+
? definition.documentUri
300+
: null,
297301
resolveLanguageId:
298302
typeof definition.resolveLanguageId === "function"
299303
? definition.resolveLanguageId

src/cm/lsp/types.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export interface WorkspaceFileUpdate {
4242
// ============================================================================
4343

4444
export type TransportKind = "websocket" | "stdio" | "external";
45+
type MaybePromise<T> = T | Promise<T>;
4546

4647
export interface WebSocketTransportOptions {
4748
binary?: boolean;
@@ -167,6 +168,10 @@ export interface LanguageResolverContext {
167168
file?: AcodeFile;
168169
}
169170

171+
export interface DocumentUriContext extends RootUriContext {
172+
normalizedUri?: string | null;
173+
}
174+
170175
export interface LspServerManifest {
171176
id?: string;
172177
label?: string;
@@ -178,8 +183,14 @@ export interface LspServerManifest {
178183
startupTimeout?: number;
179184
capabilityOverrides?: Record<string, unknown>;
180185
rootUri?:
181-
| ((uri: string, context: unknown) => string | null)
182-
| ((uri: string, context: RootUriContext) => string | null)
186+
| ((uri: string, context: unknown) => MaybePromise<string | null>)
187+
| ((uri: string, context: RootUriContext) => MaybePromise<string | null>)
188+
| null;
189+
documentUri?:
190+
| ((
191+
uri: string,
192+
context: DocumentUriContext,
193+
) => MaybePromise<string | null | undefined>)
183194
| null;
184195
resolveLanguageId?:
185196
| ((context: LanguageResolverContext) => string | null)
@@ -225,7 +236,15 @@ export interface LspServerDefinition {
225236
clientConfig?: AcodeClientConfig;
226237
startupTimeout?: number;
227238
capabilityOverrides?: Record<string, unknown>;
228-
rootUri?: ((uri: string, context: RootUriContext) => string | null) | null;
239+
rootUri?:
240+
| ((uri: string, context: RootUriContext) => MaybePromise<string | null>)
241+
| null;
242+
documentUri?:
243+
| ((
244+
uri: string,
245+
context: DocumentUriContext,
246+
) => MaybePromise<string | null | undefined>)
247+
| null;
229248
resolveLanguageId?:
230249
| ((context: LanguageResolverContext) => string | null)
231250
| null;
@@ -293,7 +312,7 @@ export interface ClientState {
293312
client: LSPClient;
294313
transport: TransportHandle;
295314
rootUri: string | null;
296-
attach: (uri: string, view: EditorView) => void;
315+
attach: (uri: string, view: EditorView, aliases?: string[]) => void;
297316
detach: (uri: string, view?: EditorView) => void;
298317
dispose: () => Promise<void>;
299318
}

0 commit comments

Comments
 (0)