Servidores de lenguaje
Textchum valida el código mediante el Language Server Protocol, con un comportamiento definitorio: una instancia de servidor por proyecto.
Una instancia por proyecto
Los procesos de servidor se identifican por (servidor, raíz del
proyecto), usando la misma noción de proyecto que
el navegador: el directorio ancestro más cercano con un
marcador de raíz. Abra archivos de dos proyectos Rust distintos y
correrán dos procesos rust-analyzer independientes, cada uno
inicializado con su propia raíz, cada uno viendo solo los archivos de su
proyecto. Las fugas entre proyectos — diagnósticos de un espacio de
trabajo colándose en otro, un índice construido sobre todo el directorio
personal — no pueden ocurrir por construcción.
Los archivos fuera de todo proyecto reciben una instancia por directorio, así que los archivos sueltos tampoco se suman al espacio de trabajo de nadie.
El archivo de una biblioteca es la excepción: un crate del registro de
cargo, las fuentes de la biblioteca estándar, un paquete en
site-packages o node_modules, una cabecera de un SDK. Saltar a la
definición cae en uno a menudo, y lo sirve el servidor del proyecto del
que se venía — que ya conoce el archivo como dependencia —, no un
servidor arrancado sobre el directorio de la biblioteca, que leería el
crate como un proyecto propio y se pondría a compilarlo. El archivo
conserva además el árbol y los ajustes del proyecto desde el que se
llegó a él.
Los proyectos anidados en otro — los miembros de un workspace de uv o de Cargo, una vez que manifest projects los hizo proyectos — son el único caso con elección. Cuando el proyecto exterior tiene recursive config, toman sus entradas de servidor y los atiende su instancia: un servidor arriba, iniciado allí, que ve a todos los miembros y el entorno que comparten. Con separate nested servers además, conservan las entradas y cada uno las corre por su cuenta, en su propia carpeta, para miembros que llevan cada uno su entorno. Un proyecto anidado con entrada propia es siempre independiente. Ambos interruptores existen por proyecto y por defecto, en Settings ▸ Projects y junto a cada entrada de proyecto en Settings ▸ Language Servers; véase configuración.
Lo que se ve
- Los hallazgos llegan mientras se escribe (enviados en lotes con debounce) y encuadran el texto afectado en su color: rojo para errores, naranja para avisos, azul para notas. Una marca se queda sobre el código que señala mientras se edita encima o alrededor, hasta que el servidor vuelve a informar; un lint que solo corre al guardar (el de cargo, por ejemplo) se refresca con el siguiente guardado.
- El subtítulo de la ventana los cuenta («2 errors, 1 warning»).
- Autocompletado al escribir: las sugerencias aparecen tras
caracteres de identificador y
., filtradas mientras se sigue escribiendo — ↑/↓ para elegir, ⏎ o ⇥ para aceptar, ⎋ para descartar, ⌃Espacio para pedirlas explícitamente. Un elemento aceptado sustituye el tramo que nombra el servidor (un autocompletado postfix se traga el receptor y el punto) y trae consigo sus ediciones adicionales, un import por ejemplo. La lista se ordena por lo bien que cada elemento coincide con lo escrito — el nombre entero, luego su comienzo, luego los comienzos de sus palabras (rdencuentraread_dir), luego cualquier parte —, después por lo elegido antes en el proyecto y después por el orden del propio servidor. Cada fila muestra los parámetros del elemento, su tipo y, para uno que trae un import, de dónde viene. La documentación del elemento seleccionado aparece junto a la lista; a un servidor que la deja fuera de la lista se le pide cuando la selección se detiene. Todavía no: la documentación junto a la lista no se desplaza con el teclado. - Show Callers (menú Edit y menú contextual; acción
showCallers) lista cada lugar donde se llama a la función bajo el cursor, en la lista que usa Find References. Las referencias responden «dónde está escrito este nombre»; esto responde «qué la ejecuta», y deja fuera la declaración, los imports y las menciones. - Go to Symbol in Project… (⌥⌘T, Ctrl+Alt+O en Linux; acción
projectSymbols) encuentra una función, un tipo o una constante en cualquier parte del proyecto por parte de su nombre, tal como los conoce el servidor del documento en primer plano, y salta a su declaración. Sin nada escrito, lista los símbolos visitados hace poco en el proyecto, el más reciente primero, sin preguntar al servidor; cada uno se busca por su nombre donde está ahora en su archivo, así que las líneas escritas encima después no lo mandan a otro sitio. Un símbolo renombrado desde entonces aparece en la línea donde estaba. - Se muestran los tipos inferidos y los nombres de los parámetros.
Los tipos que un servidor deduce — de un
letsin anotación, del argumento de una clausura — y los nombres de los parámetros a los que se pasa cada argumento aparecen atenuados dentro de la línea, donde el servidor los coloca:let d = make();se leelet d: Drinker = make();, ypair(d, 2)se leepair(who: d, n: 2). No son parte del texto: el cursor pasa por encima, no se pueden seleccionar ni copiar, y se quedan con su línea mientras se escribe y se vuelven a pedir cuando la escritura hace una pausa. Settings ▸ General ▸ «Show the types the language server inferred» los apaga (editor.inlay_hints). Todavía no: en Linux las pistas se reúnen al final de la línea, cada tipo con su nombre, y los nombres de parámetro se dejan fuera, porque la vista de texto de GTK no puede abrir un hueco dentro de una línea. Sin nada seleccionado, un cursor que descansa sobre un nombre marca dónde más se usa ese símbolo, tal como lo conoce el servidor: una variable que lo oculta con el mismo nombre se deja en paz. Seleccionar una palabra sigue marcando el mismo texto, con servidor o sin él. Ambos siguen el ajuste de marcar ocurrencias. - La llamada que se escribe dice qué recibe. Un paréntesis de apertura o una coma muestra la firma de la función junto al cursor, con el parámetro en curso en negrita; el paréntesis de cierre, ⎋ o salir de la línea la guarda.
- Dejar el ratón sobre un símbolo muestra la documentación hover del servidor en un globo, con el Markdown que envían los servidores ya renderizado — bloques de código en monoespaciada, énfasis y código en línea con su estilo. Solo se dispara sobre identificadores (nunca sobre espacios ni comentarios), sigue abierto mientras el puntero se mueve dentro del símbolo que explica, y se puede apagar en Vista ▸ Documentación al pasar (o en Ajustes). Ajustes ▸ General también puede pedir que se mantenga una tecla — ⇧, ⌃, ⌥ o ⌘ — para mostrarlo, de modo que la documentación aparezca cuando se pide y no donde repose el puntero; pulsar la tecla con el puntero ya sobre un símbolo también la pide. Mostrar documentación del símbolo (⌃⌘H) la pide para el símbolo bajo el cursor a demanda — incluso con el hover del ratón apagado.
- El globo se puede usar. Se queda mientras el puntero está sobre el texto al que se refiere y mientras está dentro del globo, y se cierra un momento después de que el puntero deja ambos, así que se puede llegar a él. Su texto se puede seleccionar y copiar, y la documentación más larga que el globo se desplaza desde su inicio.
- Saltar a la definición (⌃⌘J, o ⌘-clic) va al símbolo bajo el cursor — entre archivos, abriendo o trayendo al frente el destino según haga falta. Sobre la definición no tiene adónde ir, así que responde la pregunta que queda: quién usa esto. Un uso es un salto, varios abren la lista, y un símbolo al que nadie se refiere lo dice. Un servidor que contesta con varias definiciones —una declaración y su implementación— las ofrece igual. El atajo de buscar referencias sigue como estaba.
- Ir a la implementación (⌥⌘B, Ctrl+F12 en Linux) va de una interfaz, un trait o un método abstracto a lo que lo implementa, e Ir a la definición del tipo (⇧⌘B, Ctrl+Mayús+F12 en Linux) de una variable al tipo que contiene. Una respuesta es un salto; varias se listan con el código de alrededor de cada una. No todos los servidores responden ambas: el que no lo hace lo dice junto al cursor.
- Buscar referencias (⇧⌘R) lista cada uso del símbolo bajo el
cursor en un panel flotante — ↑/↓ para moverse, ⏎ para saltar.
Primero el código y después las pruebas, cada parte bajo un
encabezado con su cuenta: qué llama a esto es la pregunta, y qué lo
comprueba es lo siguiente. Qué archivos son pruebas es una
convención y no un hecho —un directorio
tests, unparser_test.go, unButton.test.ts, unParserTests.swift—, así que la regla es prudente ylatest.rsno es una prueba. Un#[cfg(test)] mod testsde Rust dentro de un archivo corriente aparece como código, que es lo que dice su ruta. Si todo cae de un lado, no hay encabezados. - Mostrar usos (⌥⌘F7, Ctrl+Alt+F7 en Linux) es el vistazo rápido: los usos del símbolo bajo el cursor, salvo aquel en el que está el cursor, en una lista junto al cursor con el código del uso seleccionado al lado en macOS y debajo en Linux. Un uso es un salto, y ninguno lo dice. Saltar a la definición sobre una definición muestra la misma lista, en el mismo sitio. Buscar referencias es la lista completa, con la declaración.
- Formatear documento (⌥⇧⌘F) pregunta primero al servidor y cae a la cadena de preprocesadores de guardado — así el formateo funciona en documentos sin título y en lenguajes sin servidor, siempre que haya una cadena configurada.
- Una línea marcada se puede leer. Al posar el puntero sobre un subrayado aparece lo que dijo el servidor, y Mostrar diagnóstico de la línea (⌃⌘E, Ctrl+Alt+E en Linux) dice lo mismo para la línea del cursor —el cursor suele estar al final de la línea que se arregla y no dentro de la marca, así que responde por la línea—. El mensaje nombra su gravedad, porque un subrayado solo dice que algo va mal y un aviso no debería leerse como un error. Sin ida y vuelta: el hallazgo ya está a mano.
- Siguiente hallazgo (F2, F8 en Linux) y Hallazgo anterior (⇧F2, Mayús+F8) llevan el cursor al principio del hallazgo siguiente o del anterior, dando la vuelta al final del documento, y muestran ahí su mensaje. Cada uno es un salto, así que Volver regresa adonde estaba el cursor.
- Diagnósticos… (⇧⌘E, Ctrl+Shift+E en Linux) lista todos los hallazgos del documento en el orden en que aparecen —el orden en que se arreglan y el que muestra el margen—, con la gravedad en cada fila. ⏎ salta, y el salto entra en la pila de vuelta atrás.
- Acciones de código… (⌘., Ctrl+. en Linux) pregunta qué puede
hacer el servidor con el sitio donde está el cursor —importar este
nombre, añadir la rama que falta, quitar la variable sin usar— y
lista lo que llega, con la sugerencia del propio servidor marcada.
Los hallazgos bajo el cursor viajan con la petición tal como los
publicó el servidor, con
codeydataincluidos: así reconoce el servidor lo que él mismo encontró, y uno reconstruido no le dice nada. Una acción que el servidor mandó sin su edición se le devuelve para que la termine antes de aplicarla, y una que trae un comando en vez de una edición la ejecuta el servidor, que devuelve la edición que hace el comando — así funciona organizar las importaciones — y esa edición se aplica igual que la de una acción. Una edición que crea un archivo — un archivo de pruebas que escribe el servidor — lo crea primero; Deshacer lo quita de nuevo mientras siga como se creó. Un servidor también puede pedir que se muestre una página: su documentación, un listado en ensamblador, la división de un paquete — la página se abre en el navegador — o un archivo, que se abre en el editor en la línea que nombra. Las notas que un servidor sujeta a un punto y no a un tramo de texto, como los detalles de optimización del compilador, se subrayan en la palabra de ese punto. Todavía no: una edición que renombra o borra un archivo lo dice y deja el archivo como está. Cuando el cursor se detiene en una línea donde el servidor tiene algo que ofrecer, aparece una bombilla en el margen junto a ella; un clic en la bombilla lista esas mismas acciones. Pasar a otra línea la quita. - Renombrar símbolo (⌃⌘R) pregunta en el sitio: el nombre bajo el cursor se convierte en un campo ahí donde está, relleno y seleccionado, con sus otros usos en el documento marcados mientras tanto. Retorno renombra en todo el espacio de trabajo — los documentos abiertos se editan en el sitio, un paso de deshacer cada uno, y los archivos que nadie tiene abiertos se reescriben en disco — y Escape, o el teclado yéndose a otra parte, lo deja todo como estaba. Deshacer en cualquier archivo que tocó el renombrado lo retira entero; véase Deshacer y rehacer. Lo mismo vale para una acción de código que cambia varios archivos.
- Formatear documento (⌥⇧⌘F) reformatea a través del servidor, conservando tabuladores si el documento sangra con tabuladores y espacios en caso contrario.
- Color desde el servidor: los nombres se colorean según lo que el servidor dice que son — un parámetro, una constante, un tipo de la biblioteca — encima de los colores de la gramática; ver Coloreado de sintaxis.
- Esquema del documento (⇧⌘O) lista los símbolos del archivo — el anidamiento se muestra con sangría, filtrable de forma difusa — y ⏎ salta a la selección.
- Un servidor ausente se informa una sola vez, con el comando que lo instala; todo lo demás del editor sigue funcionando sin él.
Todavía no: Show Callers lista quién llama a una función, no a qué llama ella.
Servidores
Textchum encuentra los servidores en el PATH — no los instala:
| Lenguaje | Servidor | Instalación |
|---|---|---|
| Rust | rust-analyzer | rustup component add rust-analyzer |
| Python | pyright | npm install -g pyright |
| Go | gopls | go install golang.org/x/tools/gopls@latest |
| C | clangd | Xcode CLT, o brew install llvm |
| JavaScript | typescript-language-server | npm install -g typescript-language-server typescript |
| Swift | sourcekit-lsp | viene con la cadena de herramientas de Xcode |
| Zig | zls | brew install zls |
| Bash | bash-language-server | npm install -g bash-language-server |
| C++ | clangd | Xcode CLT, or brew install llvm |
| TypeScript | typescript-language-server | npm install -g typescript-language-server typescript |
| Java | jdtls | brew install jdtls |
| C# | csharp-ls | dotnet tool install --global csharp-ls |
| Ruby | ruby-lsp | gem install ruby-lsp |
| Lua | lua-language-server | brew install lua-language-server |
| Haskell | haskell-language-server | ghcup install hls |
| OCaml | ocamllsp | opam install ocaml-lsp-server |
| Scala | metals | cs install metals |
| Nix | nil | nix profile install nixpkgs#nil |
| CMake | cmake-language-server | uv tool install cmake-language-server |
| JSON | vscode-json-language-server | npm install -g vscode-langservers-extracted |
| HTML | vscode-html-language-server | npm install -g vscode-langservers-extracted |
| CSS | vscode-css-language-server | npm install -g vscode-langservers-extracted |
| YAML | yaml-language-server | npm install -g yaml-language-server |
| TOML | taplo | brew install taplo |
| Markdown | marksman | brew install marksman |
| Elixir | elixir-ls | brew install elixir-ls |
| PHP | intelephense | npm install -g intelephense |
| Dockerfile | docker-langserver | npm install -g dockerfile-language-server-nodejs |
| XML | lemminx | brew install lemminx |
| SQL | sqls | go install github.com/sqls-server/sqls@latest |
| R | languageserver | R -e 'install.packages("languageserver")' |
Las plantillas de Go también las atiende gopls, C++ lo atiende
clangd, y TypeScript y TSX los servidores de JavaScript. Varios
lenguajes tienen más de un servidor registrado: Python cuenta con
pyright, basedpyright, pylsp, ruff, jedi, ty y pyrefly;
JavaScript, TypeScript y TSX con typescript-language-server, vtsls,
deno y biome; Ruby con ruby-lsp, solargraph y rubocop, este
último un linter para correr junto a uno de los otros dos; PHP con
intelephense y phpactor.
La tabla nombra el que se usa cuando la configuración no dice nada; a
los demás se los pide por identificador, y lsp.servers acepta uno que
el editor no conozca.
Elegir los servidores
Settings → Language Servers permite decidir qué comando sirve a un
lenguaje — para todos los proyectos (un valor por defecto) o para una
raíz de proyecto concreta. Las entradas de proyecto ganan a los valores
por defecto; los lenguajes sin entrada usan la tabla anterior. Las
entradas viven en config.json bajo "lsp", con las garantías de
edición a mano habituales del archivo:
{
"lsp": {
"defaults": {"python": "pylsp"},
"projects": {"/work/projA": {"python": "pyright-langserver --stdio"}}
}
}
El campo de lenguaje ofrece los lenguajes que esta compilación conoce y sigue aceptando cualquier texto: se puede configurar un lenguaje antes de que exista una gramática para él, y la entrada sigue sirviendo cuando llega.
Más de un servidor para un lenguaje
Un lenguaje suele ser un comprobador de tipos y un linter, y cada uno
es un servidor por sí mismo. Una entrada los lista con ;; entre
ellos:
{ "lsp": { "defaults": { "python": "pyright ;; ruff" } } }
Todos reciben el documento y sus cambios, y sus hallazgos se muestran juntos. Una petición — hover, autocompletado, ir a la definición, formato — va al primer servidor listado que dice ofrecerla, así que el orden es el orden de preferencia. Una acción de código se pide al servidor que informó del hallazgo bajo el cursor. Si uno no está instalado, los demás corren igual.
Todavía no: el autocompletado viene de un servidor, no se combina el de todos.
Definir un servidor que el editor no conoce
lsp.servers guarda entradas con la misma forma que usa la tabla
incorporada, así que se puede añadir un servidor sin tocar el código, y
redefinir uno ya conocido reutilizando su identificador:
{
"lsp": {
"servers": {
"basedpyright": {
"command": "{project}/.venv/bin/basedpyright-langserver",
"args": ["--stdio"],
"languages": ["python"],
"install": "uv tool install basedpyright"
}
},
"defaults": {"python": "basedpyright"}
}
}
command es obligatorio; el resto se puede omitir. La tabla incorporada
sigue disponible junto a estas entradas, así que una configuración que no
dice nada tiene servidores igualmente, y una versión que aprende uno
nuevo lo ofrece sin reescribir la configuración. Definir un servidor no
cambia cuál usa un lenguaje por omisión: eso lo decide lsp.defaults.
Configuración para un servidor
Los servidores aceptan configuración — qué lints corren, cuán
estricto es el comprobador de tipos — y lsp.settings es donde va: un
objeto por id de servidor, con las claves por sección tal como el
servidor las pide.
{
"lsp": {
"settings": {
"rust-analyzer": {"rust-analyzer": {"check": {"command": "clippy"}}},
"gopls": {"gopls": {"staticcheck": true}},
"pyright": {"python": {"analysis": {"typeCheckingMode": "strict"}}}
},
"init_options": {
"typescript-language-server": {"preferences": {"quotePreference": "single"}}
}
}
}
El objeto se envía al servidor una vez que arrancó y se entrega por
sección cuando el servidor pregunta, que entre las dos es como los
servidores leen su configuración. Es la forma de la tabla settings de
una configuración de nvim-lspconfig, así que una se puede traer tal
cual. lsp.init_options contiene lo que un servidor lee de
initializationOptions. Una línea de comando escrita a mano no tiene
id; su configuración se busca bajo el nombre del comando que ejecuta.
Un servidor lee su configuración al arrancar. Aplicar un perfil de lenguaje reinicia los servidores; tras una edición a mano, Settings ▸ Language Servers tiene el botón que lo hace.
Settings ▸ Language Servers las edita en Server settings: un objeto
JSON por servidor, para todos los proyectos o para una raíz. El objeto
de un proyecto vive en lsp.project_settings.<raíz>.<servidor> y, como
toda entrada por proyecto, sustituye a la de por defecto en vez de
sumarse a ella. Un texto que no es un objeto JSON no se guarda, y el
campo lo dice.
Todavía no: lsp.init_options se edita a mano, y no es por proyecto.
Nombrar un servidor y apuntar a uno dentro del proyecto
La entrada de un lenguaje acepta el identificador de un servidor que el editor conoce o una línea de órdenes.
Un identificador trae consigo los argumentos del servidor. Un lenguaje con más de un servidor registrado usa el primero salvo que la configuración nombre otro.
Una línea de órdenes se ejecuta tal cual, con dos sustituciones:
{project}— la raíz del proyecto con la que se indexa la instancia.{home}— el directorio personal del usuario.
{"lsp": {"defaults":
{"python": "{project}/.venv/bin/basedpyright-langserver --stdio"}}}
Es lo que necesita un repositorio que lleva sus propias herramientas: un
entorno virtual, una entrada de node_modules/.bin, un servidor incluido
en el propio repositorio. La sustitución ocurre por argumento después de
dividir la línea, así que una ruta con espacios sigue siendo un
argumento.
El comando de una entrada se edita en el sitio — corregir una errata o
añadir un --stdio que faltaba se hace en la propia fila, con ⏎ o
haciendo clic fuera, sin borrar y volver a crear. Los cambios se
aplican a los servidores arrancados después; el botón Restart Servers
Now de la pestaña retira las instancias en marcha y las relanza con
la nueva configuración.
Cuando no hay servidor
Dos redes de seguridad cubren el caso sin servidor:
- El respaldo con ctags. Con Ctags fallback activado en
Ajustes → Projects (como valor por defecto o por proyecto, igual que
todos los indicadores de proyecto), Ir a la Definición se responde
desde un índice de Universal Ctags del proyecto
siempre que no haya servidor de lenguaje disponible — y también cuando
un servidor en marcha no tiene respuesta. El índice se construye en el
primer uso y se refresca mientras se sigue saltando; ctags conoce
nombres, no semántica, así que es un respaldo, no un reemplazo. Debe
ser Universal Ctags (
brew install universal-ctags): elctagsque macOS trae en/usr/bines otro programa, mucho más antiguo, que no puede emitir el índice JSON que esto lee. Textchum mira más allá de ese para encontrar un Universal Ctags real en elPATH. - El registro de depuración. Cada decisión en el camino de «archivo
abierto» a «servidor en marcha» — la raíz de proyecto resuelta, qué
servidor se eligió y por qué, los fallos de arranque con el
PATHexacto consultado y cada transición de estado — se añade a:
~/Library/Logs/Textchum/lsp.log
La salida de error propia de cada servidor (stderr) también se
captura ahí, de modo que un servidor que sale durante el arranque
deja su queja registrada — un comando sin su indicador de transporte
(el --stdio de pyright, por ejemplo) se diagnostica de un vistazo,
y el registro avisa directamente cuando un comando personalizado
omite argumentos que el registro incorporado sabe necesarios. Cuando
un proyecto se queda misteriosamente sin soporte de lenguaje, este
archivo nombra la pieza que falta.
Una causa clásica merece nota aparte: las aplicaciones lanzadas desde el
Finder heredaban el PATH mínimo de macOS, que no contiene ninguno de
los lugares donde realmente viven los servidores de lenguaje (Homebrew,
npm, cargo, go). Textchum ahora adopta al arrancar el PATH de la shell
de inicio de sesión — más algunos directorios de herramientas
convencionales — de modo que un servidor que funciona desde la terminal
funciona también desde el Dock.
Por debajo
El cliente vive en el núcleo, tras la misma frontera que todo lo demás: JSON-RPC sobre stdio, un apretón de manos de inicialización antes de cualquier tráfico de documentos y sincronización de documento completo (la sincronización incremental es una optimización futura). Los mensajes del servidor se procesan fuera del hilo de interfaz y llegan a ella por el canal único de eventos del núcleo; un proceso de servidor colgado recibe un período de gracia acotado al cerrar y después se mata, así que salir de Textchum nunca puede quedarse colgado por un servidor que se porta mal. Toda la ruta del protocolo se ejercita en la CI contra un servidor guionizado.
Las instancias también se cuidan solas: un servidor que cae a mitad de sesión se reinicia automáticamente con retroceso (1 → 2 → 4 → 8 segundos; cuatro fallos seguidos y se queda abajo hasta un reinicio o un cambio de configuración), y una instancia que ningún documento abierto ha necesitado en cinco minutos se apaga — la siguiente apertura arranca una fresca.
- Los snippets del autocompletado se expanden y se recorren. El primer marcador vuelve seleccionado, así que escribir lo reemplaza; ⇥ pasa al siguiente y ⇧⇥ al anterior; un marcador escrito más de una vez copia lo que se teclea en el otro. Llegar al final, pulsar ⎋ o hacer clic fuera devuelve las teclas.
- Vista ▸ Estado de servidores lista las instancias en ejecución y las transiciones recientes de la sesión, refrescado en vivo, con un puntero al registro completo.