Cómo cerrar la brecha de inteligencia de código hizo que Cursor funcionara para nuestro monorepo Bazel de 26 millones de líneas
La mayor parte del código en Databricks ahora es escrito por agentes. Para los momentos en que los ingenieros aún programan directamente, recurren a editores ligeros que se inician rápidamente y les permiten navegar por el código con una configuración mínima. Sin embargo, para Scala y Java, IntelliJ ha establecido el estándar durante años. A la escala de nuestro monorepo, era prácticamente el único editor capaz de mantener el ritmo. Esta publicación comparte cómo creamos una solución alternativa al extender Metals, el servidor de lenguaje de Scala ampliamente utilizado, para ofrecer un soporte de primer nivel para Java y escalar al tamaño de nuestro monorepo. En colaboración con el equipo upstream de Metals, ahora hemos publicado Metals v2 como código abierto para que cualquiera que tenga una base de código grande en Java y Scala pueda combinar su agente de programación con un editor ligero.
Metals v2 está disponible hoy en Cursor, VS Code y Neovim, con instrucciones de instalación disponibles en el sitio web de Metals.
En mayo de 2025, comenzamos a estandarizar el flujo de trabajo diario de edición de Databricks en torno a Cursor. Cursor y VS Code ya se usaban ampliamente en Databricks para el frontend y otros trabajos que no requerían JVM, y ambos contaban con un sólido soporte remoto SSH para nuestro entorno de desarrollo basado en la nube. Sin embargo, la mayoría de nuestros servicios están escritos en Scala y Java, y navegar por ellos a la escala de nuestro monorepo era el último obstáculo; el problema que nos propusimos resolver.
Estandarizar en un solo editor importaba más allá de las preferencias individuales. Una plataforma de IDE unificada crea un efecto volante: los equipos comparten una base común para la exploración y el desarrollo de código, mientras que el equipo de la plataforma puede concentrar las inversiones en un solo lugar. Cursor se convirtió en el editor principal para el trabajo con JVM en nuestro monorepo, y nos consolidamos lo suficiente como para no renovar la mayoría de nuestras licencias de IntelliJ este año.
Tres señales muestran hasta qué punto llegó la transición a Cursor: el uso general del IDE, los eventos de apertura de archivos de Scala y Java donde Metals se encuentra en la ruta crítica, y la adopción fuera de Databricks.
La señal más amplia es el uso general del IDE. La adopción de Cursor creció por sí sola en Databricks, pero se estancó en septiembre de 2025, manteniéndose así durante meses. El crecimiento se reanudó con el lanzamiento de Metals v2 y, para julio de 2026, el 92% de los usuarios activos semanales de IDE abrían Cursor, en comparación con el 12% de IntelliJ. Entre los ingenieros que solo usan un único IDE, 2400 están ahora en Cursor, en comparación con los 120 de IntelliJ.
Una señal más precisa que el uso general del IDE es la proporción de archivos abiertos en cada IDE para Scala y Java, los lenguajes donde Metals v2 se encuentra en la ruta crítica. Desde que las primeras mejoras importantes de Metals v2 llegaron a Cursor en octubre de 2025, la participación de Cursor en los eventos de apertura de archivos de Scala y Java ha aumentado del 40% al 78%. La subida es más lenta que la adopción agregada del IDE porque Scala y Java son los lenguajes donde IntelliJ estaba más arraigado, pero la participación de Cursor sigue mostrando una tendencia alcista mes a mes.
Metals v2 comenzó como un fork de Databricks, pero el objetivo siempre fue devolver el trabajo a la comunidad de código abierto. Junto con el equipo de Cursor y los mantenedores principales de Metals en VirtusLab, estamos preparando una versión estable de Metals v2 para reemplazar la versión estable actual v1.
Estamos contribuyendo a Metals v2 para que Cursor funcione bien en bases de código de Java con millones de líneas, centrándonos en mejorar el soporte de Bazel, la depuración y las pruebas.—Kevin Niparko, Cursor
La IA está cambiando la forma en que los desarrolladores usan los IDE, y Metals v2 avanza en la dirección correcta: inicio rápido, orientación confiable en la base de código y una arquitectura diseñada para grandes bases de código. Databricks validó este enfoque a una escala excepcional, y en VirtusLab estamos entusiasmados de ayudar a llevar este trabajo a la comunidad más amplia de Scala y JVM.—Krzysztof Romanowski, Director de Productividad de Desarrollo, VirtusLab
Las conversaciones con responsables de otras grandes bases de código de JVM han reforzado el mismo patrón que vimos en Databricks: demanda de Cursor, VS Code y Neovim con un sólido soporte remoto SSH, presión de la plataforma hacia herramientas unificadas y la falta de una alternativa viable con los servidores de lenguaje de JVM existentes a la escala de un monorepo.
Comenzamos a implementar Metals v2 en Stripe hace menos de un mes y, aun así, estamos constantemente impresionados con lo bien que funciona en nuestra base de código de Java, el entusiasmo de nuestros ingenieros y lo agradable que es trabajar y aprender de los mantenedores.—Mahib Hosain, Plataforma de Desarrolladores, Stripe
Este es el modelo que queremos para Metals v2: una infraestructura compartida para grandes bases de código de JVM, mantenida de forma abierta y moldeada por las empresas que necesitan que funcione a escala. El desarrollo continuo está liderado por VirtusLab; póngase en contacto con ellos para preguntas, comentarios o contribuciones a través del rastreador de problemas de GitHub o en metals@virtuslab.com.
Todo lo anterior justifica el uso de Metals v2. El resto es para los lectores que deseen comprender mejor la ingeniería detrás de la inteligencia de código de baja latencia en un monorepo de 26 millones de líneas.
Dado que los agentes escriben la mayor parte del código, una orientación rápida en la base de código importaba más que una cobertura completa de autocompletado y refactorización del Language Server Protocol (LSP). Eso acotó el problema, pero no hizo que la solución fuera obvia: aún teníamos que ofrecer una navegación de baja latencia y fácil de configurar para un monorepo de Bazel en Scala y Java de este tamaño, y los LSP comerciales no estaban diseñados para soportar esa escala. Tuvimos que construir inteligencia de código a escala de repositorio desde los principios fundamentales y tratar la utilidad del inicio como una métrica clave que podíamos medir y mejorar.
El tiempo hasta la inteligencia inicial (TTII) mide qué tan rápido el editor se vuelve útil después de abrir el repositorio. Iniciamos el cronómetro cuando se activa el servidor de lenguaje y lo detenemos cuando las funciones más importantes están disponibles, asumiendo que no hay intervención del usuario. Estas funciones críticas incluyen la búsqueda difusa de símbolos en el espacio de trabajo, la opción de ir a la definición y la búsqueda de usos de símbolos en todo el repositorio.
Metals v2 es un servidor de lenguaje para Scala y Java. Comenzamos a partir de Metals v1, el servidor de lenguaje oficial de Scala, pero alcanzar nuestro objetivo de TTII requirió más que solo hacer algunos ajustes superficiales. Creamos un fork y rediseñamos tres capas centrales: el índice del repositorio, los pipelines de compilación de Scala y Java, y el límite de integración de compilación.
Las siguientes secciones detallan cada capa a su vez.
mbt significa Metals Build Tool, y el índice mbt es el principal facilitador del TTII o del contrato de "utilidad inmediata". Es un índice direccionado por contenido de las fuentes del espacio de trabajo: Metals utiliza el comando git ls-files --stage para descubrir archivos y OIDs de blob de Git para decidir qué entradas del índice se pueden reutilizar.
Con la información de todo el repositorio disponible antes de la sincronización de la compilación, Metals puede responder a preguntas de la "primera milla":
El índice mbt es, en esencia, un mapa hash desde el archivo de origen a un resumen local del archivo. Cada entrada registra las declaraciones de paquetes del archivo, las definiciones con sus ubicaciones de origen y filtros de Bloom compactos para los identificadores a los que se hace referencia en el archivo. Las definiciones impulsan la búsqueda de símbolos en el espacio de trabajo y la función de ir a la definición (jump-to-definition), mientras que los filtros de Bloom permiten a Metals descartar rápidamente los archivos que no pueden contener una referencia antes de realizar comprobaciones más precisas. Dado que cada entrada se deriva de un solo archivo, las actualizaciones incrementales siguen siendo sencillas: cuando un archivo cambia, Metals vuelve a calcular la entrada de ese archivo y la reemplaza.
En nuestro monorrepositorio, el índice mbt persistido pesa 936MB sin comprimir y contiene información sobre 2.9m de símbolos en más de 142k archivos de Scala, Java y Protobuf. Una compilación de referencia limpia tarda 22 segundos con una utilización completa de la CPU en 32 núcleos, mientras que analizar un índice precompilado desde el disco tarda 5 segundos. En producción, medimos el TTII como el tiempo para iniciar el servidor, cargar un índice mbt desactualizado, actualizarlo con el estado más reciente de git ls-files --stage y reiniciar los compiladores de presentación de Scala y Java: p50 8.7s, p90 36.7s. La búsqueda difusa de símbolos en 2.9m de símbolos del espacio de trabajo es de p50 10ms, p90 95ms. Hay margen para reducir aún más el TTII, pero con estas cifras no es el cuello de botella que debemos abordar a continuación.
El pipeline de Scala se basa en el compilador de presentación, un modo del verificador de tipos de Scala que almacena en caché y reutiliza la información de la tabla de símbolos en todas las ejecuciones de compilación. Esta reutilización, combinada con la resolución diferida de símbolos del compilador, permite que una sola instancia mantenga en alcance todo el código base de Scala de 24M de líneas mientras publica diagnósticos a p50 0.9s, p90 8.9s.
Esa única instancia del compilador de Scala se ejecuta en uno de dos modos, según la cantidad de información que Metals tenga del servidor de compilación sobre el archivo que se está editando. Antes de una sincronización de compilación, un compilador de respaldo adopta una postura permisiva, tratando cada archivo de origen en el repositorio como un candidato de dependencia elegible, lo que hace que la navegación sea útil de inmediato, incluso en código que aún no se compila en Bazel. Después de una sincronización de compilación, un compilador preciso se limita a los límites de classpath y sourcepath que informa el servidor de compilación, lo que hace que sus diagnósticos e información de dependencias sean precisos para la compilación. Ambos modos, el preciso y el de respaldo, se apoyan en las mismas dos técnicas para mantener manejable un sourcepath de este tamaño:
El modo preciso añade una tercera técnica: mantiene las fuentes de dependencias transitivas en el sourcepath, por lo que las ediciones en archivos de diferentes objetivos de Bazel se reflejan de inmediato en el editor sin tener que esperar a que Bazel genere un nuevo classpath, una limitación que frenaba a Metals v1.
Mantener un código base de 24M de líneas en una sola instancia de compilador de presentación está muy fuera de los límites para los que se diseñó originalmente el compilador de Scala. Metals v2 lo logra a través de dos modos de compilador basados en un conjunto compartido de técnicas para escalar el sourcepath. La función de ir a la definición (jump-to-definition), la característica más utilizada del editor, se ejecuta en todo el repositorio a p50 7ms, p90 575ms. Scala es el lenguaje en el que trabaja la mayoría de nuestros ingenieros, por lo que este pipeline era la pieza clave que debía funcionar para que Cursor se convirtiera en una alternativa real a IntelliJ en nuestro monorrepositorio.
Metals v2 implementa la superficie LSP de Java directamente en las API de javac. Evaluamos la posibilidad de reutilizar un servidor de lenguaje Java existente (implementaciones basadas en JDT y NetBeans), pero ambos se centran en la compilación de la misma manera que Metals v1, que es precisamente el acoplamiento que la versión v2 quería eliminar. En su lugar, compilar sobre javac permitió a Java compartir el sourcepath y el modelo de sincronización de compilación de Metals v2 con Scala, que sigue constituyendo la mayor parte de nuestro monorrepositorio, y la superficie de Java que necesitábamos era lo suficientemente pequeña como para mantenerla nosotros mismos.
Las API de javac proporcionaron suficiente acceso al compilador para implementar diagnósticos, navegación, resaltado sintáctico semántico y otros métodos clave de LSP, y funcionaron bien con código parcialmente roto, lo cual es importante para el flujo de trabajo de un editor interactivo. Al igual que con Scala, mantuvimos deliberadamente un alcance más acotado para las funciones de edición activa, como las refactorizaciones y el autocompletado.
El principal problema de escalabilidad con esta arquitectura apareció en archivos que provocaban un rendimiento patológico en la fase "enter" de javac al importar de forma transitiva millones de líneas de código en la capa de esquema de símbolos. Metals v2 soluciona esto con un modo javaSymbolLoader: "turbine-classpath", habilitado de forma predeterminada, que utiliza una versión modificada del compilador de cabeceras Turbine para producir un classpath de repositorio que funcione en un entorno de IDE, incluido el manejo adecuado de errores de resolución de nombres. Turbine procesa cerca de un millón de líneas de código Java por segundo en un solo hilo, lo que significa que Metals puede recompilar todo el código base de Java a intervalos regulares. Esto mantiene casi todos los símbolos entre archivos en el classpath en lugar del sourcepath, por lo que la fase "analyze" de javac puede ejecutarse cerca de su límite práctico para uso interactivo: casi 100k líneas de código por segundo según nuestras pruebas de rendimiento.
«Útil antes de la sincronización de compilación» no significa «ignorar la compilación». Metals sigue necesitando fidelidad en el gráfico de compilación para una larga lista de funciones, incluida la navegación a dependencias de terceros o fuentes generadas, el respeto de las reglas de sombreado (shading) personalizadas, el descubrimiento de conjuntos de pruebas y la autoconfiguración de lanzadores de depuración. En Databricks, eso significa hacer que los metadatos de 285k objetivos de JVM de Bazel se puedan consultar con latencias de editor.
Nuestra implementación de producción de esta capa es un servidor BSP interno escrito en Go y adaptado a nuestras reglas de Bazel. Este servidor BSP no forma parte de este lanzamiento de código abierto, pero vale la pena trasladar sus decisiones de diseño a otras implementaciones de BSP de Bazel.
En primer lugar, Metals v2 nunca invoca a Bazel a través del servidor BSP a menos que el usuario lo solicite explícitamente. En un monorrepositorio grande, la sincronización en segundo plano del IDE puede bloquear a Bazel y competir con las compilaciones iniciadas por el desarrollador, por lo que la sincronización de compilación es una acción explícita del usuario en lugar de un comportamiento de inicio o una tarea de mantenimiento en segundo plano. Los metadatos resultantes se almacenan en una instantánea JSON que se escala mediante la agrupación de constantes para etiquetas, rutas y prefijos de repositorio repetidos. Cuando los usuarios realizan la sincronización, el servidor BSP añade de forma incremental más objetivos a esta instantánea y ofrece los metadatos actualizados a través de BSP.
En segundo lugar, no existe un formato de configuración de sincronización compartido. Los usuarios sincronizan archivos o directorios individuales bajo demanda a medida que editan para mejorar la navegación o la fidelidad de los diagnósticos. Según nuestra experiencia, los conjuntos de sincronización predefinidos crecen con el tiempo, se copian entre equipos y se vuelven más lentos que la sincronización enfocada que el desarrollador realmente necesita.
Juntos, el índice libre de compilación, los pipelines respaldados por el compilador y la integración de compilación centrada en metadatos ofrecen inteligencia de código de baja latencia en códigos base de Bazel de millones de líneas. Al publicarlo como código abierto bajo la licencia Apache 2.0, queremos ofrecer al ecosistema en general una opción que antes no existía: emparejar un agente de programación y un editor ligero con una navegación enriquecida en Scala y Java, a una escala en la que los servidores de lenguaje JVM existentes no ofrecían un camino viable.
(Esta entrada del blog ha sido traducida utilizando herramientas basadas en inteligencia artificial) Publicación original
Suscríbete a nuestro blog y recibe las últimas publicaciones directamente en tu bandeja de entrada.