Metals v2
What is it all about?
Tomasz Godzik
VirtusLab
What do I do?
Maintainer of multiple Scala tools including Metals, Bloop, Scalameta, Munit, Mdoc and parts of the Scala 3 compiler.
Release officer for Scala LTS versions
Part of the Scala Core team as coordinator and VirtusLab representative
Part of the moderation team
Most prominent Metals issues
01
02
03
04
MBT - Metals Build Tool
Fast IDE features
Improved Java Support
05
Plans
06
Questions
Metals

Metals is a language server protocol server.





Uses Language Server Protocol to provide IDE features in any editor that implements the LSP client
Based on JSON RPC
Widely supported by multiple editors, de facto standard for most languages
Metals is also a model context protocol server.




Model Context Protocol is similar, but it designed to be used by LLM agents
Provide tools and other resources to improve LLM results.
We talked about it last year

Metals most prominent problems

- Worse Java support in mixed Scala/Java projects
- IDE features only available after a successful build
- Workspace symbol search delegated entirely to BSP — slow, incomplete, brittle
- Large monorepo setups can have even worse issues
Main Metals v2 goal: Make the IDE useful from the moment you clone a repo.
"How many of you have waited 5+ minutes for IDE features after opening a project?"
- Metals Build Tool symbol index
- IDE Features available from the start
- Better Java Language Support
Each feature reinforces the others — together they make Metals viable for large, mixed-language monorepos.
Main changes
Metals Build Tool

Wait! Another build tool!?

It's not really a build tool
It's about getting the minimal amount of information from the build to somewhere Metals can access it directly.
Symbol search depended on the BSP build server. We needed to connect to the build server (could be different from the build tool)
We had to wait to get all `src/main/scala` etc.
Before (current main)
Before (current main)
Editor opened
Build import
(1-5 min)
Build import
Build import
Build import
Build server start (20s-2min)
Build import
Build import
Build import
Not imported
Already imported
Indexing
(30s - 3min)
Build import
Build import
Build import
Ready to serve requests
Build import
Build import
Build import
Compilation (30s - 10min)
Build import
Build import
Build import
Future (main-v2)
Self-contained index, zero build dependency. Can work as soon as it's indexed.
Future (main-v2)
Editor opened
Build import
(1-5 min)
Build import
Build import
Build import
Build server start (20s-2min)
Build import
Build import
Build import
Not imported
Already imported
Indexing
(30s - 3min)
Build import
Build import
Build import
Ready to serve requests
Build import
Build import
Build import
Compilation (30s - 10min)
Build import
Build import
Build import
How MBT works
git ls-files --stage
~200-300ms
Each file checked if modified by OID
Index each file using existing fast indexers
Create IndexedDocument
Overwrite existing index.mbt on close
Special IndexedDocument
Contains information about all found identifiers added to bloom filters and definitions in a separate collection.
This is enough for navigation.
For references it's used together with the presentation compiler for correctness.
.metals/mcp.json
{
"dependencyModules": [
{
"id": "com.google.guava:guava:30.0-jre",
"jar": "/path/to/guava-30.0-jre.jar",
"sources": "/path/to/guava-30.0-jre-sources.jar"
}
]
}Bloom filters
Query: "UserSer" │ ├─ File A → MAYBE match → scan it ├─ File B → NO match → skip ├─ File C → NO match → skip └─ File D → MAYBE match → scan it Result: only 2 of 4 files opened instead of all 4
Bloom filters
Query: "UserSer" │ ├─ File A → MAYBE match → scan it ├─ File B → NO match → skip ├─ File C → NO match → skip └─ File D → MAYBE match → scan it Result: only 2 of 4 files opened instead of all 4
At scale (10k+ files), this means orders of magnitude fewer files scanned.
MBT features
| Feature | How MBT helps |
|---|---|
| Workspace Symbol | Primary provider |
| Find References | Pre-filter via Bloom filters → only scan candidates |
| Find Implementations | Walks inheritance graph (depth ≤ 10) from index |
| Go to Definition | Fallback when classpath has no entry (pre-build) |
| Protobuf support | `.proto` files indexed alongside `.scala` and `.java` |
Protobuf Support via mbt
No separate Protobuf language server needed.
Very simple indexing in most cases
// ← appears in workspace/symbol
message TextDocuments {
// ← go-to-definition soon supported
repeated TextDocument documents = 1;
}
message TextDocument {
reserved 4, 8, 9;
Schema schema = 1;
string uri = 2;
}
No startup time IDE - interactive features

git clone
Full IDE:
- Hover
- Complete
- Navigate
- References
Build import
Build import
Build import
Open editor
- BSP dependency modules from the build server
- Manually or automatically generated `.metals/mbt.json`
- Heuristic scan of `.bloop/` or `.bazelbsp/` cached outputs (disabled by default)
Dependency fallback priority
If we don't have anything we put all sources into the presentation compiler.
This means that even in projects with broken build tool you should be able to work on your code.
If we don't have anything we put all sources into the presentation compiler.
This means that even in projects with broken build tool you should be able to work on your code.
And it does work!

