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?"

 

 

 

  1. Metals Build Tool symbol index
  2. IDE Features available from the start
  3. 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

 

  1. BSP dependency modules from the build server
  2. Manually or automatically generated `.metals/mbt.json`
  3. 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.

  1. Files are pre-parsed to identify the package within each file
  2. The compiler then asks the created structure when loading symbols
  3. 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.scala
org.myorg.util -> Helper.scala
org.myorg.services -> Service.scala

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 ...
  } 
}

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

  1. Port upstream v1 changes (v1.5.2+) - new features and bugfixes
  2. Scala 3 validation -  Java + Scala 3 mixed projects, mbt coverage
  3. Stabilization - test across all build tools, not just Bazel/Bloop
  4. 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