/** * Settings › Language Servers, right-hand side: the selected tool's executable * and, for a language server, its startup options and workspace configuration * (`languageServers.`), plus the New Language Server dialog. Edits are * staged in the Settings dialog and written on Apply / OK. */ import { useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react"; import clsx from "clsx"; import type { ToolInfo } from "@/bindings"; import { Checkbox, Dialog, Icon, IconButton, Input, Popup, SearchField, Select, T } from "@/ui"; import { tx } from "@/platform/i18n"; import { useSettingsForm } from "@/workbench/contributions"; import { toolsBackend, type LanguageChoice } from "./backend"; import { formatArgs, formatEnv, formatJson, formatList, nameFromCommand, parseArgs, parseEnv, parseJsonObject, parseList, SERVER_NAME, serverKey, toolPathKey, type ServerEntry, type ServerField, } from "./serverConfig"; import { STATUS_LABEL, useTools } from "./store"; import styles from "./ServerSettings.module.css"; let languageList: Promise | null = null; /** Languages a server can be used for, loaded once. */ export function useLanguageChoices(): LanguageChoice[] { const [langs, setLangs] = useState([]); useEffect(() => { let live = true; languageList ??= toolsBackend() .languages() .catch(() => { languageList = null; return []; }); void languageList.then((l) => live && setLangs(l)); return () => { live = false; }; }, []); return langs; } /** Test hook: forget the loaded language list. */ export function resetLanguageChoices() { languageList = null; } export function languageNames(ids: readonly string[], known: readonly LanguageChoice[]): string[] { return ids.map((id) => known.find((l) => l.id === id)?.name ?? id); } // --------------------------------------------------------------------------- // Form building blocks (IDEA layout: "Label: [control]", titled separators) /** A titled separator; `collapsible` ones fold away like IDEA's hideable separators. */ function Section({ title, note, collapsible, defaultOpen = true, children, }: { title: string; note?: string; collapsible?: boolean; defaultOpen?: boolean; children: ReactNode; }) { const [open, setOpen] = useState(defaultOpen); const shown = !collapsible || open; return (
{collapsible ? ( ) : (
{title}
)} {shown && (
{note && (
{note}
)} {children}
)}
); } /** "Label:" above its control (beside it in dialogs), the hint below. */ function Row({ label, htmlFor, hint, children, }: { label: string; htmlFor?: string; hint?: ReactNode; children: ReactNode; }) { return (
{children}
{hint &&
{hint}
}
); } interface FieldProps { label: string; value: V | undefined; format: (v: V | undefined) => string; /** Text → value (null removes the setting); throws with a message when invalid. */ parse: (text: string) => V | null; onChange: (value: V | null) => void; placeholder?: string; hint?: ReactNode; multiline?: boolean; trailing?: ReactNode; } /** * A text field over a setting: valid text is staged as it is typed; invalid * text stays in the field, marked, and isn't staged. */ function Field({ label, value, format, parse, onChange, placeholder, hint, multiline, trailing }: FieldProps) { const id = useId(); const [draft, setDraft] = useState(null); const [error, setError] = useState(null); const text = draft ?? format(value); const change = (next: string) => { setDraft(next); try { onChange(parse(next)); setError(null); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } }; const common = { id, value: text, placeholder, "aria-invalid": error ? true : undefined, // Valid text is reformatted from the staged value once the field is left. onBlur: () => !error && setDraft(null), }; return ( {error} ) : ( hint ) } > {multiline ? (