Direkt zum Hauptinhalt
Plattform

Open-Sourcing von Metals v2: Databricks’ Java- und Scala-Language-Server für Codebasen mit mehreren Millionen Zeilen

Wie das Schließen der Code-Intelligence-Lücke Cursor für unser 26-Millionen-Zeilen-Bazel-Monorepo nutzbar machte

von Ólafur Páll Geirsson, Iulian Dragos und Alessandro Patti

  • Metals v2 denkt Code-Intelligence für große Monorepos neu, die über herkömmliche JVM-Language-Server hinausgewachsen sind.
  • Metals ist seit Jahren der offizielle Scala-Language-Server. Mit der vollständigen Java-Unterstützung in v2 testen Unternehmen mit einigen der größten Java-Codebasen der Branche Metals nun allein für Java.
  • KI verändert den relativen Wert von LSP-Funktionen (Language Server Protocol). Da Entwickler weniger Code von Hand schreiben, sind geringer Einrichtungsaufwand, schnelles Feedback und eine zuverlässige Orientierung in der Codebasis wichtiger als eine breite Autovervollständigung und Refactoring-Abdeckung.

Der meiste Code bei Databricks wird mittlerweile von Agenten geschrieben. In den Momenten, in denen Engineers noch selbst Hand anlegen, greifen sie zu leichtgewichtigen Editoren, die schnell starten und mit denen sie den Code ohne großen Einrichtungsaufwand durchsuchen können. Für Scala und Java setzt IntelliJ jedoch seit Jahren den Standard. Bei der Größe unseres Monorepos war es praktisch der einzige Editor, der mithalten konnte. In diesem Beitrag zeigen wir, wie wir eine alternative Lösung entwickelt haben, indem wir Metals, den weit verbreiteten Scala-Language-Server, um erstklassigen Java-Support erweitert und für die Größe unseres Monorepos skaliert haben. In Zusammenarbeit mit dem Upstream-Metals-Team haben wir Metals v2 nun als Open Source veröffentlicht, sodass jeder mit einer großen Java- und Scala-Codebasis seinen Coding-Agenten mit einem leichtgewichtigen Editor kombinieren kann.

Metals v2 ist ab heute in Cursor, VS Code und Neovim verfügbar. Installationsanweisungen finden Sie auf der Metals-Website.

Das IDE-Schwungrad aufbauen

Im Mai 2025 haben wir damit begonnen, den täglichen Editor-Workflow bei Databricks auf Cursor zu standardisieren. Cursor und VS Code wurden bei Databricks bereits häufig für das Frontend und andere Nicht-JVM-Arbeiten verwendet, und beide boten eine starke SSH-Remote-Unterstützung für unsere cloudbasierte Entwicklungsumgebung. Die meisten unserer Dienste sind jedoch in Scala und Java geschrieben, und das Navigieren darin in der Größenordnung unseres Monorepos war das verbleibende Hindernis – das Problem, das wir lösen wollten.

Die Standardisierung auf einen einzigen Editor war über persönliche Vorlieben hinaus wichtig. Eine einheitliche IDE-Plattform schafft ein Schwungrad: Teams teilen eine gemeinsame Basis für die Code-Exploration und -Entwicklung, während das Plattform-Team seine Investitionen an einem Ort bündeln kann. Cursor wurde zum primären Editor für JVM-Arbeiten in unserem Monorepo, und wir haben so weit konsolidiert, dass wir den Großteil unserer IntelliJ-Lizenzen in diesem Jahr nicht verlängert haben.

image2.png
Abbildung 1: Eine breitere Akzeptanz erhöht die Hebelwirkung von Plattform-Investitionen, was die gemeinsame IDE verbessert und die Akzeptanz weiter stärkt.

Drei Signale zeigen, wie weit der Wechsel zu Cursor ging: die allgemeine IDE-Nutzung, Ereignisse beim Öffnen von Scala- und Java-Dateien, bei denen Metals auf dem kritischen Pfad liegt, und die Akzeptanz außerhalb von Databricks.

IDE-Nutzung

