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
96 changes: 96 additions & 0 deletions src/cm/lsp/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { defineBundle, defineServer, installers } from "./providerUtils";
import {
getServerBundle,
listServerBundles,
registerServerBundle,
unregisterServerBundle,
} from "./serverCatalog";
import {
getServer,
getServersForLanguage,
listServers,
onRegistryChange,
type RegisterServerOptions,
registerServer,
type ServerUpdater,
unregisterServer,
updateServer,
} from "./serverRegistry";
import type {
LspServerBundle,
LspServerDefinition,
LspServerManifest,
} from "./types";

export { defineBundle, defineServer, installers };

export type LspRegistrationEntry = LspServerManifest | LspServerBundle;

function isBundleEntry(entry: LspRegistrationEntry): entry is LspServerBundle {
return typeof (entry as LspServerBundle)?.getServers === "function";
}

export function register(
entry: LspRegistrationEntry,
options?: RegisterServerOptions & { replace?: boolean },
): LspServerDefinition | LspServerBundle {
if (isBundleEntry(entry)) {
return registerServerBundle(entry, options);
}

return registerServer(entry, options);
}

export function upsert(
entry: LspRegistrationEntry,
): LspServerDefinition | LspServerBundle {
return register(entry, { replace: true });
}

export const servers = {
get(id: string): LspServerDefinition | null {
return getServer(id);
},
list(): LspServerDefinition[] {
return listServers();
},
listForLanguage(
languageId: string,
options?: { includeDisabled?: boolean },
): LspServerDefinition[] {
return getServersForLanguage(languageId, options);
},
update(id: string, updater: ServerUpdater): LspServerDefinition | null {
return updateServer(id, updater);
},
unregister(id: string): boolean {
return unregisterServer(id);
},
onChange(listener: Parameters<typeof onRegistryChange>[0]): () => void {
return onRegistryChange(listener);
},
};

export const bundles = {
list(): LspServerBundle[] {
return listServerBundles();
},
getForServer(id: string): LspServerBundle | null {
return getServerBundle(id);
},
unregister(id: string): boolean {
return unregisterServerBundle(id);
},
};

const lspApi = {
defineServer,
defineBundle,
register,
upsert,
installers,
servers,
bundles,
};

export default lspApi;
10 changes: 10 additions & 0 deletions src/cm/lsp/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
export {
bundles,
default as lspApi,
defineBundle,
defineServer,
installers,
register,
servers,
upsert,
} from "./api";
export { default as clientManager, LspClientManager } from "./clientManager";
export type { CodeActionItem } from "./codeActions";
export {
Expand Down
46 changes: 46 additions & 0 deletions src/cm/lsp/installRuntime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
function getExecutor(): Executor {
const executor = (globalThis as unknown as { Executor?: Executor }).Executor;
if (!executor) {
throw new Error("Executor plugin is not available");
}
return executor;
}

function getBackgroundExecutor(): Executor {
const executor = getExecutor();
return executor.BackgroundExecutor ?? executor;
}

export function quoteArg(value: unknown): string {
const str = String(value ?? "");
if (!str.length) return "''";
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(str)) return str;
return `'${str.replace(/'/g, "'\\''")}'`;
}

export function formatCommand(
command: string | string[] | null | undefined,
): string {
if (Array.isArray(command)) {
return command.map((part) => quoteArg(part)).join(" ");
}
if (typeof command === "string") {
return command.trim();
}
return "";
}

function wrapShellCommand(command: string): string {
const script = command.trim();
return `sh -lc ${quoteArg(`set -e\n${script}`)}`;
}

export async function runQuickCommand(command: string): Promise<string> {
const wrapped = wrapShellCommand(command);
return getBackgroundExecutor().execute(wrapped, true);
}

export async function runForegroundCommand(command: string): Promise<string> {
const wrapped = wrapShellCommand(command);
return getExecutor().execute(wrapped, true);
}
59 changes: 59 additions & 0 deletions src/cm/lsp/installerUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const ARCH_ALIASES = {
aarch64: ["aarch64", "arm64", "arm64-v8a"],
x86_64: ["x86_64", "amd64"],
armv7: ["armv7", "armv7l", "armeabi-v7a"],
} as const;

export type NormalizedArch = keyof typeof ARCH_ALIASES;

export function normalizeArchitecture(arch: string | null | undefined): string {
const normalized = String(arch || "")
.trim()
.toLowerCase();

for (const [canonical, aliases] of Object.entries(ARCH_ALIASES)) {
if (aliases.includes(normalized as never)) {
return canonical;
}
}

return normalized;
}

export function getArchitectureMatchers(
assets: Record<string, string> | undefined | null,
): Array<{ canonicalArch: string; aliases: string[]; asset: string }> {
if (!assets || typeof assets !== "object") return [];

const resolved = new Map<string, { aliases: string[]; asset: string }>();
for (const [rawArch, rawAsset] of Object.entries(assets)) {
const asset = String(rawAsset || "").trim();
if (!asset) continue;

const canonicalArch = normalizeArchitecture(rawArch);
if (!canonicalArch) continue;

const aliases = (
ARCH_ALIASES[canonicalArch as NormalizedArch] || [canonicalArch]
).map((value) => String(value));
resolved.set(canonicalArch, { aliases, asset });
}

return Array.from(resolved.entries()).map(([canonicalArch, value]) => ({
canonicalArch,
aliases: value.aliases,
asset: value.asset,
}));
}

export function buildShellArchCase(
assets: Record<string, string> | undefined | null,
quote: (value: unknown) => string,
): string {
return getArchitectureMatchers(assets)
.map(
({ aliases, asset }) =>
`\t${aliases.join("|")}) ASSET=${quote(asset)} ;;`,
)
.join("\n");
}
Loading