Passa al contenuto principale
Piattaforma

Rilascio open source di Metals v2: il language server Java e Scala di Databricks per codebase con milioni di righe

Come colmare il divario di code intelligence ha permesso di far funzionare Cursor per il nostro monorepo Bazel da 26 milioni di righe

di Ólafur Páll Geirsson, Iulian Dragos e Alessandro Patti

  • Metals v2 ripensa la code intelligence per i monorepo su larga scala che hanno superato i tradizionali language server JVM.
  • Metals è stato il language server ufficiale di Scala per anni. Con il supporto completo di Java nella v2, aziende con alcune delle codebase Java più grandi del settore stanno ora testando Metals solo per Java.
  • L'AI cambia il valore relativo delle funzionalità del Language Server Protocol (LSP). Poiché gli ingegneri scrivono meno codice a mano, un basso costo di configurazione, un feedback rapido e un orientamento affidabile della codebase contano più di un'ampia copertura di completamento automatico e refactoring.

La maggior parte del codice in Databricks è ora scritta da agenti. Per i momenti in cui gli ingegneri lavorano ancora direttamente sul codice, scelgono editor leggeri che si avviano rapidamente e consentono di esplorare il codice con una configurazione minima. Per Scala e Java, tuttavia, IntelliJ ha rappresentato lo standard per anni. Alla scala del nostro monorepo, era di fatto l'unico editor in grado di tenere il passo. Questo post spiega come abbiamo creato una soluzione alternativa estendendo Metals, il server di linguaggio Scala ampiamente utilizzato, per offrire un supporto Java di prim'ordine e scalare per le dimensioni del nostro monorepo. In collaborazione con il team upstream di Metals, abbiamo ora reso open source Metals v2, in modo che chiunque disponga di una base di codice Java e Scala di grandi dimensioni possa associare il proprio agente di codifica a un editor leggero.

Metals v2 è disponibile oggi in Cursor, VS Code e Neovim, con le istruzioni di installazione disponibili sul sito web di Metals.

Costruire il volano dell'IDE

Nel maggio 2025, abbiamo iniziato a standardizzare il flusso di lavoro quotidiano degli editor di Databricks attorno a Cursor. Cursor e VS Code erano già ampiamente utilizzati in Databricks per il frontend e altri lavori non JVM, ed entrambi offrivano un solido supporto remoto SSH per il nostro ambiente di sviluppo basato su cloud. Tuttavia, la maggior parte dei nostri servizi è scritta in Scala e Java, e l'esplorazione di questi servizi alla scala del nostro monorepo era l'ultimo ostacolo; il problema che ci eravamo proposti di risolvere.

La standardizzazione su un unico editor era importante al di là delle preferenze individuali. Una piattaforma IDE unificata crea un volano: i team condividono un'unica base per l'esplorazione e lo sviluppo del codice, mentre il team della piattaforma può concentrare gli investimenti in un unico punto.

Cursor è diventato l'editor principale per il lavoro JVM nel nostro monorepo, e abbiamo consolidato a tal punto da non rinnovare la maggior parte delle nostre licenze IntelliJ quest'anno.

image2.png
Figura 1: Un'adozione più ampia aumenta l'effetto leva degli investimenti nella piattaforma, migliorando l'IDE condiviso e rafforzando l'adozione.

Tre segnali mostrano quanto sia stato profondo il passaggio a Cursor: l'utilizzo complessivo dell'IDE, gli eventi di apertura dei file Scala e Java in cui Metals si trova sul percorso critico e l'adozione al di fuori di Databricks.

Utilizzo dell'IDE

Il segnale più ampio è l'utilizzo complessivo dell'IDE. L'adozione di Cursor è cresciuta spontaneamente in Databricks, ma si è arrestata entro settembre 2025, rimanendo stabile per mesi. La crescita è ripresa con il rollout di Metals v2 ed entro luglio 2026, il 92% degli utenti attivi settimanali dell'IDE apriva Cursor rispetto al 12% di IntelliJ. Tra gli ingegneri che utilizzano un solo IDE, 2.4k usano ora Cursor rispetto ai 120 di IntelliJ.

image3.png
Figura 2: Utilizzo dell'IDE, aprile 2025 – luglio 2026. La linea nera indica la prima release interna di Metals v2, che ha spostato l'esplorazione di Scala e Java su un sourcepath fornito da Metals (approfondimento di seguito).