Das deutlichste Signal ist die allgemeine IDE-Nutzung. Die Einführung von Cursor wuchs bei Databricks zunächst von selbst, stagnierte dann aber bis September 2025 und hielt sich über Monate auf diesem Niveau. Mit dem Rollout von Metals v2 wurde das Wachstum fortgesetzt, und bis Juli 2026 öffneten 92 % der wöchentlich aktiven IDE-Nutzer Cursor, verglichen mit 12 % bei IntelliJ. Unter den Engineers, die nur eine einzige IDE nutzen, arbeiten mittlerweile 2,4 Tsd. mit Cursor, verglichen mit 120 bei IntelliJ.

image3.png
Abbildung 2: IDE-Nutzung, April 2025 – Juli 2026. Die schwarze Linie markiert das erste interne Metals v2-Release, das die Scala- und Java-Navigation auf einen von Metals bereitgestellten Sourcepath verlagert hat (Deep Dive unten).

Ereignisse beim Öffnen von Scala- und Java-Dateien

Ein genaueres Signal als die allgemeine IDE-Nutzung ist das Verhältnis der in der jeweiligen IDE geöffneten Dateien für Scala und Java – also den Sprachen, bei denen Metals v2 auf dem kritischen Pfad liegt. Seit die ersten großen Verbesserungen von Metals v2 im Oktober 2025 in Cursor Einzug gehalten haben, ist der Anteil von Cursor an den Ereignissen beim Öffnen von Scala- und Java-Dateien von 40 % auf 78 % gestiegen. Der Anstieg ist langsamer als die gesamte IDE-Einführung, da IntelliJ bei Scala und Java am stärksten etabliert war, aber der Anteil von Cursor verzeichnet weiterhin einen monatlichen Aufwärtstrend.

image1.png
Abbildung 3: Ereignisse beim Öffnen von Scala- und Java-Dateien nach IDE, April 2025 – Juli 2026.

Frühe externe Akzeptanz

Metals v2 begann als ein Fork von Databricks, aber das Ziel war immer, die Arbeit wieder in die Open-Source-Community einzubringen. Zusammen mit dem Cursor-Team und den Haupt-Maintainern von Metals bei VirtusLab bereiten wir ein stabiles Metals v2-Release vor, das die aktuelle stabile Version v1 ersetzen soll.

Wir tragen zu Metals v2 bei, damit Cursor auch bei Java-Codebasen mit mehreren Millionen Zeilen gut funktioniert. Der Fokus liegt dabei auf der Verbesserung der Bazel-Unterstützung, des Debuggings und des Testens.—Kevin Niparko, Cursor
AI verändert die Art und Weise, wie Entwickler IDEs nutzen, und Metals v2 geht in die richtige Richtung: schneller Start, zuverlässige Orientierung in der Codebasis und eine Architektur, die für große Codebasen ausgelegt ist. Databricks hat diesen Ansatz in einem außergewöhnlichen Maßstab validiert, und VirtusLab freut sich darauf, diese Arbeit der breiteren Scala- und JVM-Community zugänglich zu machen.—Krzysztof Romanowski, Head of Development Productivity, VirtusLab

Gespräche mit Verantwortlichen anderer großer JVM-Codebasen haben dasselbe Muster bestätigt, das wir auch bei Databricks beobachtet haben: die Nachfrage nach Cursor, VS Code und Neovim mit starker SSH-Remote-Unterstützung, Druck von Plattformseite hin zu vereinheitlichten Tools und kein praktikabler Weg über bestehende JVM-Language-Server in der Größenordnung von Monorepos.

Wir haben vor weniger als einem Monat mit dem Rollout von Metals V2 bei Stripe begonnen, und wir sind bereits jetzt tief beeindruckt davon, wie gut es in unserer Java-Codebasis funktioniert, wie begeistert unsere Engineers sind und wie viel Freude es macht, mit den Maintainern zusammenzuarbeiten und von ihnen zu lernen.—Mahib Hosain, Developer Platform, Stripe

Das ist das Modell, das wir uns für Metals v2 wünschen: eine gemeinsame Infrastruktur für große JVM-Codebasen, die offen gepflegt und von den Unternehmen mitgestaltet wird, die sie im großen Stil benötigen. Die laufende Entwicklung wird von VirtusLab geleitet. Wenden Sie sich bei Fragen, Feedback oder Beiträgen über den GitHub-Issue-Tracker oder per E-Mail an metals@virtuslab.com an sie.

