Tomasz Godzik
VirtusLab
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 issues
Indexing
Interactive features
Standalone mode
Improved Java support
Configuring Metals
Plans and questions
Metals is a language server using the Language Server Protocol.
Uses Language Server Protocol to provide IDE features in any editor that implements the protocol client.
Based on JSON RPC.
Widely supported by multiple editors, de facto standard for most languages.
Model Context Protocol is similar, but it is designed to be used by LLM agents
Provide tools and other resources to improve LLM results.
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.
We are continuing their work to introduce it to the world.
Editor opened
Not imported
Already imported
Editor opened
Build import:
1-5 min
Build server start 20s - 3min
Not imported
Already imported
Editor opened
Build import:
1-5 min
Build server start 20s - 3min
Indexing
20s - 3min
Not imported
Already imported
Editor opened
Build import:
1-5 min
Build server start 20s - 3min
Indexing
20s - 3min
Compilation
20s - 15min
Not imported
Already imported
Editor opened
Build import:
1-5 min
Build server start 20s - 3min
Indexing
20s - 3min
Compilation
20s - 15min
Read to serve requests
Editor opened
Read to serve requests
Editor opened
Read to serve requests*
* Some more time might be needed initially for all features to work
Load up old index from protobuf with oids
git ls-files --stage
Load up old index from protobuf with oids
Index each changed file using fast indexers
Load up old index from protobuf with oids
git ls-files --stage
Index each changed file using fast indexers
Update each changed entry in memory
Load up old index from protobuf with oids
git ls-files --stage
Index each changed file using fast indexers
Update each changed entry in memory
Dump data in protobuf on close
Load up old index from protobuf with oids
git ls-files --stage
Each document is indexed into special IndexedDocument protobuf entry.
Contains information about all found identifiers added to bloom filters and definitions in a separate collection.
case class IndexedDocument(
file: AbsolutePath,
oid: String,
semanticdbPackages: Seq[String],
language: Semanticdb.Language,
symbols: collection.Seq[Mbt.SymbolInformation],
bloomFilter: StringBloomFilter,
)
case class IndexedDocument(
file: Path,
oid: String,
semanticdbPackages: Seq[String],
language: Language,
symbols: Seq[SymbolInformation],
bloomFilter: StringBloomFilter,
)
package a
package b:
object O
class C:
def main(str: String) =
println(s"Hello $str!")
/home/tgodzik/Main.scala
case class IndexedDocument(
file: Path,
oid: String,
semanticdbPackages: Seq[String],
language: Language,
symbols: Seq[SymbolInformation],
bloomFilter: StringBloomFilter,
)
package a
package b:
object O
class C:
def main(str: String) =
println(s"Hello $str!")
de5e6fb46e02c83f120eff21
case class IndexedDocument(
file: Path,
oid: String,
semanticdbPackages: Seq[String],
language: Language,
symbols: Seq[SymbolInformation],
bloomFilter: StringBloomFilter,
)
package a
package b:
object O
class C:
def main(str: String) =
println(s"Hello $str!")
case class IndexedDocument(
file: Path,
oid: String,
semanticdbPackages: Seq[String],
language: Language,
symbols: Seq[SymbolInformation],
bloomFilter: StringBloomFilter,
)
package a
package b:
object O
class C:
def main(str: String) =
println(s"Hello $str!")
case class IndexedDocument(
file: Path,
oid: String,
semanticdbPackages: Seq[String],
language: Language,
symbols: Seq[SymbolInformation],
bloomFilter: StringBloomFilter,
)
package a
package b:
object O
class C:
def main(str: String) =
println(s"Hello $str!")
case class IndexedDocument(
file: Path,
oid: String,
semanticdbPackages: Seq[String],
language: Language,
symbols: Seq[SymbolInformation],
bloomFilter: StringBloomFilter,
)
package a
package b:
object O
class C:
def main(str: String) =
println(s"Hello $str!")
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
At scale (10k+ files), this means orders of magnitude fewer files scanned. 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
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 Scala compiler now receives the full source path of the project.
This means the compiler can resolve most symbols even in a large multi-module project without any compiled output or even without any build structure.
In the age of LLMs we need the tool to be able to keep up with the changes in user code to be always up to date.
Relying on integration with the build tool makes it slower and more brittle.
Metals will now also return errors and warnings from the compiler when typing alongside the build warnings.
This makes it more responsive to show errors to the user.
package org.myorg.core
import org.myorg.util.Helper
object Main {
Helper.printMessage(
"Hello Berlin!"
)
}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 Berlin!"
)
}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.scalauses
package org.myorg.core
import org.myorg.util.Helper
object Main {
Helper.printMessage(
"Hello Berlin!"
)
}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 editting that specific file.
We can skip the method bodies!
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 =
println("Hello again!")
println("Very important message!")
println("Really, you should!!")
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
We can get rid of the body
Please use ExplicitResults rules in scalafix, it helps the tooling a lot!
Since we no longer need the classfiles, we can just offer IDE support without any underlying build server such as Bloop, sbt or mill.
This will not have all the same capabilities as a build server!
.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"
}
]
}We can still have some structure with namespaces
{
"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"
}
]
"namespaces": {
"activity:test": {
"sources": [],
"scalacOptions": ["-Wunused:all"],
"javacOptions": ["--release", "21"],
"javaHome": "/Users/tgodzik/.sdkman/candidates/java/25.0.1-tem",
"dependencyModules": [
"com.google.guava:guava:30.0-jre"
],
"dependsOn": ["activity:compile", "puzzle:compile"]
},
}
}.metals/mbt.json
* Bazel remote tests might be a bit of an issue
Works with the existing tests API!
This approach is currently recommended for Bazel as it is faster and more reliable than the Bazel BSP solution
We have tested it on some large monorepos and it seems to work pretty good!
If you are having problems with a large codebase, let us know!
One of the main goals in Metals 2 to was to make it viable for large mixed-language codebases.
Hence we needed to step up out Java support as well and we implemented all the LSP based feature together with a number of refactorings.
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
Caveat: It does not support project Lombok or annotation processors currently.
Compile all Java files on startup using turbine
Compile all Java files on startup using turbine
Presentation compiler compiles current file using turbine classpath
Opened Main.java
Compile all Java files on startup using turbine
Opened Main.java
Outline javac compiler compiles all changed files on sourcepath
Checked out different branch
Presentation compiler compiles current file using turbine classpath
Compile all Java files on startup using turbine
Opened Main.java
Checked out different branch
PC recompiles the file with both turbine and outline class files
Focused on Main.java again
Presentation compiler compiles current file using turbine classpath
Outline javac compiler compiles all changed files on sourcepath
Compile all Java files on startup using turbine
Full recompile every 60s
Opened Main.java
Checked out different branch
PC recompiles the file with both turbine and outline class files
Focused on Main.java again
Presentation compiler compiles current file using turbine classpath
Outline javac compiler compiles all changed files on sourcepath
Classpath from Google Turbine
Opened OtherFile.java
Presentation compiler compiles current file using turbine classpath
Google Turbine is much faster than using the default Java compiler even with outline compilation only.
So Metals tries to include the most important options to allow people to tinker with their setup.
Influences how we discover classes from dependencies:
- classpath - uses only the information from class files
- sourcepath - goes over dependency sources, includes types and extension methods
{
"metals": {
"initializationSettings": {
"definitionIndexStrategy" : "sourcepath"
}
}
}Turn off or on presentation compiler diagnostics. They are added alongside the build tool diagnostics and Metals will try to deduplicate them.
{
"metals": {
"presentationCompilerDiagnostics": true
}
}Where to put automatic imports:
- smart - tries to add it in the correct place alphabetically.
- append-last - always puts the imports last
{
"metals": {
"scalaImportsPlacement": "smart"
}
}If set to true Metals will compile using the build server when a file is focused or when the file is changed and saved.
Metals tries to not compile anything that was already compiled by default.
{
"metals": {
"buildOnChange": true
"buildOnFocus": true
}
}Fine tuning options for mbt standalone mode:
- importGeneratedSources - will try to add generated files when creating mbt.json file
- semanticdbCacheEnabled - save calculated semanticdb files to disk to survive reloads
- semanticdbCacheMaxSize - maximum in memory semanticdb cache size
{
"metals": {
"mbtConfig": {
"importGeneratedSources": true,
"semanticdbCacheEnabled": true,
"semanticdbCacheMaxSize": 2000
}
}
}Choose a server to use when multiple server are available for a build tool. In Gradle, Maven and Bazel workspaces mbt will be suggested by default alongside normal build server.
{
"metals": {
"preferredBuildServer": "mbt"
}
}Whether to use the new mbt base symbol provider for searching symbols (classes, methods etc.) or the old build server based approach.
Allowed values are mbt and bsp.
{
"metals": {
"workspaceSymbolProvider": "mbt"
}
}Whether or not prompt the user when the build definition changed. It can be run manually if set to none. Allowed values are none and prompt.
{
"metals": {
"buildChangedAction": "prompt"
}
}Where from to load the symbols for the Java presentation compilere:
- turbine-classpath - turbine compiled class file stubs
- javac-classpath - normal classpath on disk
{
"metals": {
"javaSymbolLoader": "turbine-classpath"
}
}How often should we recompile the Turbine outline. By default 1 minute. Over 1 hour treated as effectively disabled.
{
"metals": {
"javaTurbineRecompileDelay": "1 minute"
}
}Should we save the Turbine stubs to disk. Useful for large codebases as it provides almost instant workspace reload.
{
"metals": {
"javaTurbineCache": true
}
}Whether to show the compile progress in the UI. Enabled by default.
{
"metals": {
"compilerProgress": "enabled"
}
}Which approach of finding dependencies to use:
- mbt - will search for references using mbt index and then load semanticdb for each file.
- bsp - will use bsp based index, previous v1 implementation.
We are aiming at merging both approaches.
{
"metals": {
"referenceProvider": "bsp"
}
}Whether the compiler should run and report diagnostics for addition compiler phases.
By default it only runs up to the typer to avoid additional cost.
{
"metals": {
"additionalPcChecks": ["refchecks"]
}
}Whether to prompt the user when no import was done previously in the new codebase.
{
"metals": {
"promptBuildImport": true
}
}Enabled protobuf features. In VS Code it will require you to also have a protobuf syntax installed separately.
Unless we make metals protobuf extensions as well.
{
"metals": {
"protobufLspEnabled": true
}
}What classpath to use when edited files is outside build target:
- all-3rdparty - use only the dependency jars
- mbt - use dependencies declared in mbt.json
- guessed - try to guess from other tools
- none - no classpath
- default - both mbt and all-3rdparty
{
"metals": {
"fallbackClasspath": "all-3rdparty"
}
}What sourcepath to use when edited files is outside build target:
- allSources - use all sources
- none - just the current source
{
"metals": {
"fallbackSourcepath": "allSources"
}
}A full guide will be published on the website before the release of Metals 2.
Build from source on main-v2
or
metals.serverVersion : "2.0.0-RC1"
Improved Java support
Relying more on
-sourcepath
Fast git based index instead of bsp
Standalone mbt.json mode
Bluesky: @tgodzik.bsky.social
Mastodon: fosstodon.org/@tgodzik
Discord: tgodzik
Presentation: https://slides.com/tomekgodzik/scala-days