Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 92 additions & 43 deletions src/cm/lsp/clientManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import type {
BuiltinExtensionsConfig,
ClientManagerOptions,
ClientState,
EnsureServerResult,
DocumentUriContext,
FileMetadata,
FormattingOptions,
LspServerDefinition,
Expand Down Expand Up @@ -242,30 +242,23 @@ export class LspClientManager {
const servers = serverRegistry.getServersForLanguage(effectiveLang);
if (!servers.length) return [];

// Normalize the document URI for LSP (convert content:// to file://)
let normalizedUri = normalizeDocumentUri(originalUri);
if (!normalizedUri) {
// Fall back to cache file path for unrecognized URIs
// This allows LSP to work with any file system provider using the local cache
const cacheFile = file?.cacheFile;
if (cacheFile && typeof cacheFile === "string") {
normalizedUri = buildFileUri(cacheFile.replace(/^file:\/\//, ""));
if (normalizedUri) {
console.info(
`LSP using cache path for unrecognized URI: ${originalUri} -> ${normalizedUri}`,
);
}
}
if (!normalizedUri) {
console.warn(`Cannot normalize document URI for LSP: ${originalUri}`);
return [];
}
}

const lspExtensions: Extension[] = [];
const diagnosticsUiExtension = this.options.diagnosticsUiExtension;

for (const server of servers) {
const normalizedUri = await this.#resolveDocumentUri(server, {
uri: originalUri,
file,
view,
languageId: effectiveLang,
rootUri,
});
if (!normalizedUri) {
console.warn(
`Cannot resolve document URI for LSP server ${server.id}: ${originalUri}`,
);
continue;
}
let targetLanguageId = effectiveLang;
if (server.resolveLanguageId) {
try {
Expand Down Expand Up @@ -296,7 +289,9 @@ export class LspClientManager {
normalizedUri,
targetLanguageId,
);
clientState.attach(normalizedUri, view as EditorView);
const aliases =
originalUri && originalUri !== normalizedUri ? [originalUri] : [];
clientState.attach(normalizedUri, view as EditorView, aliases);
lspExtensions.push(plugin);
} catch (error) {
const lspError = error as LSPError;
Expand Down Expand Up @@ -328,26 +323,25 @@ export class LspClientManager {
const effectiveLang = safeString(languageId ?? languageName).toLowerCase();
if (!effectiveLang || !view) return false;

let normalizedUri = normalizeDocumentUri(originalUri);
if (!normalizedUri) {
const cacheFile = file?.cacheFile;
if (cacheFile && typeof cacheFile === "string") {
normalizedUri = buildFileUri(cacheFile.replace(/^file:\/\//, ""));
}
if (!normalizedUri) {
console.warn(
`Cannot normalize document URI for formatting: ${originalUri}`,
);
return false;
}
}

const servers = serverRegistry.getServersForLanguage(effectiveLang);
if (!servers.length) return false;

for (const server of servers) {
if (!supportsBuiltinFormatting(server)) continue;
try {
const normalizedUri = await this.#resolveDocumentUri(server, {
uri: originalUri,
file,
view,
languageId: effectiveLang,
rootUri: metadata.rootUri,
});
if (!normalizedUri) {
console.warn(
`Cannot resolve document URI for formatting with ${server.id}: ${originalUri}`,
);
continue;
}
const context: RootUriContext = {
uri: normalizedUri,
languageId: effectiveLang,
Expand Down Expand Up @@ -834,28 +828,44 @@ export class LspClientManager {
originalRootUri,
} = params;
const fileRefs = new Map<string, Set<EditorView>>();
const uriAliases = new Map<string, string>();
const effectiveRoot = normalizedRootUri ?? originalRootUri ?? null;

const attach = (uri: string, view: EditorView): void => {
const attach = (
uri: string,
view: EditorView,
aliases: string[] = [],
): void => {
const existing = fileRefs.get(uri) ?? new Set();
existing.add(view);
fileRefs.set(uri, existing);
uriAliases.set(uri, uri);
for (const alias of aliases) {
if (!alias || alias === uri) continue;
uriAliases.set(alias, uri);
}
const suffix = effectiveRoot ? ` (root ${effectiveRoot})` : "";
logLspInfo(`[LSP:${server.id}] attached to ${uri}${suffix}`);
};

const detach = (uri: string, view?: EditorView): void => {
const existing = fileRefs.get(uri);
const actualUri = uriAliases.get(uri) ?? uri;
const existing = fileRefs.get(actualUri);
if (!existing) return;
if (view) existing.delete(view);
if (!view || !existing.size) {
fileRefs.delete(uri);
fileRefs.delete(actualUri);
for (const [alias, target] of uriAliases.entries()) {
if (target === actualUri) {
uriAliases.delete(alias);
}
}
try {
// Only pass uri to closeFile - view is not needed for closing
// and passing it may cause issues if the view is already disposed
(client.workspace as AcodeWorkspace)?.closeFile?.(uri);
(client.workspace as AcodeWorkspace)?.closeFile?.(actualUri);
} catch (error) {
console.warn(`Failed to close LSP file ${uri}`, error);
console.warn(`Failed to close LSP file ${actualUri}`, error);
}
}

Expand Down Expand Up @@ -897,8 +907,6 @@ export class LspClientManager {
server: LspServerDefinition,
context: RootUriContext,
): Promise<string | null> {
if (context?.rootUri) return context.rootUri;

if (typeof server.rootUri === "function") {
try {
const value = await server.rootUri(context?.uri ?? "", context);
Expand All @@ -908,6 +916,8 @@ export class LspClientManager {
}
}

if (context?.rootUri) return safeString(context.rootUri);

if (typeof this.options.resolveRoot === "function") {
try {
const value = await this.options.resolveRoot(context);
Expand All @@ -919,6 +929,45 @@ export class LspClientManager {

return null;
}

async #resolveDocumentUri(
server: LspServerDefinition,
context: RootUriContext,
): Promise<string | null> {
const originalUri = context?.uri;
if (!originalUri) return null;

let normalizedUri = normalizeDocumentUri(originalUri);
if (!normalizedUri) {
// Fall back to cache file path for providers that do not expose a file:// URI.
const cacheFile = context.file?.cacheFile;
if (cacheFile && typeof cacheFile === "string") {
normalizedUri = buildFileUri(cacheFile.replace(/^file:\/\//, ""));
if (normalizedUri) {
console.info(
`LSP using cache path for unrecognized URI: ${originalUri} -> ${normalizedUri}`,
);
}
}
}

if (typeof server.documentUri === "function") {
try {
const value = await server.documentUri(originalUri, {
...context,
normalizedUri,
} as DocumentUriContext);
if (value) return safeString(value);
} catch (error) {
console.warn(
`Server document URI resolver failed for ${server.id}`,
error,
);
}
}

return normalizedUri;
}
}

interface Change {
Expand Down
1 change: 1 addition & 0 deletions src/cm/lsp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export type {
ClientManagerOptions,
ClientState,
DiagnosticRelatedInformation,
DocumentUriContext,
FileMetadata,
FormattingOptions,
LSPClientWithWorkspace,
Expand Down
3 changes: 3 additions & 0 deletions src/cm/lsp/providerUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface ManagedServerOptions {
clientConfig?: LspServerManifest["clientConfig"];
resolveLanguageId?: LspServerManifest["resolveLanguageId"];
rootUri?: LspServerManifest["rootUri"];
documentUri?: LspServerManifest["documentUri"];
capabilityOverrides?: Record<string, unknown>;
}

Expand Down Expand Up @@ -83,6 +84,7 @@ export function defineServer(options: ManagedServerOptions): LspServerManifest {
clientConfig,
resolveLanguageId,
rootUri,
documentUri,
capabilityOverrides,
} = options;

Expand Down Expand Up @@ -118,6 +120,7 @@ export function defineServer(options: ManagedServerOptions): LspServerManifest {
clientConfig,
resolveLanguageId,
rootUri,
documentUri,
capabilityOverrides,
};
}
Expand Down
4 changes: 4 additions & 0 deletions src/cm/lsp/serverRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,10 @@ function sanitizeDefinition(
capabilityOverrides: clone(definition.capabilityOverrides),
rootUri:
typeof definition.rootUri === "function" ? definition.rootUri : null,
documentUri:
typeof definition.documentUri === "function"
? definition.documentUri
: null,
resolveLanguageId:
typeof definition.resolveLanguageId === "function"
? definition.resolveLanguageId
Expand Down
27 changes: 23 additions & 4 deletions src/cm/lsp/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface WorkspaceFileUpdate {
// ============================================================================

export type TransportKind = "websocket" | "stdio" | "external";
type MaybePromise<T> = T | Promise<T>;

export interface WebSocketTransportOptions {
binary?: boolean;
Expand Down Expand Up @@ -167,6 +168,10 @@ export interface LanguageResolverContext {
file?: AcodeFile;
}

export interface DocumentUriContext extends RootUriContext {
normalizedUri?: string | null;
}

export interface LspServerManifest {
id?: string;
label?: string;
Expand All @@ -178,8 +183,14 @@ export interface LspServerManifest {
startupTimeout?: number;
capabilityOverrides?: Record<string, unknown>;
rootUri?:
| ((uri: string, context: unknown) => string | null)
| ((uri: string, context: RootUriContext) => string | null)
| ((uri: string, context: unknown) => MaybePromise<string | null>)
| ((uri: string, context: RootUriContext) => MaybePromise<string | null>)
| null;
documentUri?:
| ((
uri: string,
context: DocumentUriContext,
) => MaybePromise<string | null | undefined>)
| null;
resolveLanguageId?:
| ((context: LanguageResolverContext) => string | null)
Expand Down Expand Up @@ -225,7 +236,15 @@ export interface LspServerDefinition {
clientConfig?: AcodeClientConfig;
startupTimeout?: number;
capabilityOverrides?: Record<string, unknown>;
rootUri?: ((uri: string, context: RootUriContext) => string | null) | null;
rootUri?:
| ((uri: string, context: RootUriContext) => MaybePromise<string | null>)
| null;
documentUri?:
| ((
uri: string,
context: DocumentUriContext,
) => MaybePromise<string | null | undefined>)
| null;
resolveLanguageId?:
| ((context: LanguageResolverContext) => string | null)
| null;
Expand Down Expand Up @@ -293,7 +312,7 @@ export interface ClientState {
client: LSPClient;
transport: TransportHandle;
rootUri: string | null;
attach: (uri: string, view: EditorView) => void;
attach: (uri: string, view: EditorView, aliases?: string[]) => void;
detach: (uri: string, view?: EditorView) => void;
dispose: () => Promise<void>;
}
Expand Down