Deep Dive: Wie Metals v2 skaliert

Das alles spricht für Metals v2. Der Rest richtet sich an Leserinnen und Leser, die die Technik hinter der Code-Intelligence mit geringer Latenz in einem Monorepo mit 26 Millionen Zeilen besser verstehen möchten.

Da Agenten den Großteil des Codes schreiben, war eine schnelle Orientierung in der Codebasis wichtiger als eine umfassende Abdeckung von Autovervollständigung und Refactoring über das Language Server Protocol (LSP). Das schränkte das Problem zwar ein, machte die Lösung jedoch nicht offensichtlich: Wir mussten immer noch eine einfach einzurichtende Navigation mit geringer Latenz für ein so großes Scala- und Java-Bazel-Monorepo bereitstellen, und Standard-LSPs waren nicht für diese Größenordnung ausgelegt. Wir mussten eine Code-Intelligence auf Repository-Ebene von Grund auf neu entwickeln und die Nützlichkeit beim Start als eine Schlüsselmetrik betrachten, die wir messen und verbessern konnten.

Definition der Time-to-Initial-Intelligence (TTII)

Die Time-to-Initial-Intelligence (TTII) misst, wie schnell der Editor nach dem Öffnen des Repositorys einsatzbereit ist. Wir starten die Zeitmessung, wenn der Language-Server aktiviert wird, und stoppen sie, wenn die wichtigsten Funktionen verfügbar sind – ohne dass der Benutzer eingreifen muss. Zu diesen kritischen Funktionen gehören die unscharfe Suche nach Workspace-Symbolen (Fuzzy Search), die Definitionssuche (Jump-to-Definition) und das Finden von Symbolverwendungen im gesamten Repository.

Die drei Schichten von Metals v2

Metals v2 ist ein Language-Server für Scala und Java. Wir sind von Metals v1 ausgegangen, dem offiziellen Scala-Language-Server, aber das Erreichen unseres TTII-Ziels erforderte mehr als nur ein paar kleine Anpassungen. Wir haben das Projekt geforkt und drei zentrale Schichten überarbeitet: den Repo-Index, die Compiler-Pipelines für Scala und Java sowie die Schnittstelle zur Build-Integration.

  1. Build-freier Repo-Index: Metals v2 entfernt das Build Server Protocol (BSP) aus dem kritischen Pfad beim Start, indem es Workspace-Quellen direkt über seinen eigenen mbt-Index indiziert, der im folgenden Abschnitt beschrieben wird. Der entscheidende Unterschied zu Metals v1 besteht darin, dass Metals nun das initiale Projektmodell selbst besitzt, anstatt darauf zu warten, dass der Build-Server es bereitstellt.
  2. Compiler-gestützte interaktive Pipelines für Scala und Java: Metals v2 verlagert das Laden von Symbolen von einem vom Build bereitgestellten Classpath in einen von Metals bereitgestellten Sourcepath. Dadurch spiegeln Diagnose und Navigation den tatsächlichen Code auf der Festplatte genauer wider als ein veralteter Snapshot der letzten erfolgreichen Kompilierung. Dies erforderte ein Überdenken einer Vielzahl von Kernannahmen aus der v1-Codebasis.
  3. Metadaten-fokussierte Build-Integration: Metals v2 verwendet weiterhin BSP, jedoch mit einer engeren Vereinbarung. Metals v1 nutzte den Build-Server auf dem Hot-Path des Editors für Diagnosen, während Metals v2 Routine-Diagnosen aus dem Build-Server auslagert und BSP hauptsächlich zur Abfrage von Build-Metadaten verwendet: Abhängigkeiten, generierte Quellen, Test-Discovery und Debug-Launcher. Diese Umstellung senkt die Hürde für die Implementierung eines BSP-Servers für Metals v2, und unser interner Bazel-BSP-Server bestätigt, dass das Modell für große Bazel-Monorepos skaliert.

Die folgenden Abschnitte gehen nacheinander auf die einzelnen Schichten ein.

Der mbt-Index: Repository-weite Code-Intelligence vor dem Build-Sync

