mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
Pyrefly (`pyrefly lsp`) joins basedpyright, Pyright and ty as a fallback Python language server. It is installable from Tools as a managed virtualenv tool and is listed among the interchangeable type checkers, before Ruff, so Ruff still runs after them. Pyrefly only reports type errors in projects with a Pyrefly configuration (`pyrefly.toml` or `[tool.pyrefly]`); hover, completion and navigation work without one. A real-server test (ignored by default, needs `pyrefly` on PATH) covers startup, go-to-definition and diagnostics. Co-authored-by: pcastro93 <2058578+pcastro93@users.noreply.github.com>
250 lines
7.6 KiB
TypeScript
250 lines
7.6 KiB
TypeScript
/**
|
|
* Tool installer backend: `tools_*` IPC in the desktop app; an in-memory
|
|
* simulation in plain-browser development and tests (no network, ever).
|
|
*/
|
|
import { Channel } from "@tauri-apps/api/core";
|
|
import type { InstallEvent, RequestId, ToolInfo } from "@/bindings";
|
|
import { call, commands, IpcError, isTauri } from "@/platform/ipc";
|
|
|
|
export interface LanguageChoice {
|
|
id: string;
|
|
name: string;
|
|
}
|
|
|
|
export interface ToolsBackend {
|
|
list(): Promise<ToolInfo[]>;
|
|
install(name: string, request: RequestId, onEvent: (e: InstallEvent) => void): Promise<ToolInfo>;
|
|
uninstall(name: string): Promise<ToolInfo>;
|
|
cancel(request: RequestId): void;
|
|
/** Restarts running language servers named `name` (after an install). */
|
|
restartServers(name: string): Promise<void>;
|
|
/** File chooser for an executable; null when cancelled. */
|
|
chooseExecutable(title: string, defaultPath?: string): Promise<string | null>;
|
|
/** Languages a server defined in settings can serve. */
|
|
languages(): Promise<LanguageChoice[]>;
|
|
}
|
|
|
|
export const tauriToolsBackend: ToolsBackend = {
|
|
list: () => call(commands.toolsList()),
|
|
install: (name, request, onEvent) => {
|
|
const channel = new Channel<InstallEvent>();
|
|
channel.onmessage = onEvent;
|
|
return call(commands.toolsInstall(name, request, channel));
|
|
},
|
|
uninstall: (name) => call(commands.toolsUninstall(name)),
|
|
cancel: (request) => void commands.cancelRequest(request).catch(() => {}),
|
|
restartServers: async (name) => {
|
|
let servers;
|
|
try {
|
|
servers = await call(commands.lspServers());
|
|
} catch {
|
|
return; // no project open
|
|
}
|
|
await Promise.all(
|
|
servers.filter((s) => s.name === name).map((s) => call(commands.lspRestart(s.id)).catch(() => {})),
|
|
);
|
|
},
|
|
chooseExecutable: async (title, defaultPath) => {
|
|
const { open } = await import("@tauri-apps/plugin-dialog");
|
|
const picked = await open({ directory: false, multiple: false, title, defaultPath: defaultPath || undefined });
|
|
return (Array.isArray(picked) ? picked[0] : picked) ?? null;
|
|
},
|
|
languages: async () =>
|
|
(await commands.languages()).map((l) => ({ id: l.id, name: l.name })).sort((a, b) => a.name.localeCompare(b.name)),
|
|
};
|
|
|
|
function tool(partial: Partial<ToolInfo> & Pick<ToolInfo, "name" | "languages">): ToolInfo {
|
|
return {
|
|
displayName: partial.name,
|
|
kind: "languageServer",
|
|
command: partial.name,
|
|
status: "missing",
|
|
path: null,
|
|
version: null,
|
|
installMethod: "npm",
|
|
canInstall: true,
|
|
unavailableReason: null,
|
|
canUninstall: false,
|
|
installHint: null,
|
|
homepage: null,
|
|
customError: null,
|
|
defaultArgs: [],
|
|
defaultInitializationOptions: null,
|
|
alternatives: [],
|
|
...partial,
|
|
};
|
|
}
|
|
|
|
export function sampleTools(): ToolInfo[] {
|
|
return [
|
|
tool({
|
|
name: "rust-analyzer",
|
|
languages: ["Rust"],
|
|
installMethod: "rustup",
|
|
status: "installed",
|
|
version: "1.85.0",
|
|
path: "~/.cargo/bin/rust-analyzer",
|
|
canUninstall: true,
|
|
}),
|
|
tool({ name: "vtsls", displayName: "vtsls (TypeScript)", languages: ["TypeScript", "JavaScript"] }),
|
|
tool({
|
|
name: "typescript-language-server",
|
|
languages: ["TypeScript", "JavaScript"],
|
|
status: "onPath",
|
|
version: "4.3.3",
|
|
path: "/opt/homebrew/bin/typescript-language-server",
|
|
}),
|
|
...["basedpyright", "pyright", "ty", "pyrefly"].map((name) =>
|
|
tool({
|
|
name,
|
|
displayName:
|
|
name === "pyright"
|
|
? "Pyright"
|
|
: name === "ty"
|
|
? "ty (Python)"
|
|
: name === "pyrefly"
|
|
? "Pyrefly (Python)"
|
|
: name,
|
|
languages: ["Python"],
|
|
installMethod: "uv (virtualenv)",
|
|
alternatives: ["basedpyright", "pyright", "ty", "pyrefly"],
|
|
}),
|
|
),
|
|
tool({ name: "gopls", languages: ["Go"], installMethod: "go install" }),
|
|
tool({
|
|
name: "delve",
|
|
displayName: "Delve (Go debugger)",
|
|
kind: "debugger",
|
|
command: "dlv",
|
|
languages: ["Go"],
|
|
installMethod: "go install",
|
|
}),
|
|
tool({
|
|
name: "codelldb",
|
|
displayName: "CodeLLDB",
|
|
kind: "debugger",
|
|
languages: ["Rust", "C", "C++"],
|
|
installMethod: "GitHub release",
|
|
}),
|
|
tool({
|
|
name: "kotlin-lsp",
|
|
displayName: "Kotlin LSP (JetBrains)",
|
|
languages: ["Kotlin"],
|
|
installMethod: "GitHub release",
|
|
}),
|
|
tool({
|
|
name: "zls",
|
|
displayName: "ZLS (Zig)",
|
|
languages: ["Zig"],
|
|
installMethod: "GitHub release",
|
|
canInstall: false,
|
|
unavailableReason: "curl is required to download tools",
|
|
}),
|
|
tool({
|
|
name: "solargraph",
|
|
languages: ["Ruby"],
|
|
installMethod: null,
|
|
canInstall: false,
|
|
installHint: "gem install solargraph",
|
|
}),
|
|
];
|
|
}
|
|
|
|
/** Deterministic in-memory installer: emits a few lines + progress, then flips the status. */
|
|
export class MemoryToolsBackend implements ToolsBackend {
|
|
tools: ToolInfo[];
|
|
restarted: string[] = [];
|
|
/** Delay between simulated steps (ms). */
|
|
stepMs: number;
|
|
failNext: string | null = null;
|
|
private cancelled = new Set<RequestId>();
|
|
|
|
constructor(tools: ToolInfo[] = sampleTools(), stepMs = 120) {
|
|
this.tools = tools;
|
|
this.stepMs = stepMs;
|
|
}
|
|
|
|
async list() {
|
|
return this.tools.map((t) => ({ ...t }));
|
|
}
|
|
|
|
private replace(next: ToolInfo) {
|
|
this.tools = this.tools.map((t) => (t.name === next.name ? next : t));
|
|
return { ...next };
|
|
}
|
|
|
|
private wait() {
|
|
return new Promise<void>((r) => setTimeout(r, this.stepMs));
|
|
}
|
|
|
|
async install(name: string, request: RequestId, onEvent: (e: InstallEvent) => void) {
|
|
const t = this.tools.find((x) => x.name === name);
|
|
if (!t) throw new IpcError({ kind: "notFound", message: `unknown tool \`${name}\`` });
|
|
if (!t.canInstall)
|
|
throw new IpcError({
|
|
kind: "unavailable",
|
|
message: t.unavailableReason ?? `${name} cannot be installed automatically`,
|
|
});
|
|
onEvent({ type: "line", text: `Installing ${t.displayName} via ${t.installMethod}…` });
|
|
for (const fraction of [0.25, 0.5, 0.75, 1]) {
|
|
await this.wait();
|
|
if (this.cancelled.delete(request)) throw new IpcError({ kind: "cancelled", message: "cancelled" });
|
|
onEvent({ type: "progress", fraction, message: `Downloaded ${(fraction * 8).toFixed(1)} of 8.0 MB` });
|
|
}
|
|
if (this.failNext === name) {
|
|
this.failNext = null;
|
|
throw new IpcError({ kind: "io", message: `installer for ${name} failed` });
|
|
}
|
|
onEvent({ type: "line", text: `${t.displayName} 1.0.0 installed` });
|
|
return this.replace({
|
|
...t,
|
|
status: "installed",
|
|
version: "1.0.0",
|
|
path: `~/tools/${name}/bin/${t.command}`,
|
|
canUninstall: true,
|
|
});
|
|
}
|
|
|
|
async uninstall(name: string) {
|
|
const t = this.tools.find((x) => x.name === name);
|
|
if (!t) throw new IpcError({ kind: "notFound", message: `unknown tool \`${name}\`` });
|
|
return this.replace({ ...t, status: "missing", version: null, path: null, canUninstall: false });
|
|
}
|
|
|
|
cancel(request: RequestId) {
|
|
this.cancelled.add(request);
|
|
}
|
|
|
|
async restartServers(name: string) {
|
|
this.restarted.push(name);
|
|
}
|
|
|
|
/** What `chooseExecutable` answers. */
|
|
chosen: string | null = null;
|
|
|
|
async chooseExecutable() {
|
|
return this.chosen;
|
|
}
|
|
|
|
async languages() {
|
|
return [
|
|
{ id: "go", name: "Go" },
|
|
{ id: "python", name: "Python" },
|
|
{ id: "rust", name: "Rust" },
|
|
{ id: "typescript", name: "TypeScript" },
|
|
];
|
|
}
|
|
}
|
|
|
|
let backend: ToolsBackend | null = null;
|
|
|
|
export function toolsBackend(): ToolsBackend {
|
|
backend ??= isTauri() ? tauriToolsBackend : new MemoryToolsBackend();
|
|
return backend;
|
|
}
|
|
|
|
/** Test hook. */
|
|
export function setToolsBackend(b: ToolsBackend | null) {
|
|
backend = b;
|
|
}
|