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
214 changes: 152 additions & 62 deletions src/cm/lsp/clientManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import type {
FormattingOptions,
LspServerDefinition,
LspRuntimeConnection,
LspRuntimeProvider,
LspClientScope,
NormalizedRootUri,
ParsedUri,
RootUriContext,
Expand Down Expand Up @@ -238,14 +240,23 @@ function buildBuiltinExtensions(
return { extensions, diagnosticsExtension };
}

interface LSPError extends Error {
code?: string;
}

interface InitContext {
key: string;
normalizedRootUri: string | null;
originalRootUri: string | null;
originalDocumentUri: string;
documentUri: string;
runtimeProvider: LspRuntimeProvider;
scope: LspClientScope;
}

interface ResolvedRuntimeTarget {
originalDocumentUri: string;
documentUri: string;
normalizedRootUri: string | null;
originalRootUri: string | null;
runtimeProvider: LspRuntimeProvider;
scope: LspClientScope;
}

interface ExtendedLSPClient extends LSPClient {
Expand Down Expand Up @@ -295,19 +306,20 @@ export class LspClientManager {
if (isSettingsOrKeybindingsFile(server, originalUri, file)) {
continue;
}
const normalizedUri = await this.#resolveDocumentUri(server, {
const target = await this.#resolveRuntimeTarget(server, {
uri: originalUri,
file,
view,
languageId: effectiveLang,
rootUri,
});
if (!normalizedUri) {
if (!target) {
console.warn(
`Cannot resolve document URI for LSP server ${server.id}: ${originalUri}`,
`Cannot resolve runtime or document URI for LSP server ${server.id}: ${originalUri}`,
);
continue;
}
const normalizedUri = target.documentUri;
let targetLanguageId = effectiveLang;
if (server.resolveLanguageId) {
try {
Expand All @@ -327,13 +339,17 @@ export class LspClientManager {
}

try {
const clientState = await this.#ensureClient(server, {
uri: normalizedUri,
file,
view,
languageId: targetLanguageId,
rootUri,
});
const clientState = await this.#ensureClient(
server,
{
uri: normalizedUri,
file,
view,
languageId: targetLanguageId,
rootUri: target.normalizedRootUri ?? undefined,
},
target,
);
const plugin = clientState.client.plugin(
normalizedUri,
targetLanguageId,
Expand All @@ -343,13 +359,6 @@ export class LspClientManager {
clientState.attach(normalizedUri, view as EditorView, aliases);
lspExtensions.push(plugin);
} catch (error) {
const lspError = error as LSPError;
if (lspError?.code === "LSP_SERVER_UNAVAILABLE") {
console.info(
`Skipping LSP client for ${server.id}: ${lspError.message}`,
);
continue;
}
console.error(
`Failed to initialize LSP client for ${server.id}`,
error,
Expand Down Expand Up @@ -382,27 +391,28 @@ export class LspClientManager {
}
if (!supportsBuiltinFormatting(server)) continue;
try {
const normalizedUri = await this.#resolveDocumentUri(server, {
const target = await this.#resolveRuntimeTarget(server, {
uri: originalUri,
file,
view,
languageId: effectiveLang,
rootUri: metadata.rootUri,
});
if (!normalizedUri) {
if (!target) {
console.warn(
`Cannot resolve document URI for formatting with ${server.id}: ${originalUri}`,
);
continue;
}
const normalizedUri = target.documentUri;
const context: RootUriContext = {
uri: normalizedUri,
languageId: effectiveLang,
view,
file,
rootUri: metadata.rootUri,
rootUri: target.normalizedRootUri ?? undefined,
};
const state = await this.#ensureClient(server, context);
const state = await this.#ensureClient(server, context, target);
const capabilities = state.client.serverCapabilities;
if (!capabilities?.documentFormattingProvider) continue;
state.attach(normalizedUri, view);
Expand Down Expand Up @@ -498,16 +508,24 @@ export class LspClientManager {
async #ensureClient(
server: LspServerDefinition,
context: RootUriContext,
target: ResolvedRuntimeTarget,
): Promise<ClientState> {
const useWsFolders = server.useWorkspaceFolders === true;
const resolvedRoot = await this.#resolveRootUri(server, context);
const { normalizedRootUri, originalRootUri } = normalizeRootUriForServer(
server,
resolvedRoot,
);

// For workspace folders mode, use a shared key based on server ID only
const key = pluginKey(server.id, normalizedRootUri, useWsFolders);
const {
documentUri,
normalizedRootUri,
originalRootUri,
runtimeProvider,
scope,
} = target;
const useWsFolders =
scope === "workspace" && server.useWorkspaceFolders === true;
const runtimeServerKey = `${server.id}@${runtimeProvider.id}`;

// Workspace-folder clients are shared only within the selected runtime.
const key =
scope === "document"
? `${runtimeServerKey}::__document__::${documentUri}`
: pluginKey(runtimeServerKey, normalizedRootUri, useWsFolders);

// Return existing client if already initialized
if (this.#clients.has(key)) {
Expand All @@ -532,6 +550,10 @@ export class LspClientManager {
key,
normalizedRootUri: useWsFolders ? null : normalizedRootUri,
originalRootUri: useWsFolders ? null : originalRootUri,
originalDocumentUri: target.originalDocumentUri,
documentUri,
runtimeProvider,
scope,
});
this.#pendingClients.set(key, initPromise);

Expand All @@ -547,7 +569,15 @@ export class LspClientManager {
context: RootUriContext,
initContext: InitContext,
): Promise<ClientState> {
const { key, normalizedRootUri, originalRootUri } = initContext;
const {
key,
normalizedRootUri,
originalRootUri,
originalDocumentUri,
documentUri,
runtimeProvider,
scope,
} = initContext;

const workspaceOptions = {
displayFile: this.options.displayFile,
Expand Down Expand Up @@ -813,24 +843,15 @@ export class LspClientManager {
try {
const runtimeContext = {
...context,
uri: documentUri,
documentUri,
originalDocumentUri,
rootUri: normalizedRootUri ?? null,
originalRootUri: originalRootUri ?? undefined,
serverId: server.id,
allowNonTerminalWorkspace:
this.options.allowNonTerminalWorkspace === true,
};
const runtimeProvider = await selectRuntimeProvider(
server,
runtimeContext,
);
if (!runtimeProvider) {
const unavailable: LSPError = new Error(
`No LSP runtime provider can handle ${server.id}.`,
);
unavailable.code = "LSP_SERVER_UNAVAILABLE";
throw unavailable;
}

const connection = await runtimeProvider.start(server, runtimeContext);
const connectionDispose = connection.dispose;
connection.dispose = async () => {
Expand Down Expand Up @@ -892,7 +913,7 @@ export class LspClientManager {
client,
transportHandle,
normalizedRootUri,
originalRootUri,
originalRootUri: scope === "document" ? null : originalRootUri,
});

this.#clients.set(key, state);
Expand Down Expand Up @@ -1018,26 +1039,95 @@ export class LspClientManager {
return null;
}

async #resolveRuntimeTarget(
server: LspServerDefinition,
context: RootUriContext,
): Promise<ResolvedRuntimeTarget | null> {
const originalDocumentUri = context.uri;
if (!originalDocumentUri) return null;

const originalRootUri = await this.#resolveRootUri(server, context);
const { normalizedRootUri } = normalizeRootUriForServer(
server,
originalRootUri,
);
const normalizedDocumentUri = await this.#resolveDocumentUri(
server,
context,
);
const providerContext = {
...context,
uri: originalDocumentUri,
documentUri: normalizedDocumentUri,
originalDocumentUri,
rootUri: originalRootUri,
originalRootUri: originalRootUri ?? undefined,
serverId: server.id,
allowNonTerminalWorkspace:
this.options.allowNonTerminalWorkspace === true,
};
const runtimeProvider = await selectRuntimeProvider(server, providerContext);
if (!runtimeProvider) {
console.warn(
`No LSP runtime provider selected for ${server.id}: uri=${originalDocumentUri}, root=${originalRootUri ?? "none"}, normalizedUri=${normalizedDocumentUri ?? "none"}`,
);
return null;
}

let documentUri = normalizedDocumentUri;
let rootUri = normalizedRootUri;
let scope: LspClientScope = "workspace";

if (runtimeProvider.resolveUris) {
try {
const resolution = await runtimeProvider.resolveUris(server, {
...providerContext,
originalRootUri,
normalizedDocumentUri,
normalizedRootUri,
});
if (resolution) {
if (Object.prototype.hasOwnProperty.call(resolution, "documentUri")) {
documentUri = resolution.documentUri || null;
}
if (Object.prototype.hasOwnProperty.call(resolution, "rootUri")) {
rootUri = resolution.rootUri || null;
}
if (resolution.scope) scope = resolution.scope;
}
} catch (error) {
console.warn(
`LSP runtime provider ${runtimeProvider.id} failed to resolve URIs for ${server.id}`,
error,
);
return null;
}
}

if (!documentUri) {
console.warn(
`LSP runtime provider ${runtimeProvider.id} produced no document URI for ${server.id}: uri=${originalDocumentUri}, normalizedUri=${normalizedDocumentUri ?? "none"}`,
);
return null;
}
return {
originalDocumentUri,
documentUri,
normalizedRootUri: rootUri,
originalRootUri,
runtimeProvider,
scope,
};
}

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}`,
);
}
}
}
const normalizedUri = normalizeDocumentUri(originalUri);

if (typeof server.documentUri === "function") {
try {
Expand All @@ -1054,7 +1144,7 @@ export class LspClientManager {
}
}

return normalizedUri || originalUri;
return normalizedUri;
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/cm/lsp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,12 @@ export type {
LSPFormattingOptions,
LSPPluginAPI,
LspDiagnostic,
LspClientScope,
LspRuntimeConnection,
LspRuntimeContext,
LspRuntimeProvider,
LspRuntimeUriResolution,
LspRuntimeUriResolutionContext,
LspServerDefinition,
Position,
Range,
Expand Down
3 changes: 2 additions & 1 deletion src/cm/lsp/runtimeProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function normalizeProvider(
throw new Error(`LSP runtime provider ${id} requires start()`);
}
for (const method of [
"resolveUris",
"checkInstallation",
"install",
"uninstall",
Expand Down Expand Up @@ -202,7 +203,7 @@ export function isBuiltinAlpineAccessible(
const scheme = schemeMatch ? schemeMatch[1].toLowerCase() : null;

if (!scheme) return uri.startsWith("/");
if (scheme === "file" || scheme === "untitled") return true;
if (scheme === "file") return true;
if (scheme !== "content") return false;

return /^content:\/\/com\.foxdebug\.acode(?:free)?\.documents\//i.test(uri);
Expand Down
Loading