Eventi di apertura dei file Scala e Java

Un segnale più preciso rispetto all'utilizzo complessivo dell'IDE è la percentuale di file aperti in ciascun IDE per Scala e Java, i linguaggi in cui Metals v2 si trova sul percorso critico. Da quando i primi importanti miglioramenti di Metals v2 sono arrivati in Cursor nell'ottobre 2025, la quota di eventi di apertura dei file Scala e Java di Cursor è passata dal 40% al 78%. La salita è più lenta rispetto all'adozione complessiva dell'IDE perché Scala e Java sono i linguaggi in cui IntelliJ era più radicato, ma la quota di Cursor continua a registrare un trend di crescita mese su mese.

image1.png
Figura 3: Eventi di apertura dei file Scala e Java per IDE, aprile 2025 – luglio 2026.

Prima adozione esterna

Metals v2 è nato come fork di Databricks, ma l'obiettivo è sempre stato quello di riportare il lavoro alla community open source. Insieme al team di Cursor e ai manutentori principali di Metals presso VirtusLab, stiamo preparando una versione stabile di Metals v2 che sostituirà l'attuale versione stabile v1.

Stiamo contribuendo a Metals v2 per fare in modo che Cursor funzioni bene per basi di codice Java con milioni di righe, concentrandoci sul miglioramento del supporto per Bazel, sul debug e sui test.—Kevin Niparko, Cursor
L'AI sta cambiando il modo in cui gli sviluppatori utilizzano gli IDE, e Metals v2 si muove nella direzione giusta: avvio rapido, orientamento affidabile all'interno della base di codice e un'architettura progettata per codebase di grandi dimensioni. Databricks ha convalidato questo approccio su scala eccezionale e VirtusLab è entusiasta di contribuire a portare questo lavoro alla community più ampia di Scala e JVM.—Krzysztof Romanowski, Head of Development Productivity, VirtusLab

I confronti con altre grandi codebase JVM hanno confermato lo stesso pattern riscontrato in Databricks: richiesta di Cursor, VS Code e Neovim con un solido supporto remoto SSH, pressione della piattaforma verso strumenti unificati e nessuna strada credibile attraverso i server di linguaggio JVM esistenti alla scala del monorepo.

Abbiamo iniziato a distribuire Metals V2 in Stripe meno di un mese fa, eppure siamo costantemente colpiti da come funzioni bene nella nostra codebase Java, dall'entusiasmo dei nostri ingegneri e da quanto sia piacevole collaborare con i manutentori e imparare da loro.—Mahib Hosain, Developer Platform, Stripe

Questo è il modello che vogliamo per Metals v2: un'infrastruttura condivisa per grandi codebase JVM, mantenuta in modo aperto e plasmata dalle aziende che hanno bisogno che funzioni su scala.

Lo sviluppo continuo è guidato da VirtusLab; contattali per domande, feedback o contributi tramite il tracker dei problemi di GitHub o all'indirizzo metals@virtuslab.com.

Approfondimento: come scala Metals v2

Tutto ciò che è stato descritto sopra costituisce la motivazione a favore di Metals v2. Il resto è rivolto ai lettori che desiderano comprendere meglio l'ingegneria alla base dell'intelligenza del codice a bassa latenza in un monorepo da 26 milioni di righe.

Poiché gli agenti scrivono la maggior parte del codice, un orientamento rapido all'interno della codebase era più importante di una copertura completa del completamento e del refactoring del Language Server Protocol (LSP). Questo ha ristretto il problema, ma non ha reso la soluzione ovvia: dovevamo comunque fornire un'esplorazione facile da configurare e a bassa latenza per un monorepo Bazel in Scala e Java di queste dimensioni, e gli LSP pronti all'uso non erano progettati per supportare tale scala. Abbiamo dovuto creare un'intelligenza del codice su scala di repository partendo dai principi fondamentali e considerare l'utilità all'avvio come una metrica chiave da misurare e migliorare.

Definizione del time-to-initial-intelligence (TTII)

Il Time-to-initial-intelligence (TTII) misura la rapidità con cui l'editor diventa utile dopo l'apertura del repository. Avviamo il cronometro quando il server di linguaggio si attiva e lo fermiamo quando sono disponibili le funzionalità più importanti, ipotizzando che non vi sia alcun intervento da parte dell'utente. Queste funzionalità critiche includono la ricerca fuzzy dei simboli dell'area di lavoro, il passaggio alla definizione (jump-to-definition) e l'individuazione degli utilizzi dei simboli nell'intero repository.

