/** * Settings › Languages & Frameworks › Language Servers, laid out like the * Plugins page: language servers and debug adapters in a list on the left * (+ adds a server of one's own, − removes it), the selected tool's install * actions and settings on the right. Settings are staged in the dialog and * written on Apply / OK; installs run right away. */ import { useEffect, useMemo, useState, type KeyboardEvent } from "react"; import clsx from "clsx"; import type { ToolInfo } from "@/bindings"; import { Badge, Button, EmptyState, IconButton, ProgressBar, SearchField, Spinner, T } from "@/ui"; import { onIdeEvent } from "@/platform/ipc"; import { useProject } from "@/platform/project"; import { useSettingsForm } from "@/workbench/contributions"; import { languageNames, NewServerDialog, ServerSettings, ToolSettings, useLanguageChoices } from "./ServerSettings"; import { serverKey, type ServerEntry } from "./serverConfig"; import { cancelInstall, installTool, refreshTools, STATUS_LABEL, uninstallTool, useTools, type InstallJob, } from "./store"; import styles from "./LanguageServersPage.module.css"; import { tx } from "@/platform/i18n"; type Filter = "all" | "missing" | "installed"; const STATUS_TONE: Record = { installed: "success", onPath: "accent", custom: "accent", missing: "warning", }; const KIND_LABEL: Record = { languageServer: "Language server", debugger: "Debugger", plugin: "Plugin", formatter: "Formatter", cli: "Command-line tool", }; export function filterTools(tools: readonly ToolInfo[], query: string, filter: Filter): ToolInfo[] { const q = query.trim().toLowerCase(); return tools.filter((t) => { if (filter === "missing" && t.status !== "missing") return false; if (filter === "installed" && t.status === "missing") return false; if (!q) return true; return [t.name, t.displayName, t.command, ...t.languages].some((s) => s.toLowerCase().includes(q)); }); } /** A server added in this dialog, listed before Apply checks its executable. */ function pendingTool(name: string, entry: ServerEntry, languages: string[]): ToolInfo { return { name, displayName: name, kind: "languageServer", command: entry.command ?? "", languages, status: "missing", path: null, version: null, installMethod: null, canInstall: false, unavailableReason: null, canUninstall: false, installHint: null, homepage: null, customError: null, defaultArgs: [], defaultInitializationOptions: null, alternatives: [], }; } function JobRow({ name, job }: { name: string; job: InstallJob }) { return (
{job.message ?? "Starting…"}
); } function ToolRow({ tool, selected, off, pending, onSelect, }: { tool: ToolInfo; selected: boolean; off: boolean; pending: boolean; onSelect: () => void; }) { return (
  • ); } function Details({ tool, pending }: { tool: ToolInfo; pending: boolean }) { const job = useTools((s) => s.jobs[tool.name]); const last = useTools((s) => s.logs[tool.name]); const [logOpen, setLogOpen] = useState(false); const log = job?.lines ?? last?.lines ?? []; const lastError = !job && last?.error !== "Cancelled" ? last?.error : null; const busy = !!job; // Servers and adapters show the install hint next to their executable. const manualHint = !pending && !tool.canInstall && tool.status === "missing" && (tool.unavailableReason ?? (tool.kind === "plugin" ? tool.installHint : null)); return (
    {tool.displayName} {pending ? ( New ) : ( {STATUS_LABEL[tool.status]} )}
    {KIND_LABEL[tool.kind]} {tool.version && <> · {tool.version}} {tool.homepage && ( <> {" · "} Homepage )}
    {tool.canInstall && ( )} {tool.canUninstall && ( )}
    {job && } {lastError && (
    {lastError}
    )} {manualHint && (
    {manualHint}
    )} {log.length > 0 && (
    {logOpen && (
                  {log.join("\n")}
                
    )}
    )} {tool.kind === "languageServer" ? ( ) : ( tool.kind === "debugger" && )}
    ); } export function LanguageServersPage() { const { tools, loaded, loading, error } = useTools(); const form = useSettingsForm(); const [query, setQuery] = useState(""); const [filter, setFilter] = useState("all"); const [selectedName, setSelectedName] = useState(null); const [adding, setAdding] = useState(false); const known = useLanguageChoices(); const staged = form.useValue("languageServers") as Record | undefined; const servers = useMemo(() => staged ?? {}, [staged]); const applied = useProject((s) => s.settings?.languageServers); useEffect(() => { void refreshTools(); // Applied paths and servers change what is found and listed. return onIdeEvent("settingsChanged", (e) => { if (!e.key || /^(tools|languageServers)(\.|$)/.test(e.key)) void refreshTools(); }); }, []); // The backend's list, minus servers removed here and plus servers added here (until Apply). const { all, pending } = useMemo(() => { const isCustom = (e: ServerEntry | undefined) => (e?.languages?.length ?? 0) > 0; const list = tools.filter((t) => !isCustom(applied?.[t.name]) || isCustom(servers[t.name])); const added = new Set(); for (const [name, entry] of Object.entries(servers)) { if (!isCustom(entry) || list.some((t) => t.name === name)) continue; list.push(pendingTool(name, entry, languageNames(entry.languages ?? [], known))); added.add(name); } return { all: list, pending: added }; }, [tools, servers, applied, known]); const shown = useMemo(() => filterTools(all, query, filter), [all, query, filter]); const ready = shown.filter((t) => t.status !== "missing"); const missing = shown.filter((t) => t.status === "missing"); const ordered = [...ready, ...missing]; const selected = ordered.find((t) => t.name === selectedName) ?? ordered[0] ?? null; const missingCount = all.filter((t) => t.status === "missing").length; const removable = !!selected && (servers[selected.name]?.languages?.length ?? 0) > 0; const onListKey = (e: KeyboardEvent) => { if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return; e.preventDefault(); const i = selected ? ordered.indexOf(selected) : -1; const next = ordered[Math.min(ordered.length - 1, Math.max(0, i + (e.key === "ArrowDown" ? 1 : -1)))]; if (!next) return; setSelectedName(next.name); e.currentTarget.querySelector(`[data-tool="${CSS.escape(next.name)}"]`)?.focus(); }; const group = (title: string, list: ToolInfo[]) => list.length > 0 && ( <>
  • {title} {list.length}
  • {list.map((t) => ( setSelectedName(t.name)} /> ))} ); return (

    {tx( "Language servers and debug adapters used by the IDE. Missing tools can be installed into the IDE's own tools folder; nothing is downloaded until you click Install.", )}

    {error && (
    {error}
    )}
    setAdding(true)} /> selected && form.set(serverKey(selected.name), null)} /> {loading ? ( ) : ( void refreshTools()} /> )}
    {(["all", "missing", "installed"] as const).map((f) => ( ))}
      {group("Available", ready)} {group("Missing", missing)} {loaded && shown.length === 0 && (
    • No tools match.
    • )} {!loaded && !error && (
    • Checking installed tools…
    • )}
    {selected ? (
    ) : ( )}
    {adding && ( t.name)} onClose={() => setAdding(false)} onAdded={(name) => { setAdding(false); setFilter("all"); setQuery(""); setSelectedName(name); }} /> )}
    ); }