If we don't have anything we put all sources into the presentation compiler.
This means that even in projects with broken build tool you should be able to work on your code.
And it does work!
With caveats

The Scala presentation compiler accepts a `-sourcepath` flag that lets it resolve symbols directly from source files rather than requiring compiled class files on the classpath.
Scala compiler -sourcepath option
The fallback Scala compiler now receives the full source path of the project, including sources from transitive dependencies — not just direct ones.
This means the fallback compiler can resolve most symbols even in a large multi-module project without any compiled output.
Presentation compiler fallbacks
Two improvements make it possible.
- Files are pre-parsed to identify the package within each file
- The compiler then asks the created structure when loading symbols
- Files found in the package are then compiled
Everything is lazy
Everything is lazy
package org.myorg.core
import org.myorg.util.Helper
object Main {
Helper.printMessage(
"Hello London!"
)
}package org.myorg.util
import org.myorg.util.Helper
object Helper {
def printMessage(msg: String): Unit = {
val message = "<info>" + msg
println(message)
}
}package org.myorg.services
object Service {
def execute(): Unit = {
// very complicated code ...
}
}org.myorg.core -> Main.scalaorg.myorg.util -> Helper.scalaorg.myorg.services -> Service.scalaEverything is lazy
package org.myorg.core
import org.myorg.util.Helper
object Main {
Helper.printMessage(
"Hello London!"
)
}package org.myorg.util
import org.myorg.util.Helper
object Helper {
def printMessage(msg: String): Unit = {
val message = "<info>" + msg
println(message)
}
}package org.myorg.services
object Service {
def execute(): Unit = {
// very complicated code ...
}
}This file is ignored
This is compiled
When we edit this file
It's even more lazy
If we have types specified for bodies of our methods there is no reason to compile it if we are not in that specific file.
Actual compiler view
package org.myorg.core
import org.myorg.util.Helper
object Main {
Helper.printMessage(
"Hello London!"
)
}package org.myorg.util
import org.myorg.util.Helper
object Helper {
def printMessage(msg: String): Unit = ???
}Bodies are ignored
When we edit this file
This approach also works if we do have a full project structure
Results in completions and other features being up to date even with compilation issues
You can experiment with the new options
Check in the source code
Better Java support

One of the main goals in Metals 2 to was to make it viable for large mixed codebases.
Hence we needed to step up out Java support as well.
| Feature | Status | Powered by. |
|---|---|---|
| Completions | ✅ | Presentation compiler |
| Hover | ✅ | Presentation compiler |
| Signature Help | ✅ | Presentation compiler |
| Diagnostics | ✅ | Presentation compiler |
| Semantic Highlighting | ✅ | Java Semanticdb |
| Selection Range | ✅ | Javac AST |
| Document Symbols | ✅ | Javac AST |
| Go to Definition | ✅ | SemanticDB + mbt |
| Find References | ✅ | mbt + Bloom filters |
But how to make it efficient in large codebase?
We use google turbine which allows us to define stubs for each file without compiling method bodies.
We get in-memory classfiles which the javac compiler can use as a normal classpath
But how to make it efficient in large codebase?
example/File.java (edited)
example/Other.java
...
Classfiles stubs: example/Other.class, example/Other2.class
File.java,
Google Turbine
Presentation compiler
Javac outline compiler, compiles the file fully
example/File.java (edited)
example/Other.java
...
Classfiles stubs: example/Other.class, example/Other2.class
File.java
Google Turbine
Presentation compiler
Javac outline compiler, compiles the file fully
Javac outline compiler, compiles without method bodies
AlsoEdited.java
By default turbine will run every minute, so we are back to classfiles once that completes
Why not just `-sourcepath`?
Edit Foo.java
javac resolves Bar (from sourcepath)
Bar references Baz → compile Baz too
Baz references Qux → compile Qux...
💥 500 files compiled for one edit
Why not just `-sourcepath`?
Sourcepath will still be used for any broken files and any changed files within last minute.
We still prefer to depend on fully compiled workspace
Plans
- Port upstream v1 changes (v1.5.2+) - new features and bugfixes
- Scala 3 validation - Java + Scala 3 mixed projects, mbt coverage
- Stabilization - test across all build tools, not just Bazel/Bloop
- Documentation - configuration guide for all new options
What's left?
A lot was done to improve Bazel support:
- source jar as roots
- handling large BSP messages
But a lot of this was done with internal Bazel BSP server inside Databricks
We're working on our own Scala based inlined inside Metals
Bazel Improvements
We are happy to work with you to improve your workflow
Via contracts if needed, but usually just happy to help
Summary
Java IDE
Support
- mostly feature complete
- turbine
- prune compiler
Fast IDE features
- sourcepath fallback
- mbt fallback
- better crash resilience
MBT Index
- bloom filters
- protobuf
- persists to disk
- can use mbt.json
Thank You
Tomasz Godzik
Bluesky: @tgodzik.bsky.social
Mastodon: fosstodon.org/@tgodzik
Discord: tgodzik
tgodzik@virtuslab.com
Some links:
https://www.anthropic.com/engineering/code-execution-with-mcp
Metals V2
By Tomek Godzik
Metals V2
- 4