I tre livelli di Metals v2

Metals v2 è un server di linguaggio per Scala e Java. Siamo partiti da Metals v1, il server di linguaggio ufficiale di Scala, ma raggiungere il nostro obiettivo di TTII ha richiesto molto più di una semplice ottimizzazione superficiale. Ne abbiamo creato un fork e abbiamo rielaborato tre livelli centrali: l'indice del repository, le pipeline dei compilatori Scala e Java e il confine di integrazione della build.

  1. Indice del repository build-free: Metals v2 rimuove il Build Server Protocol (BSP) dal percorso critico di avvio indicizzando direttamente i sorgenti dell'area di lavoro tramite il proprio indice mbt descritto nella sezione seguente. La differenza fondamentale rispetto a Metals v1 è che Metals ora possiede il modello di progetto iniziale invece di attendere che il server di build lo fornisca.
  2. Pipeline interattive supportate dal compilatore per Scala e Java: Metals v2 sposta il caricamento dei simboli da un classpath fornito dalla build a un sourcepath fornito da Metals, consentendo alla diagnostica e all'esplorazione di riflettere in modo più accurato il codice effettivo su disco anziché uno snapshot obsoleto dell'ultima compilazione riuscita. Ciò ha richiesto di ripensare un'ampia serie di presupposti fondamentali della codebase v1.
  3. Integrazione della build orientata ai metadati: Metals v2 utilizza ancora il BSP, ma con un contratto più limitato. Metals v1 utilizzava il server di build sul percorso critico dell'editor per la diagnostica, mentre Metals v2 sposta la diagnostica di routine fuori dal server di build e utilizza il BSP principalmente per interrogare i metadati della build: dipendenze, sorgenti generati, rilevamento dei test e launcher di debug. Questo cambiamento abbassa la barriera per l'implementazione di un server BSP per Metals v2, e il nostro server BSP Bazel interno conferma che il modello scala su monorepo Bazel di grandi dimensioni.

Le sezioni seguenti analizzano ciascun livello in dettaglio.

L'indice mbt: intelligenza a livello di repository prima della sincronizzazione della build

mbt sta per Metals Build Tool, e l'indice mbt è il principale fattore abilitante del TTII o del contratto "utile immediatamente". Si tratta di un indice indirizzato per contenuto dei sorgenti dell'area di lavoro: Metals utilizza il comando git ls-files --stage per individuare i file e gli OID dei blob Git per decidere quali voci dell'indice possono essere riutilizzate. Con le informazioni a livello di repository disponibili prima della sincronizzazione della build, Metals può rispondere alle domande del "primo miglio":

  • diagnostica per i riferimenti tra file
  • ricerca fuzzy dei simboli
  • passaggio alla definizione (jump-to-definition) all'interno del repository, escludendo le dipendenze esterne o il codice generato
  • ampie funzionalità find-references e find-implementations tramite l'uso creativo di filtri di Bloom per singolo documento

L'indice mbt è a tutti gli effetti una hash map che associa i file sorgente a un riepilogo locale del file. Una voce registra le dichiarazioni di pacchetto del file, le definizioni con le relative posizioni sorgente e filtri di Bloom compatti per gli identificatori a cui si fa riferimento nel file. Le definizioni alimentano la ricerca dei simboli nell'area di lavoro (workspace) e la funzionalità jump-to-definition, mentre i filtri di Bloom consentono a Metals di escludere rapidamente i file che non possono contenere un riferimento prima di eseguire controlli più precisi. Poiché ogni voce deriva da un solo file, gli aggiornamenti incrementali rimangono semplici: quando un file cambia, Metals ricalcola la voce di quel file e la sostituisce.

Nel nostro monorepo, l'indice mbt persistito pesa 936 MB non compresso e contiene informazioni su 2,9 milioni di simboli in oltre 142.000 file Scala, Java e Protobuf. Una build di benchmark pulita richiede 22 secondi con un utilizzo completo della CPU su 32 core, mentre l'analisi di un indice precompilato dal disco richiede 5 secondi. In produzione, misuriamo il TTII come il tempo necessario per avviare il server, caricare un indice mbt obsoleto, aggiornarlo rispetto allo stato git ls-files --stage più recente e riavviare i presentation compiler di Scala e Java: p50 8,7 s, p90 36,7 s. La ricerca fuzzy dei simboli su 2,9 milioni di simboli dell'area di lavoro richiede p50 10 ms, p90 95 ms. C'è spazio per ridurre ulteriormente il TTII, ma con questi numeri non è il collo di bottiglia prioritario da affrontare.

