Comment combler l'écart en matière d'intelligence de code a permis de faire fonctionner Cursor pour notre monorepo Bazel de 26 millions de lignes
La majeure partie du code chez Databricks est désormais écrite par des agents. Pour les moments où les ingénieurs doivent encore intervenir directement, ils se tournent vers des éditeurs légers qui démarrent rapidement et leur permettent de naviguer dans le code avec un minimum de configuration. Pour Scala et Java, cependant, IntelliJ est la référence depuis des années. À l'échelle de notre monorepo, c'était pratiquement le seul éditeur capable de suivre le rythme. Cet article explique comment nous avons construit une solution alternative en étendant Metals, le serveur de langage Scala très utilisé, pour lui apporter un support Java de premier ordre et lui permettre de s'adapter à la taille de notre monorepo. En collaboration avec l'équipe upstream de Metals, nous avons désormais publié Metals v2 en open source afin que toute personne disposant d'une grande base de code Java et Scala puisse associer son agent de codage à un éditeur léger.
Metals v2 est disponible dès aujourd'hui dans Cursor, VS Code et Neovim, avec des instructions d'installation disponibles sur le site web de Metals.
En mai 2025, nous avons commencé à standardiser le flux de travail quotidien des éditeurs de Databricks autour de Cursor. Cursor et VS Code étaient déjà largement utilisés chez Databricks pour le frontend et d'autres tâches non-JVM, et tous deux offraient une excellente prise en charge SSH à distance pour notre environnement de développement basé sur le cloud. Cependant, la plupart de nos services sont écrits en Scala et Java, et y naviguer à l'échelle de notre monorepo était le dernier obstacle ; le problème que nous nous sommes donné pour mission de résoudre.
La standardisation sur un seul éditeur allait bien au-delà des préférences individuelles. Une plateforme IDE unifiée crée un cercle vertueux : les équipes partagent une base commune pour l'exploration et le développement du code, tandis que l'équipe de la plateforme peut concentrer ses investissements au même endroit. Cursor est devenu le principal éditeur pour le travail JVM dans notre monorepo, et nous avons suffisamment consolidé nos outils pour ne pas renouveler la majorité de nos licences IntelliJ cette année.
Trois indicateurs montrent l'ampleur de la transition vers Cursor : l'utilisation globale de l'IDE, les événements d'ouverture de fichiers Scala et Java où Metals se trouve sur le chemin critique, et l'adoption en dehors de Databricks.
L'indicateur le plus large est l'utilisation globale de l'IDE. L'adoption de Cursor a d'abord progressé d'elle-même chez Databricks, avant de stagner en septembre 2025 pendant plusieurs mois. La croissance a repris avec le déploiement de Metals v2 et, en juillet 2026, 92 % des utilisateurs actifs hebdomadaires d'IDE ouvraient Cursor, contre 12 % pour IntelliJ. Parmi les ingénieurs qui n'utilisent qu'un seul IDE, 2 400 sont désormais sur Cursor, contre 120 pour IntelliJ.
Un indicateur plus précis que l'utilisation globale de l'IDE est le ratio de fichiers ouverts dans chaque IDE pour Scala et Java, les langages pour lesquels Metals v2 se trouve sur le chemin critique. Depuis que les premières améliorations majeures de Metals v2 ont été intégrées à Cursor en octobre 2025, la part de Cursor dans les événements d'ouverture de fichiers Scala et Java est passée de 40 % à 78 %. Cette progression est plus lente que l'adoption globale de l'IDE car c'est pour Scala et Java qu'IntelliJ était le plus solidement implanté, mais la part de Cursor continue de progresser mois après mois.
Metals v2 a commencé comme un fork de Databricks, mais l'objectif a toujours été de restituer ce travail à la communauté open source. En collaboration avec l'équipe de Cursor et les principaux mainteneurs de Metals chez VirtusLab, nous préparons une version stable de Metals v2 pour remplacer la version stable v1 actuelle.
Nous contribuons à Metals v2 pour que Cursor fonctionne de manière optimale avec des bases de code Java de plusieurs millions de lignes, en mettant l'accent sur l'amélioration du support de Bazel, du débogage et des tests.—Kevin Niparko, Cursor
L'IA change la façon dont les développeurs utilisent les IDE, et Metals v2 va dans la bonne direction : un démarrage rapide, une orientation fiable dans la base de code et une architecture conçue pour les grandes bases de code. Databricks a validé cette approche à une échelle exceptionnelle, et VirtusLab est ravi de contribuer à apporter ce travail à l'ensemble de la communauté Scala et JVM.—Krzysztof Romanowski, responsable de la productivité du développement, VirtusLab
Les discussions concernant d'autres grandes bases de code JVM ont confirmé la même tendance que celle observée chez Databricks : une demande pour Cursor, VS Code et Neovim avec une solide prise en charge SSH à distance, une pression de la plateforme en faveur d'outils unifiés, et aucune alternative crédible parmi les serveurs de langage JVM existants à l'échelle d'un monorepo.
Nous avons commencé à déployer Metals V2 chez Stripe il y a moins d'un mois, et pourtant nous sommes constamment impressionnés par son excellent fonctionnement dans notre base de code Java, par l'enthousiasme de nos ingénieurs, et par le plaisir que nous avons à collaborer avec les mainteneurs et à apprendre à leurs côtés.—Mahib Hosain, Developer Platform, Stripe
C'est le modèle que nous souhaitons pour Metals v2 : une infrastructure partagée pour les grandes bases de code JVM, maintenue de manière ouverte et façonnée par les entreprises qui en ont besoin pour fonctionner à grande échelle. Le développement continu est dirigé par VirtusLab ; n'hésitez pas à les contacter pour toute question, retour d'expérience ou contribution via le suivi des tickets GitHub ou à l'adresse metals@virtuslab.com.
Tout ce qui précède plaide en faveur de Metals v2. La suite s'adresse aux lecteurs qui souhaitent mieux comprendre l'ingénierie derrière l'intelligence de code à faible latence sur un monorepo de 26 millions de lignes.
Puisque les agents écrivent la majeure partie du code, une orientation rapide dans la base de code importait plus qu'une couverture complète de la complétion et de la refactorisation du protocole LSP (Language Server Protocol). Cela a restreint le problème, mais n'a pas rendu la solution évidente : nous devions tout de même fournir une navigation facile à configurer et à faible latence pour un monorepo Scala et Java utilisant Bazel de cette taille, et les LSP prêts à l'emploi n'étaient pas conçus pour supporter une telle échelle. Nous avons dû concevoir l'intelligence de code à l'échelle du dépôt à partir de principes fondamentaux, et considérer l'utilité au démarrage comme une métrique clé à mesurer et à améliorer.
Le Time-to-initial-intelligence (TTII) mesure la rapidité avec laquelle l'éditeur devient utile après l'ouverture du dépôt. Nous déclenchons le chronomètre au moment où le serveur de langage s'active et l'arrêtons lorsque les fonctionnalités les plus importantes sont disponibles, sans intervention de l'utilisateur. Ces fonctionnalités critiques incluent la recherche floue des symboles de l'espace de travail, l'accès à la définition et la recherche des utilisations de symboles dans l'ensemble du dépôt.
Metals v2 est un serveur de langage pour Scala et Java. Nous sommes partis de Metals v1, le serveur de langage Scala officiel, mais atteindre notre objectif de TTII a nécessité bien plus que de simples ajustements marginaux. Nous l'avons forké et avons retravaillé trois couches centrales : l'index du dépôt, les pipelines de compilation Scala et Java, et la frontière d'intégration du build.
Les sections ci-dessous détaillent chacune de ces couches.
mbt signifie Metals Build Tool, et l'index mbt est le principal moteur du TTII ou du contrat « utile immédiatement ». Il s'agit d'un index adressable par le contenu des sources de l'espace de travail : Metals utilise la commande git ls-files --stage pour découvrir les fichiers et les OID des blobs Git afin de décider quelles entrées d'index peuvent être réutilisées. Grâce aux informations à l'échelle du dépôt disponibles avant la synchronisation du build, Metals peut répondre aux questions de « premier kilomètre » :
L'index mbt est en fait une table de hachage associant un fichier source à un résumé local au fichier. Une entrée enregistre les déclarations de package du fichier, les définitions avec leurs emplacements source, et des filtres de Bloom compacts pour les identifiants référencés dans le fichier. Les définitions alimentent la recherche de symboles dans l'espace de travail et le saut à la définition, tandis que les filtres de Bloom permettent à Metals d'exclure rapidement les fichiers qui ne peuvent pas contenir de référence avant d'effectuer des vérifications plus précises. Comme chaque entrée est dérivée d'un seul fichier, les mises à jour incrémentielles restent simples : lorsqu'un fichier change, Metals recalcule l'entrée de ce fichier et la remplace.
Dans notre monorepo, l'index mbt persistant pèse 936 Mo non compressé et contient des informations sur 2,9 millions de symboles répartis sur plus de 142 000 fichiers Scala, Java et Protobuf. Une compilation de benchmark propre prend 22 secondes avec une utilisation complète du CPU sur 32 cœurs, tandis que l'analyse d'un index pré-compilé à partir du disque prend 5 secondes. En production, nous mesurons le TTII comme le temps nécessaire pour démarrer le serveur, charger un index mbt obsolète, le mettre à jour par rapport au dernier état de git ls-files --stage, et redémarrer les compilateurs de présentation Scala et Java : p50 8,7 s, p90 36,7 s. La recherche floue de symboles parmi 2,9 millions de symboles de l'espace de travail est de p50 10 ms, p90 95 ms. Il est possible de réduire encore le TTII, mais avec ces chiffres, ce n'est pas le goulot d'étranglement que nous devons traiter en priorité.
Le pipeline Scala est construit autour du compilateur de présentation, un mode du vérificateur de types Scala qui met en cache et réutilise les informations de la table des symboles d'une compilation à l'autre. Cette réutilisation, combinée à la résolution paresseuse des symboles du compilateur, permet à une seule instance de conserver l'intégralité de la base de code Scala de 24 millions de lignes à portée, tout en publiant des diagnostics à p50 0,9 s, p90 8,9 s.
Cette instance unique du compilateur Scala fonctionne selon l'un des deux modes suivants, en fonction de la quantité d'informations dont dispose Metals de la part du serveur de build concernant le fichier en cours d'édition. Avant une synchronisation du build, un compilateur de secours adopte une approche permissive, traitant chaque fichier source du dépôt comme un candidat de dépendance éligible, ce qui rend la navigation immédiatement utile, même dans du code qui ne compile pas encore dans Bazel. Après une synchronisation du build, un compilateur précis se limite aux frontières du classpath et du sourcepath signalées par le serveur de build, ce qui garantit l'exactitude de ses diagnostics et de ses informations de dépendance par rapport au build. Les modes précis et de secours s'appuient tous deux sur les deux mêmes techniques pour maintenir un sourcepath de cette taille gérable :
Le mode précis ajoute une troisième technique : il conserve les sources des dépendances transitives sur le sourcepath, de sorte que les modifications apportées à des fichiers provenant de différentes cibles Bazel soient immédiatement reflétées dans l'éditeur sans attendre que Bazel produise un nouveau classpath, une contrainte qui freinait Metals v1.
Conserver une base de code de 24 millions de lignes dans une seule instance de compilateur de présentation va bien au-delà de ce pour quoi le compilateur Scala a été conçu à l'origine. Metals v2 y parvient grâce à deux modes de compilateur basés sur un ensemble partagé de techniques pour mettre à l'échelle le sourcepath. Le saut à la définition, la fonctionnalité la plus utilisée de l'éditeur, s'exécute sur l'ensemble du dépôt à p50 7 ms, p90 575 ms. Le Scala étant le langage dans lequel travaillent la plupart de nos ingénieurs, ce pipeline était l'élément clé qui devait fonctionner pour que Cursor devienne une véritable alternative à IntelliJ dans notre monorepo.
Metals v2 implémente la surface LSP Java directement sur les API de javac. Nous avons envisagé de réutiliser un serveur de langage Java existant (implémentations basées sur JDT et NetBeans), mais tous deux sont centrés sur le build de la même manière que Metals v1, le couplage exact que la v2 visait à éliminer. S'appuyer sur javac a plutôt permis à Java de partager le sourcepath et le modèle de synchronisation de build de Metals v2 avec Scala, qui constitue toujours la majeure partie de notre monorepo, et la surface Java dont nous avions besoin était suffisamment restreinte pour que nous puissions la maintenir nous-mêmes.
Les API de javac ont fourni un accès suffisant au compilateur pour implémenter les diagnostics, la navigation, la coloration sémantique et d'autres méthodes LSP clés, et elles ont bien résisté sur du code partiellement cassé, ce qui est important pour le flux de travail d'un éditeur interactif. Comme pour Scala, nous avons délibérément restreint la portée des fonctionnalités d'édition active telles que les refactorisations et les complétions.
Le principal problème d'évolutivité de cette architecture est apparu dans des fichiers qui déclenchaient des performances pathologiques lors de la phase « enter » de javac en important de manière transitive des millions de lignes de code au niveau de la couche de plan des symboles. Metals v2 résout ce problème avec un mode javaSymbolLoader: "turbine-classpath", activé par défaut, qui utilise une version modifiée du compilateur d'en-têtes Turbine pour produire un classpath de dépôt fonctionnel dans un environnement IDE, y compris une gestion fluide des erreurs de résolution de noms. Turbine traite près d'un million de lignes de code Java par seconde sur un seul thread, ce qui signifie que Metals peut recompiler l'ensemble de la base de code Java à intervalles réguliers. Cela permet de conserver presque tous les symboles inter-fichiers sur le classpath plutôt que sur le sourcepath, de sorte que la phase « analyze » de javac puisse s'exécuter au plus près de sa limite pratique pour une utilisation interactive : près de 100 000 lignes de code par seconde selon nos benchmarks.
« Utile avant la synchronisation du build » ne signifie pas « ignorer le build ». Metals a toujours besoin de la fidélité du graphe de build pour de nombreuses fonctionnalités secondaires, notamment la navigation vers des dépendances tierces ou des sources générées, le respect des règles de shading personnalisées, la découverte de suites de tests et la configuration automatique des lanceurs de débogage. Chez Databricks, cela signifie rendre les métadonnées de 285 000 cibles JVM Bazel interrogeables avec les latences de l'éditeur.
Notre implémentation en production de cette couche est un serveur BSP interne écrit en Go et adapté à nos règles Bazel. Ce serveur BSP ne fait pas partie de cette version open-source, mais ses choix de conception méritent tout de même d'être transposés à d'autres implémentations BSP de Bazel.
Tout d'abord, Metals v2 n'invoque jamais Bazel via le serveur BSP à moins que l'utilisateur ne le demande explicitement. Dans un grand monorepo, la synchronisation de l'IDE en arrière-plan peut s'emparer du verrou Bazel et entrer en concurrence avec les builds lancés par les développeurs. La synchronisation du build est donc une action explicite de l'utilisateur plutôt qu'un comportement au démarrage ou une tâche de maintenance en arrière-plan. Les métadonnées obtenues sont stockées dans un instantané JSON dont la taille s'adapte grâce à la mutualisation des constantes pour les libellés, chemins et préfixes de dépôt répétés. Lorsque les utilisateurs se synchronisent, le serveur BSP ajoute progressivement de nouvelles cibles à cet instantané et fournit les métadonnées mises à jour via BSP.
Deuxièmement, il n'y a pas de format de configuration de synchronisation partagé. Les utilisateurs synchronisent des fichiers ou des répertoires individuels à la demande au fur et à mesure de leur édition pour améliorer la navigation ou la précision des diagnostics. D'après notre expérience, les ensembles de synchronisation prédéfinis s'étoffent avec le temps, sont copiés d'une équipe à l'autre et finissent par être plus lents que la synchronisation ciblée dont le développeur a réellement besoin.
Ensemble, l'index sans build, les pipelines s'appuyant sur le compilateur et l'intégration du build axée sur les métadonnées offrent une intelligence de code à faible latence dans des bases de code Bazel de plusieurs millions de lignes. En le publiant en open-source sous licence Apache 2.0, nous voulons offrir à l'écosystème au sens large un choix qui n'existait pas auparavant : associer un agent de codage et un éditeur léger à une navigation Scala et Java riche, à une échelle où les serveurs de langage JVM existants n'offraient aucune solution crédible.
(Cet article de blog a été traduit à l'aide d'outils basés sur l'intelligence artificielle) Article original
Abonnez-vous à notre blog et recevez les derniers articles directement dans votre boîte mail.