Metals with MBT

What is it all about?

Tomasz Godzik

VirtusLab

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 LSP Server

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

Most prominent problems

 

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.

 

  1. Metals Build Tool symbol index

  2. IDE Features available from the start
  3. Better Java Language Support

 

 

 

Main changes

Each feature reinforces the others — together they make Metals viable for large, mixed-language monorepos.

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.

Bloom filters

  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


Bloom filters

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


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`

No startup time IDE - interactive features

git clone

 Full IDE: 

  • Hover
  • Complete
  • Navigate
  • References

 

Build import 

Build import 

Build import 

Open editor

Ideal scenario

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.

How does it work?

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.  It uses it to find out which files it needs to compile from a specific package.

 

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

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 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!

Actual compiler view

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

Actual compiler view

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"

 

What about the dependencies

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

Separate source of information

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

Separate source of information

  1. Stabilization - make sure that MBT works well with Bazel
  2. Introduce and optimize remaining features on MBT
  3. Documentation - configuration guide for all new options, make sure config is up to date.

What's left?

Summary

 

Java Support

  • mostly feature complete
  • turbine
  • outline compiler

 

Fast IDE features

  • sourcepath fallback
  • mbt fallback
  • better crash resilience

 

MBT Index

  • bloom filters
  • protobuf
  • persists to disk
  • can use mbt.json

How do I actually use it?

Build from source on main-v2 

 

or

 

​metals.serverVersion : "2.0.0-M8"

Thank You

Tomasz Godzik