Pipeline Scala: gestire 24 milioni di righe di codice su una singola istanza del compilatore

La pipeline Scala è costruita attorno al presentation compiler, una modalità del type-checker di Scala che memorizza nella cache e riutilizza le informazioni della tabella dei simboli tra le varie esecuzioni di compilazione. Questo riutilizzo, combinato con la risoluzione pigra (lazy) dei simboli del compilatore, consente a una singola istanza di mantenere l'intera codebase Scala da 24 milioni di righe nell'ambito (scope), pubblicando al contempo la diagnostica a p50 0,9 s, p90 8,9 s.

Quella singola istanza del compilatore Scala viene eseguita in una di due modalità, a seconda di quante informazioni Metals riceve dal build server sul file in fase di modifica. Prima di una sincronizzazione della build, un compilatore di fallback adotta un approccio permissivo, trattando ogni file sorgente nel repository come un candidato di dipendenza idoneo, rendendo la navigazione immediatamente utile, anche su codice che non viene ancora compilato in Bazel. Dopo una sincronizzazione della build, un compilatore preciso si limita ai confini di classpath e sourcepath segnalati dal build server, il che rende la diagnostica e le informazioni sulle dipendenze accurate rispetto alla build. Sia la modalità precisa che quella di fallback si basano sulle stesse due tecniche per mantenere gestibile un sourcepath così ampio:

  • Modalità outline per i sorgenti non aperti. Ai sorgenti che non sono aperti in un editor vengono rimossi i corpi dei metodi prima del controllo dei tipi (type-checking). Ciò preserva le firme dei tipi necessarie al compilatore, evitando al contempo il lavoro sui corpi dei metodi, dove viene spesa la maggior parte del tempo di type-checking.
  • Un indice del layout dei sorgenti in memoria. I percorsi dei sorgenti Scala non codificano in modo affidabile la struttura dei pacchetti, quindi Metals esegue un parser per creare un indice leggero che mappa le classi alle posizioni dei sorgenti, caricando poi i simboli tramite tale indice anziché scansionare il file system. Il compilatore di fallback crea questo indice direttamente dall'indice mbt in meno di 50 ms, riutilizzando i dati a livello di repository che Metals possiede già anziché analizzare nuovamente i sorgenti.

La modalità precisa aggiunge una terza tecnica: mantiene i sorgenti delle dipendenze transitive nel sourcepath, in modo che le modifiche apportate ai file di diversi target Bazel si riflettano immediatamente nell'editor senza attendere che Bazel produca un nuovo classpath, un vincolo che limitava Metals v1.

Gestire una codebase da 24 milioni di righe in una singola istanza di presentation compiler va ben oltre lo scopo originario per cui è stato progettato il compilatore Scala. Metals v2 ci riesce grazie a due modalità del compilatore basate su un insieme condiviso di tecniche per scalare il sourcepath. La funzionalità jump-to-definition, la più utilizzata dell'editor, viene eseguita sull'intero repository a p50 7 ms, p90 575 ms. Scala è il linguaggio utilizzato dalla maggior parte dei nostri ingegneri, quindi questa pipeline era l'elemento fondamentale che doveva funzionare affinché Cursor diventasse una reale alternativa a IntelliJ nel nostro monorepo.

Pipeline Java: elaborare un milione di righe di codice Java al secondo con Turbine

Metals v2 implementa l'interfaccia LSP di Java direttamente sulle API di javac. Abbiamo valutato il riutilizzo di un server di linguaggio Java esistente (implementazioni basate su JDT e NetBeans), ma entrambi sono incentrati sulla build nello stesso modo in cui lo era Metals v1, proprio l'accoppiamento che la v2 mirava a eliminare. Basarsi invece su javac ha permesso a Java di condividere il sourcepath e il modello di sincronizzazione della build di Metals v2 con Scala, che costituisce ancora la maggior parte del nostro monorepo, e l'interfaccia Java di cui avevamo bisogno era sufficientemente ridotta da poter essere gestita autonomamente.

