What is it all about?
Tomasz Godzik
VirtusLab
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
Main Metals v2 goal:
Make the IDE useful from the moment you clone a repo.
Main Metals v2 was worked on in Databricks by Ólafur Páll Geirsson, Iulian Dragos and others in the team.
Metals Build Tool symbol index
Each feature reinforces the others — together they make Metals viable for large, mixed-language monorepos.
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.
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
Self-contained index, zero build dependency. Can work as soon as it's indexed.
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
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
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.
Query: "UserSer"
│
├─ File A → MAYBE match → compile it
├─ File B → NO match → skip
├─ File C → NO match → skip
└─ File D → MAYBE match → compile it
Result: only 2 of 4 files opened instead of all 4
Query: "UserSer"
│
├─ File A → MAYBE match → compile it
├─ File B → NO match → skip
├─ File C → NO match → skip
└─ File D → MAYBE match → compile it
Result: only 2 of 4 files opened instead of all 4
| 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` |
git clone
Full IDE:
Build import
Build import
Build import
Open editor
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.
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.
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.
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.scalapackage org.myorg.core
import org.myorg.util.Helper
object Main {
Helper.printMessage(
"Hello London!"
)
}package org.myorg.util
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
If we have explicit types for our methods there is no reason to compile the method itself if we are not in that specific file.
Please use ExplicitResults rules in scalafix, it helps the tools a lot!
package org.myorg.core
import org.myorg.util.Helper
object Main {
Helper.printMessage(
"Hello London!"
)
}package org.myorg.util
object Helper {
def printMessage(msg: String): Unit = ???
}Bodies are ignored
When we edit this file
package org.myorg.core
import org.myorg.util.Helper
object Main {
Helper.printMessage(
"Hello London!"
)
}package org.myorg.util
object Helper {
def printMessage(msg: String): Unit = ???
}When we edit this file
Bodies are ignored
Sourcepath helps with the current project, but we still need to know what are the dependencies for the project.
This is where another feature of mbt comes in and the reason it's a "build tool"
.metals/mbt.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"
}
]
}It's a very simple file format, latest 2.0.0-M12 has a Bazel extractor ready.
It can be setup for your project and updated similar to lock files.
Will make the experience work faster and better, you might not need a build server sometimes
Java Support
Fast IDE features
MBT Index
Build from source on main-v2
or
metals.serverVersion : "2.0.0-M8"