mbt steht für Metals Build Tool, und der mbt-Index ist der Hauptfaktor für TTII bzw. das Versprechen, „sofort einsatzbereit“ zu sein. Es handelt sich um einen inhaltsadressierten Index von Workspace-Quellen: Metals verwendet den Befehl git ls-files --stage, um Dateien und Git-Blob-OIDs zu finden und zu entscheiden, welche Indexeinträge wiederverwendet werden können. Da repository-weite Informationen bereits vor dem Build-Sync verfügbar sind, kann Metals grundlegende Fragen direkt beantworten:

  • Diagnosen für dateiübergreifende Referenzen
  • unscharfe Symbolsuche (Fuzzy Search)
  • Definitionssuche (Jump-to-Definition) im gesamten Repository, jedoch nicht für externe Abhängigkeiten oder generierten Code
  • umfassende Suche nach Referenzen und Implementierungen durch den kreativen Einsatz von Bloom-Filtern pro Dokument

Der mbt-Index ist im Grunde eine Hash-Map von der Quelldatei zu einer dateilokalen Zusammenfassung. Ein Eintrag erfasst die Paketdeklarationen der Datei, Definitionen mit Quellpfaden und kompakte Bloom-Filter für in der Datei referenzierte Bezeichner. Die Definitionen ermöglichen die Arbeitsbereich-Symbolsuche und „Gehe zu Definition“, während die Bloom-Filter es Metals erlauben, Dateien, die keine Referenz enthalten können, schnell auszuschließen, bevor präzisere Prüfungen durchgeführt werden. Da jeder Eintrag nur von einer einzigen Datei abgeleitet wird, bleiben inkrementelle Aktualisierungen unkompliziert: Wenn sich eine Datei ändert, berechnet Metals den Eintrag dieser Datei neu und ersetzt ihn.

In unserem Monorepo ist der persistierte mbt-Index unkomprimiert 936 MB groß und enthält Informationen zu 2,9 Mio. Symbolen in über 142.000 Scala-, Java- und Protobuf-Dateien. Ein sauberer Benchmark-Build dauert 22 Sekunden bei voller CPU-Auslastung auf 32 Kernen, während das Parsen eines vorgefertigten Index von der Festplatte 5 Sekunden dauert. In der Produktion messen wir die TTII als die Zeit, die benötigt wird, um den Server zu starten, einen veralteten mbt-Index zu laden, ihn mit dem neuesten git ls-files --stage-Status zu aktualisieren und die Scala- und Java-Presentation-Compiler neu zu starten: p50 8,7 s, p90 36,7 s. Die Fuzzy-Symbolsuche über 2,9 Mio. Arbeitsbereich-Symbole hinweg liegt bei p50 10 ms, p90 95 ms. Es gibt noch Spielraum, um die TTII weiter zu reduzieren, aber bei diesen Werten ist dies nicht der Engpass, den wir als Nächstes angehen müssen.

Scala-Pipeline: 24 Mio. Zeilen Code auf einer einzigen Compiler-Instanz verarbeiten

Die Scala-Pipeline basiert auf dem Presentation-Compiler, einem Modus des Scala-Type-Checkers, der Symboltabelleninformationen über Compilerläufe hinweg zwischenspeichert und wiederverwendet. Diese Wiederverwendung, kombiniert mit der verzögerten Symbolauflösung des Compilers, ermöglicht es einer einzigen Instanz, die gesamte Scala-Codebasis mit 24 Mio. Zeilen im Scope zu behalten, während Diagnosen mit p50 0,9 s, p90 8,9 s ausgegeben werden.