Le API di javac hanno fornito un accesso al compilatore sufficiente per implementare diagnostica, navigazione, evidenziazione semantica e altri metodi LSP chiave, comportandosi bene anche su codice parzialmente errato, un aspetto fondamentale per il flusso di lavoro di un editor interattivo. Come per Scala, abbiamo deliberatamente limitato la portata delle funzionalità di editing attivo, come il refactoring e il completamento automatico.

Il principale problema di scalabilità di questa architettura si è manifestato nei file che causavano prestazioni patologiche nella fase "enter" di javac, importando in modo transitivo milioni di righe di codice a livello di outline dei simboli. Metals v2 affronta questo problema con una modalità javaSymbolLoader: "turbine-classpath", abilitata per impostazione predefinita, che utilizza una versione modificata del compilatore di intestazioni Turbine per produrre un classpath di repository adatto a un ambiente IDE, inclusa la gestione fluida degli errori di risoluzione dei nomi. Turbine elabora quasi un milione di righe di codice Java al secondo su un singolo thread, il che significa che Metals può ricompilare l'intera codebase Java a intervalli regolari. Questo mantiene quasi tutti i simboli tra file diversi nel classpath anziché nel sourcepath, consentendo alla fase "analyze" di javac di essere eseguita vicino al suo limite pratico per l'uso interattivo: quasi 100.000 righe di codice al secondo in base ai nostri benchmark.

Integrazione della build: interrogare 285.000 target Bazel con latenze da editor

"Utile prima della sincronizzazione della build" non significa "ignorare la build". Metals necessita comunque della fedeltà del grafo di build per una lunga serie di funzionalità, tra cui la navigazione verso dipendenze di terze parti o sorgenti generati, il rispetto di regole di shading personalizzate, l'individuazione di suite di test e la configurazione automatica dei launcher di debug. In Databricks, ciò significa rendere i metadati di 285.000 target JVM Bazel interrogabili con latenze da editor.

La nostra implementazione in produzione di questo livello è un server BSP interno scritto in Go e personalizzato per le nostre regole Bazel. Questo server BSP non fa parte di questo rilascio open source, ma vale comunque la pena applicare le sue scelte di progettazione ad altre implementazioni BSP di Bazel.

In primo luogo, Metals v2 non richiama mai Bazel tramite il server BSP a meno che l'utente non lo richieda esplicitamente. In un monorepo di grandi dimensioni, la sincronizzazione dell'IDE in background può acquisire il lock di Bazel ed entrare in conflitto con le build avviate dagli sviluppatori; pertanto, la sincronizzazione della build è un'azione esplicita dell'utente anziché un comportamento all'avvio o un'attività di manutenzione in background. I metadati risultanti vengono memorizzati in uno snapshot JSON che scala tramite il pooling delle costanti per etichette, percorsi e prefissi di repository ripetuti. Quando gli utenti eseguono la sincronizzazione, il server BSP aggiunge in modo incrementale altri target a questo snapshot e fornisce i metadati aggiornati tramite BSP.

In secondo luogo, non esiste un formato di configurazione di sincronizzazione condiviso. Gli utenti sincronizzano singoli file o directory on-demand durante la modifica per migliorare la navigazione o l'accuratezza della diagnostica. Nella nostra esperienza, i set di sincronizzazione predefiniti crescono nel tempo, vengono copiati tra i team e diventano più lenti rispetto alla sincronizzazione mirata di cui lo sviluppatore ha effettivamente bisogno.

Unire i tre livelli

Insieme, l'indice indipendente dalla build, le pipeline basate sul compilatore e l'integrazione della build incentrata sui metadati offrono un'intelligenza del codice a bassa latenza in codebase Bazel da milioni di righe. Rilasciandolo come open source con licenza Apache 2.0, vogliamo offrire al più ampio ecosistema una scelta che prima non esisteva: associare un assistente di programmazione (coding agent) e un editor leggero a una ricca navigazione Scala e Java, su una scala in cui i server di linguaggio JVM esistenti non offrivano alcuna soluzione credibile.

(Questo post sul blog è stato tradotto utilizzando strumenti basati sull'intelligenza artificiale) Post originale

Ricevi gli ultimi articoli nella tua casella di posta

Iscriviti al nostro blog e ricevi gli ultimi articoli direttamente nella tua casella di posta.