Video Summary and Transcription
La charla de hoy explora la integración y refactorización de TypeScript, el manejo de acciones de código, el servidor de TypeScript y el proceso de refactorización, la generación de informes de errores y el protocolo y LSP de TypeScript. La charla analiza cómo se integra TypeScript en los editores, el papel de los proveedores de acciones de código y la comunicación entre el cliente y el servidor. También destaca el proceso de dos etapas de las acciones de código y la importancia de la generación de informes de errores. Además, menciona el protocolo de TypeScript y cómo permite extensiones específicas del lenguaje. LSP se menciona como una solución de extensibilidad potente utilizada por varios lenguajes.
1. Integración y Refactorización de TypeScript
¡Hola a todos! Hoy exploraremos cómo se integra TypeScript en tu editor, quién hace qué y cuándo. Usaremos el refactorizado de variables en línea de TypeScript como ejemplo. Una acción de código está vinculada a un diagnóstico, que representa algo que puedes hacer para solucionar errores. La refactorización proporciona recomendaciones inteligentes para escribir un código más limpio o más agradable. El editor reconoce eventos desencadenantes, que pueden ser pasivos o explícitos. TypeScript muestra refactorizaciones basadas en el tipo de desencadenante, y puedes solicitar explícitamente una refactorización para variables.
Hola a todos, y gracias por acompañarme hoy. Soy María Solano, y soy ingeniera de software en Microsoft. Permítanme presentarme rápidamente, me enfoco en las herramientas del editor de TypeScript, pero a veces también profundizo en la extensibilidad de LSP, el sistema de proyectos de JavaScript de Visual Studio, o simplemente miro fijamente el verificador de TypeScript durante un par de horas, tratando de absorber su sabiduría, como estoy segura de que todos ustedes han hecho.
Hoy exploraremos cómo se integra TypeScript en tu editor, quién hace qué y cuándo. Para esto, usaremos el refactorizado de variables en línea de TypeScript como ejemplo, que es mi favorito, principalmente porque lo implementé. Supongamos que tienes esta función de saludo por consola, y tu cursor está en la primera línea de su cuerpo. En un editor como VS Code, verás una bombilla. ¿De dónde viene esa bombilla? Spoiler alerta, si haces clic en ella, aparecerá una lista. ¿Quién construye esa lista y cómo se insertan los elementos en ella? No solo VS Code tiene esta funcionalidad, Visual Studio, NeoVim, Emacs, Zed y otros editores oscuros también tienen una experiencia de bombilla de TypeScript que no difiere mucho de la de VS Code. ¿Cómo se hace eso?
Como nota al margen, durante esta charla usaré acción de código y refactorización como sinónimos, a pesar de que no son exactamente lo mismo. Aunque la diferencia es una tecnicidad, lo explicaré de todos modos para que puedas entenderlo en tu próximo juego de trivia de editores de código. Una acción de código está vinculada a un diagnóstico y, por lo tanto, representa algo que puedes hacer para solucionar esos errores. La refactorización, por otro lado, no son correcciones, son recomendaciones inteligentes para escribir un código más limpio o más agradable, pero no significa que haya algo mal con lo que escribiste.
Volviendo a PrintGreeting, lo primero que sucede es que el editor reconocerá un evento desencadenante. Esto puede ser una acción pasiva, como colocar el cursor en una posición donde se pueda aplicar una refactorización. El desencadenante también puede ser explícito, como uno que configuras con un atajo de teclado. Ten en cuenta que los resultados pueden diferir según el tipo de desencadenante. Tener una bombilla constante apareciendo en todas partes de tu pantalla puede ser molesto, por lo que TypeScript podría decidir mostrarte ciertas refactorizaciones solo si realmente quieres verlas. En este ejemplo, TypeScript mostrará una bombilla junto al identificador de una variable que se podría inlinear, pero deberás solicitar explícitamente la refactorización en las referencias de dichas variables.
2. Manejo de Acciones de Código y Proveedores
El editor necesita a alguien que pueda entender el código para determinar qué refactorizaciones mostrar. Registra la posición y el tipo de contenido, que puede no corresponder directamente a la extensión del archivo. Las extensiones pueden manejar las solicitudes de acciones de código utilizando diferentes mecanismos, como registrar una devolución de llamada. Varios proveedores de acciones de código pueden mejorar las correcciones proporcionadas por el servidor de lenguaje.
Ahora supongamos que no somos expertos en bombillas y usamos el mouse para hacer clic en la bombilla. El editor en realidad no sabe qué refactorizaciones mostrar aquí. Necesita a alguien que pueda entender el código.
Para comunicarse con esa persona, el editor registrará la posición y el tipo de contenido. Ten en cuenta que el tipo de contenido no es exactamente una correspondencia 1 a 1 con el tipo que proviene de la extensión del archivo. Un fragmento de TypeScript dentro de un bloque de script de un archivo HTML también se asigna al tipo de contenido TypeScript. Así es como aún puedes obtener acciones de código, autocompletado y resaltado de sintaxis correcto en código incrustado.
Una extensión anuncia que puede manejar la solicitud de acción de código. Hay diferentes mecanismos para hacerlo. En VS Code, registrarías un proveedor de acciones de código para el tipo de contenido TypeScript. En Visual Studio, podrías usar las API de Roslyn o el protocolo del servicio de lenguaje. La mayoría de las veces, esto implica básicamente registrar una devolución de llamada y decirle al editor cuándo invocarla. Ten en cuenta que esto no se limita a los servidores de lenguaje. Las extensiones como ESLint también pueden conectarse a la lista de acciones de código para mejorar las correcciones proporcionadas por el servidor de lenguaje. Esto también significa que puede haber varios proveedores de acciones de código provenientes de diferentes extensiones. El editor luego combinará todos los resultados en una sola lista.
3. Proceso de Servidor y Refactorización de TypeScript
TS-Server es el servidor de lenguaje de JavaScript y TypeScript que encapsula el compilador de lenguaje y otras características utilizando un protocolo JSON. Los editores proporcionan la misma experiencia de TypeScript siguiendo este protocolo. TSC muestra errores que impiden una compilación exitosa de TypeScript, mientras que otras características se centran en la experiencia de edición. El editor traduce la posición y la información del archivo en un formato entendido por TS-Server. La comunicación entre el cliente y el servidor se gestiona en session.ts, que maneja comandos, servicios de lenguaje y seguimiento de proyectos. La función getApplicableRefactors delega las solicitudes al servicio asignado, asegurando que las modificaciones de código se alineen con las preferencias. La refactorización de variables en línea verifica la inicialización de variables, la estabilidad del código y las referencias antes de realizar la modificación.
Pero bueno, me retiraré ahora porque ya mencioné el servidor de TypeScript antes de presentarlo adecuadamente. Como su nombre sugiere, TS-Server es el servidor de lenguaje de JavaScript y TypeScript, un ejecutable que encapsula el compilador de lenguaje junto con otras características que se representan con un protocolo JSON. Este protocolo define un conjunto de comandos que representan cada mensaje que se puede enviar a TS-Server junto con el formato de solicitud y respuesta. Así es como diferentes editores pueden proporcionar la misma experiencia de TypeScript, todos ellos siguiendo el mismo protocolo.
Por ejemplo, al enviar una solicitud de autocompletado con una carga útil de solicitud de autocompletado, recibirás una respuesta de autocompletado. Cuando me uní a este equipo, recuerdo haberme confundido con la diferencia entre TS-Server y el compilador de TypeScript. TSC puede mostrar errores de TypeScript, pero estos son solo los que impidieron una compilación exitosa de TypeScript. Otras características giran en torno a la experiencia de edición. Piensa en cómo no tiene sentido buscar refactorizaciones si solo estás transpilando a código JavaScript.
Antes de volver al manejo de eventos, recuerda que el editor solo tenía la posición y la información del archivo, por lo que primero necesita traducir eso en algo que TS-Server pueda entender. Aquí muestro un fragmento simplificado de lo que hace la extensión de TypeScript para VS Code, pero Visual Studio tiene una implementación básicamente idéntica en C# de la misma lógica. Sobre esa primera línea de los argumentos, eso se debe a que VS Code usa líneas y columnas basadas en cero, pero TypeScript requiere posiciones basadas en 1 para estas. Puedes imaginar los errores de desplazamiento que hemos tenido debido a eso. También puedes preguntarte, ¿de dónde viene esa variable cliente? En este contexto, usamos cliente y editor indistintamente. Dado que TypeScript es el servidor, el cliente es cualquier cosa que envíe mensajes en el otro lado. El servidor de TS está escuchando al otro lado, y el estado y la delegación de esta comunicación están mayormente encapsulados en session.ts. En este archivo, hay un controlador para cada comando definido en el protocolo, delegando en cada servicio de lenguaje y realizando algún procesamiento de los argumentos y respuestas antes de enviarlos de vuelta. Aquí también llevamos un seguimiento del proyecto de TypeScript, que está definido por tu archivo tsconfig.json o mediante algunas heurísticas de descubrimiento en el caso de que tengas un conjunto de archivos TypeScript en una carpeta aleatoria. Depende de esta capa recoger los resultados de un proyecto o combinarlos en varios proyectos.
Continuando con el controlador de sesión de interés, llegamos a la función getApplicableRefactors. Aquí extraemos cada argumento, obtenemos el proyecto y luego delegamos en el servicio asignado para esa solicitud. Observa la llamada a getPreferences. Dado que las refactorizaciones pueden cambiar tu código, intentamos asegurarnos de que las modificaciones aplicadas tengan la indentación, el tipo de código y otras necesidades de estilo que has configurado. Cada refactorización tiene su propio controlador y el de la variable en línea hace algo como esto. Dados los argumentos de la solicitud, se solicitará al comprobador de tipos su respectivo nodo de TypeScript. Y por cierto, si quieres aprender más sobre el funcionamiento interno de TypeScript, puede ser divertido echar un vistazo a esa bestia de 50,000 líneas de código. Luego se realizan una serie de comprobaciones para asegurarse de que dicho nodo corresponda a una variable inicializada con un valor. También intentamos asegurarnos de que tu código no se rompa al aplicar la refactorización, por lo que comprobamos que no estés exportando la variable, entre otras verificaciones oscuras que mis compañeros más conocedores me dijeron que considerara. Después de eso, aprovechamos findAllReferences para encontrar todas las referencias que necesitaríamos modificar. Luego nos aseguramos de que las ubicaciones de todas las referencias puedan manejar correctamente la sustitución del valor.
4. Proceso de Refactorización y Reporte de Errores
Y si alguna de estas verificaciones falla, simplemente retornamos en defy para indicar que el refactor no se puede aplicar aquí. Luego, la respuesta se envía de vuelta al editor, que realiza una traducción análoga para agregar esas refactorizaciones a la lista de sugerencias. El editor ahora desplegó la lista de acciones de código y mostró al usuario cómo podrían mejorar su código. Eligieron la mejor acción de todas, la de variable en línea, y enviamos otra solicitud a tserver. Las acciones de código son un ejemplo de un comando de tserver que ocurre en dos etapas. En getApplicableRefactors solo notificamos a los editores qué refactorizaciones se pueden aplicar, pero no les decimos cómo. Las refactorizaciones pueden parecer especiales aquí, pero hay otras operaciones con dos pasos que ya has utilizado mucho. Las completaciones son otro ejemplo. Volvamos al controlador de esta refactorización. Cuando se le pide que aplique la acción, nuevamente solicitará al compilador el nodo que se está inlineando, las referencias a él, así como la expresión con la que reemplazaremos la variable. Luego actualiza todas las referencias para usar el valor en lugar del identificador de la variable. Y finalmente, se deshace de la variable que ya no se usa. Después de que el editor lee y traduce la respuesta, finalmente se aplica el refactor. Ahora que eres experto en TS Server, impresiona al editor y a los mantenedores de TypeScript la próxima vez que encuentres un error y crea un problema en GitHub. Di que al aplicar la acción de código, la línea modificada está desplazada en uno. Sin más contexto, esto es un error del servidor de lenguaje. Además, si es un error del servidor, podrías reproducir el problema en otro editor. Como otro escenario, di que al hacer clic en la bombilla, la lista mostrada tiene elementos duplicados. Esto es un error del editor. Si realmente quieres lucir como un profesional, incluso puedes echar un vistazo a los registros del servidor de TS o configurar el nivel de traza de TypeScript en verbose en VS Code para ver qué exactamente está recibiendo y devolviendo el servidor. Eso ayudará a identificar dónde van mal las cosas.
aplicar aquí. Luego, la respuesta se envía de vuelta al editor, que realiza una traducción análoga para agregar esas refactorizaciones a la lista de sugerencias.
¡Y fantástico! El editor ahora desplegó la lista de acciones de código y mostró al usuario cómo podrían mejorar su código. Eligieron la mejor acción de todas, la de variable en línea, y enviamos otra solicitud a tserver.
En este punto, es posible que te preguntes, ¿por qué necesitamos hablar con tserver nuevamente? Esto se debe a que las acciones de código son un ejemplo de un comando de tserver que ocurre en dos etapas. En getApplicableRefactors solo notificamos a los editores qué refactorizaciones se pueden aplicar, pero no les decimos cómo. Esto se debe a que no queremos calcular todas las modificaciones de archivo necesarias para las refactorizaciones si ni siquiera vas a hacer clic en ellas. Solo cuando realmente se selecciona la refactorización, el editor le pedirá a tsserver que aplique el elemento elegido.
Las refactorizaciones pueden parecer especiales aquí. Pero hay otras operaciones con dos pasos que ya has utilizado mucho. Las completaciones son otro ejemplo, es posible que tengas un elemento de completado con un objeto exportado desde otro archivo. Si se selecciona, no solo se modificará la línea completada, TypeScript también actualizará tus importaciones para asegurarse de que la nueva referencia sea válida. Ya sabes cómo funciona, el editor registra una selección, que luego el cliente de TypeScript mapeará de nuevo a la refactorización que recibió en la solicitud anterior. A partir de eso, construye los argumentos de los comandos siguiendo el protocolo de TypeScript. Y cuando recibe el resultado, devuelve las ediciones del espacio de trabajo, que incluyen modificaciones de archivos, adiciones y eliminaciones que implica la refactorización.
Volviendo al controlador de esta refactorización. Cuando se le pide que aplique la acción, nuevamente solicitará al compilador el nodo que se está inlineando, las referencias a él, así como la expresión con la que reemplazaremos la variable. Luego actualiza todas las referencias para usar el valor en lugar del identificador de la variable. Y finalmente, se deshace de la variable que ya no se usa. ¡Y ta-da! Después de que el editor lee y traduce la respuesta, finalmente se aplica el refactor.
Ahora que eres experto en TS Server, impresiona al editor y a los mantenedores de TypeScript la próxima vez que encuentres un error y crea un problema en GitHub. Di que al aplicar la acción de código, la línea modificada está desplazada en uno. Sin más contexto, me atrevería a decir que esto es un error del servidor de lenguaje, ya que al final del día el editor está aplicando ciegamente el cambio que el servidor le indicó que hiciera. Además, si es un error del servidor, podrías reproducir el problema en otro editor, así que también puedes intentar eso. Como otro escenario, di que al hacer clic en la bombilla, la lista mostrada tiene elementos duplicados. Esto es un poco más difícil de determinar, pero por mi experiencia, supondría que esto es un error del editor, ya que es el responsable de consultar a los proveedores de acciones de código, combinar los resultados y mostrarlos en la interfaz de usuario. Como una historia divertida, uno de los primeros errores que solucioné en Microsoft fue la duplicación de entradas de autocompletado en Visual Studio. Fue una experiencia fascinante, pero ligeramente traumática. Si realmente quieres lucir como un profesional, incluso puedes echar un vistazo a los registros del servidor de TS o configurar el nivel de traza de TypeScript en verbose en VS Code para ver qué exactamente está recibiendo y devolviendo el servidor. Eso ayudará a identificar dónde van mal las cosas.
5. Protocolo TypeScript y LSP
Ahora que hemos repasado cómo funciona el protocolo TypeScript, es posible que te preguntes si siempre se diseñó así. El servidor de TS siempre ha existido de alguna manera, aunque no se haya especificado formalmente. Más adelante, el servidor de TS se trasladó a su propio proceso de nodo, como un esfuerzo para mejorar el rendimiento del IDE. Esto permite que el mismo servidor se comunique con cualquier persona que siga el protocolo TypeScript. El servidor de TS solo se preocupa por las cosas de TypeScript y el editor por todo lo demás. Gracias a este diseño, podemos tener un cliente de TypeScript en VS Code, una implementación de Lua para NeoVim y un controlador de Python para Sublime. LSP utiliza tipos neutrales al lenguaje, como URIs de archivo y posiciones de documentos. Entonces, ¿por qué no usamos simplemente LSP? Sin embargo, un cambio así requeriría una reescritura importante no solo del servidor, sino también de todos los clientes que han estado siguiendo este protocolo de comunicación durante años. Las características mejoradas específicas de TypeScript requieren extensiones personalizadas para el protocolo. LSP sigue siendo una solución de extensibilidad potente, e incluso lenguajes complejos y populares como Rust utilizan LSP para su interfaz de servicios de lenguaje.
Ahora que hemos repasado cómo funciona el protocolo TypeScript, es posible que te preguntes si siempre se diseñó así. La respuesta es un poco vaga, porque el servidor de TS siempre ha existido de alguna manera, aunque no se haya especificado formalmente. En los primeros días, el servidor de TS se ejecutaba en Visual Studio. Y sí, me refiero a IDE y no a VS Code, porque VS Code ni siquiera existía en ese entonces.
Más adelante, el servidor de TS se trasladó a su propio proceso de nodo, como un esfuerzo para mejorar el rendimiento del IDE. Afortunadamente, esa transición fue fluida, porque con el protocolo JSON existente, pasar a una comunicación entre procesos prácticamente no tuvo impacto. A partir de los fragmentos que acabamos de ver, es posible que sientas que traducir los argumentos y las respuestas puede ser molesto o incluso redundante. Sin embargo, esto permite que el mismo servidor se comunique con cualquier persona que siga el protocolo TypeScript. No importa en qué lenguaje esté implementado el cliente, ni la interfaz de usuario utilizada para interactuar con el usuario. El servidor de TS solo se preocupa por las cosas de TypeScript y el editor por todo lo demás.
Gracias a este diseño, podemos tener un cliente de TypeScript en VS Code, una implementación de Lua para NeoVim y un controlador de Python para Sublime. De hecho, el servidor de TS incluso fue una inspiración para LSP, el protocolo que define un estándar sobre cómo se comunican las herramientas del editor y los servidores de lenguaje. Con él, un solo servidor puede reutilizarse en múltiples herramientas de desarrollo y los editores pueden admitir lenguajes con un esfuerzo mínimo. LSP utiliza tipos neutrales al lenguaje, como URIs de archivo y posiciones de documentos. Esto se debe a que es mucho más fácil estandarizar el URI del documento de texto o una posición del cursor que definir un terreno común entre árboles de sintaxis abstracta y símbolos del compilador en múltiples lenguajes de programación.
Entonces, ¿por qué no usamos simplemente LSP? ¿Por qué seguir con este protocolo personalizado de TypeScript en lugar del protocolo de servicio de lenguaje más flexible y general? Después de todo, al hacer esa transición, no sería necesario traducir entre TypeScript y los tipos del editor. Sin embargo, un cambio así requeriría una reescritura importante no solo del servidor, sino también de todos los clientes que han estado siguiendo este protocolo de comunicación durante años. Además, dicho cambio podría no significar una gran ventaja de todos modos, ya que para proporcionar interacciones ricas del editor, a menudo necesitamos anular los métodos de LSP. Recuerda que LSP sigue siendo neutral al lenguaje. Las características mejoradas específicas de TypeScript requieren extensiones personalizadas para el protocolo. Un ejemplo de esto es el nuevo refactor de TypeScript Mover a archivo. Con él, puedes mover un bloque de código a un archivo existente, lo que requiere solicitar al usuario el nombre del archivo de destino. LSP no tiene un estándar para acciones de código que requieren entrada del usuario, mientras que el servidor de TS puede evolucionar para hacerlo.
Dicho esto, LSP sigue siendo una solución de extensibilidad potente, e incluso lenguajes complejos y populares como Rust utilizan LSP para su interfaz de servicios de lenguaje. Y eso es todo lo que tenía para ti hoy. Espero que hayas disfrutado y aprendido más sobre cómo obtener todas esas ventajas de tipos. Y la próxima vez que hagas clic en una bombilla, recordarás esta charla. Gracias por escuchar.
Comments