Diese einzelne Scala-Compiler-Instanz läuft in einem von zwei Modi, je nachdem, wie viele Informationen Metals vom Build-Server über die gerade bearbeitete Datei hat. Vor einer Build-Synchronisierung nimmt ein Fallback-Compiler eine tolerante Haltung ein und behandelt jede Quelldatei im Repository als potenziellen Abhängigkeitskandidaten. Dadurch ist die Navigation sofort nutzbar, selbst bei Code, der in Bazel noch nicht kompiliert werden kann. Nach einer Build-Synchronisierung beschränkt sich ein präziser Compiler auf die vom Build-Server gemeldeten Classpath- und Sourcepath-Grenzen, was seine Diagnosen und Abhängigkeitsinformationen build-genau macht. Sowohl der präzise als auch der Fallback-Modus stützen sich auf dieselben zwei Techniken, um einen so großen Sourcepath handhabbar zu machen:

  • Outline-Modus für nicht geöffnete Quellen. Quellen, die nicht in einem Editor geöffnet sind, werden vor der Typprüfung um ihre Methodenrümpfe bereinigt. Dies bewahrt die vom Compiler benötigten Typsignaturen und vermeidet gleichzeitig Arbeit in den Methodenrümpfen, in denen die meiste Zeit für die Typprüfung aufgewendet wird.
  • Ein In-Memory-Quelllayout-Index. Da Scala-Quellpfade die Paketstruktur nicht zuverlässig codieren, führt Metals einen Parser aus, um einen leichtgewichtigen Index zu erstellen, der Klassen den Quellpfaden zuordnet. Anschließend werden Symbole über diesen Index geladen, anstatt das Dateisystem zu scannen. Der Fallback-Compiler erstellt diesen Index in weniger als 50 ms direkt aus dem mbt-Index und verwendet dabei die bereits vorhandenen repository-weiten Daten von Metals wieder, anstatt sie erneut aus der Quelle zu parsen.

Der präzise Modus fügt eine dritte Technik hinzu: Er belässt transitive Abhängigkeitsquellen auf dem Sourcepath, sodass Änderungen an Dateien aus verschiedenen Bazel-Targets sofort im Editor widergespiegelt werden, ohne darauf warten zu müssen, dass Bazel einen neuen Classpath erstellt – eine Einschränkung, die Metals v1 noch ausbremste.

Eine Codebasis mit 24 Mio. Zeilen in einer einzigen Presentation-Compiler-Instanz zu halten, liegt weit außerhalb dessen, wofür der Scala-Compiler ursprünglich entwickelt wurde. Metals v2 erreicht dies durch zwei Compiler-Modi, die auf einer gemeinsamen Reihe von Techniken zur Skalierung des Sourcepaths basieren. „Gehe zu Definition“, die am häufigsten genutzte Funktion des Editors, läuft über das gesamte Repository hinweg mit p50 7 ms, p90 575 ms. Da Scala die Sprache ist, in der die meisten unserer Entwickler arbeiten, war diese Pipeline das entscheidende Puzzleteil, das funktionieren musste, damit Cursor in unserem Monorepo zu einer echten Alternative zu IntelliJ werden konnte.

Java-Pipeline: Eine Million Zeilen Java-Code pro Sekunde mit Turbine verarbeiten

Metals v2 implementiert die Java-LSP-Oberfläche direkt auf den APIs von javac. Wir haben die Wiederverwendung eines bestehenden Java-Language-Servers (JDT- und NetBeans-basierte Implementierungen) evaluiert, aber beide sind in gleicher Weise build-zentriert wie Metals v1 – genau die Kopplung, die v2 beseitigen sollte. Die Entwicklung auf Basis von javac ermöglichte es Java stattdessen, das Sourcepath- und Build-Synchronisierungsmodell von Metals v2 mit Scala zu teilen, das nach wie vor den größten Teil unseres Monorepos ausmacht, und die von uns benötigte Java-Oberfläche war klein genug, um sie selbst zu warten.

Die APIs von javac boten ausreichend Compiler-Zugriff, um Diagnosen, Navigation, semantisches Highlighting und andere wichtige LSP-Methoden zu implementieren, und sie bewährten sich auch bei teilweise fehlerhaftem Code, was für einen interaktiven Editor-Workflow wichtig ist. Wie bei Scala haben wir den Umfang aktiver Bearbeitungsfunktionen wie Refactorings und Vervollständigungen bewusst schmal gehalten.

Das Hauptproblem bei der Skalierung dieser Architektur trat bei Dateien auf, die ein extrem schlechtes Performance-Verhalten in der javac-„Enter“-Phase auslösten, indem sie transitiv Millionen von Codezeilen auf der Symbol-Outline-Ebene importierten. Metals v2 löst dies mit einem standardmäßig aktivierten javaSymbolLoader: "turbine-classpath"-Modus, der eine modifizierte Version des Turbine-Header-Compilers verwendet, um einen Repository-Classpath zu erstellen, der in einer IDE-Umgebung funktioniert, einschließlich der fehlertoleranten Behandlung von Namensauflösungsfehlern. Turbine verarbeitet fast eine Million Zeilen Java-Code pro Sekunde auf einem einzigen Thread, was bedeutet, dass Metals die gesamte Java-Codebasis in regelmäßigen Abständen neu kompilieren kann. Dadurch verbleiben fast alle dateiübergreifenden Symbole auf dem Classpath statt auf dem Sourcepath, sodass die javac-„Analyze“-Phase nahe an ihrer praktischen Grenze für die interaktive Nutzung ausgeführt werden kann: fast 100.000 Zeilen Code pro Sekunde laut unseren Benchmarks.

Build-Integration: Abfrage von 285.000 Bazel-Targets mit Editor-Latenzen

„Nützlich vor der Build-Synchronisierung“ bedeutet nicht „Ignorieren des Builds“. Metals benötigt weiterhin eine hohe Genauigkeit des Build-Graphen für eine Vielzahl von Funktionen, darunter die Navigation zu Drittanbieter-Abhängigkeiten oder generierten Quellen, die Einhaltung benutzerdefinierter Shading-Regeln, das Erkennen von Test-Suites und die automatische Konfiguration von Debug-Launchern. Bei Databricks bedeutet dies, dass Metadaten von 285.000 Bazel-JVM-Targets mit Editor-Latenzen abfragbar sein müssen.

Unsere Produktionsimplementierung dieser Ebene ist ein interner BSP-Server, der in Go geschrieben und auf unsere Bazel-Regeln zugeschnitten ist. Dieser BSP-Server ist nicht Teil dieses Open-Source-Releases, aber seine Designentscheidungen sind es dennoch wert, auf andere Bazel-BSP-Implementierungen übertragen zu werden.

Erstens ruft Metals v2 Bazel niemals über den BSP-Server auf, es sei denn, der Benutzer fordert dies explizit an. In einem großen Monorepo kann die IDE-Synchronisierung im Hintergrund die Bazel-Sperre beanspruchen und mit von Entwicklern initiierten Builds konkurrieren. Daher ist die Build-Synchronisierung eine explizite Benutzeraktion und kein Startverhalten oder eine Hintergrundwartungsaufgabe. Die resultierenden Metadaten werden in einem JSON-Snapshot gespeichert, der durch Constant-Pooling für wiederholte Labels, Pfade und Repository-Präfixe skaliert. Wenn Benutzer synchronisieren, fügt der BSP-Server diesem Snapshot inkrementell weitere Targets hinzu und stellt die aktualisierten Metadaten über BSP bereit.

Zweitens gibt es kein gemeinsames Format für die Synchronisationskonfiguration. Benutzer synchronisieren einzelne Dateien oder Verzeichnisse bei Bedarf während der Bearbeitung, um die Navigation oder die Genauigkeit der Diagnosen zu verbessern. Unserer Erfahrung nach wachsen vordefinierte Synchronisationssets im Laufe der Zeit, werden zwischen Teams kopiert und sind letztendlich langsamer als die gezielte Synchronisierung, die der Entwickler tatsächlich benötigt.

Zusammenführung der drei Ebenen

Zusammen bieten der build-freie Index, die compilergestützten Pipelines und die metadatenfokussierte Build-Integration Code-Intelligence mit geringer Latenz in Bazel-Codebasen mit mehreren Millionen Zeilen. Durch die Veröffentlichung als Open Source unter Apache 2.0 möchten wir dem breiteren Ökosystem eine Option bieten, die es zuvor nicht gab: einen Coding-Agenten und einen leichtgewichtigen Editor mit einer umfassenden Scala- und Java-Navigation zu kombinieren, und das in einer Größenordnung, bei der bestehende JVM-Language-Server keinen praktikablen Weg boten.

(Dieser Blogbeitrag wurde mit KI-gestützten Tools übersetzt.) Originalbeitrag

Erhalten Sie die neuesten Beiträge in Ihrem Posteingang

Abonnieren Sie unseren Blog und erhalten Sie die neuesten Beiträge direkt in Ihren Posteingang.