This document describes the high-level architecture of Metals following the philosophy of rust-analyzer's ARCHITECTURE.md
- For the general contributing guidelines, see Getting started guide for contributors.
- If you're interested in the overview of how Metals works, A Dive into how Metals works video will be helpful.
- For LSP specification, please refer the LSP document, you don't have to read all of the
specification
, just read theoverview
and pick out the interesting parts when you need it.
MetalsLanguageServer.scala
is an entrypoint to Metals. This class is responsible for handling LSP lifecycle messages. Upon receiving an initialize request, an instance of WorkspaceLspService.scala
containing a dedicated instance of MetalsLspService.scala
for each workspace folder is created. Together they are responsible for handling other LSP requests.
In WorkspaceLspService.scala
we implement all of the LSP endpoints. All endpoints we use are defined in the scala.meta.metals.lsp
package in 3 files:
WorkspaceService.scala
TextDocumentService.scala
MetalsService.scala
For example, you will find the endpoint for textDocument/completion
request in TextDocumentService
.
@JsonRequest("textDocument/completion")
def completion(...) = ...
WorkspaceLspService
keeps track of the currently opened workspace folders each treated as a separate Scala project. We keep an instance of MetalsLspService
per workspace folder or a single instance for a single root project. The main purpose of WorkspaceLspService
is redirecting requests/notifications to the correct instance of MetalsLspService
.
MetalsLspService
is the most important class of the project, it creates and manages many components, for instance:
private val compilers: Compilers = ...
private val codeLensProvider: CodeLensProvider = ...
private val diagnostics: Diagnostics = ...
Hacking in Metals usually starts with recognizing in which component one has to make a change to get something working.
Metals features are powered by presentation compilers, if you hit Compilers.scala
it is the client of compilers.
mtags
module is the Scala version specific module used to interact with the Scala presentation compiler using Java defined interfaces. You can find the interfaces under mtags-interface
project.
For example, ScalaPresentationCompiler.java
in mtags-interface
is the interface for ScalaPresentationCompiler.scala
under scala-2
and scala-3
directories of mtags
module.
For more details
- An introduction to the Scala presentation compiler
- Presentation compilers' endpoint, which are the class from the compiler jar that Metals uses.
- Scala3
InteractiveDriver.scala
andInteractive.scala
in lampepfl/dotty - Scala2:
interactive/Global.scala
in scala/scala
- Scala3
Metals uses semanticdb for many features (Providers that receive the instance of Semanticdbs
) such as references and renames. Semanticdb offers us information from the compiler that are written to disk and easily consumable. Metals consumes SemanticDBs in two ways:
FileSystemsSemanticdbs
consumes SemanticDBs on disks- Semanticdb will be generated by the build servers to disk, see https://scalameta.org/metals/docs/integrations/new-build-tool#enable-semanticdb
InteractiveSemanticdbs
will generate SemanticDB on the fly using the presentation compiler viamtags
, in caseFileSystemsSemanticdbs
failed or when the build tool doesn't compiler specific files such as dependency sources or sbt files.
In addition, classes extend SemanticdbFeatureProvider
index the semanticdb symbols, that will be updated by SemanticdbIndexer
.
Metals communicates with build server such as bloop
and sbt
using Build Server Protocol.
BspConnector.scala
manages the connections between Metals and build server, and BuildServerConnection.scala
represents the API wrapper for the build server.
For more details about sbt's BSP support in Metals, see the blog post.
Worksheet support is provided by mdoc, which is able to typecheck and evaluate each line of the input. The main class responsible for worksheets is WorksheetProvider.scala. It is responsible for downloading mdoc instance for each Scala version that is supported and running the evaluation in the file input.
Later the evaluations are published using decoration extension or via additional Text Edits for editors that do not support decorations. This is done in the two classes implementing WorksheetPublisher.scala:
- DecorationWorksheetPublisher.scala for decoration publishing
- WorkspaceEditWorksheetPublisher.scala for publishing decorations as comments in the code
FormattingProvider.scala
takes care of how Metals handles textDocument/formatting
. It uses scalafmt as a code formatter downloading dynamically using scalafmt-dynamic
.
We don't embed a specific version of scalafmt into Metals so that users can switch scalafmt's version using .scalafmt.conf
.
Note that FormattingProvider
doesn't handle textDocument/rangeFormatting
.
Scalafix support is implemented in the ScalafixProvider.scala. The class uses scalafix API scalafix.interfaces.Scalafix
to run the rules and get text edits, which are later changed to LSP TextEdits and applied to the file using WorkspaceEdit.
Metals downloads separate version of Scalafix for each binary version of Scala. It can also download additional dependencies for each of the used rules.
Debugging is handled by Debug Adapter Protocol, which is a complementary protocol to LSP.
The main code for debugging resides in scala.meta.internal.metals.debug
package with DebugProvider.scala being the main entrypoint.
DebugProvider sets up the communication between the debug server process started by the build server and the client. This communication is handled in DebugProxy.scala which translates some of the messages in order to enrich them with the information from Metals itself.
You can find more information about DAP here
MtagsIndexers are primarily designed for symbol indexing, generating approximate SemanticDB TextDocument
instances based on syntax information. They are particularly useful when we are interested in symbol names and their locations, without requiring further information such as their types or document synthetics.
We use Mtags-generated SemanticDB instead of compiler-generated SemanticDB because there are times when we want to rely on a symbol index even if the compiler cannot generate SemanticDB: for example, with 3rd-party dependencies, non-compilable code, or not yet compiled code.
The endpoints is scala.meta.internal.mtags.Mtags
, which dispatches to several MtagsIndexer implementations:
ScalaMtags
parses the provided Scala file using Scalameta's parser.- To see which symbols ScalaMtags extracts, refer to the unit tests (
MtagsSuite.scala
andtests/unit/src/test/resources/mtags
). pScalaToplevelMtags
tokenizes the given Scala file using Scalameta, and parses it on the Metals side with custom parser. - To enable fast indexing with a low memory footprint, nested symbols (such as functions and members defined in top-level classes, traits, and objects) are skipped since nested symbols are not of interest when performing symbol search.
- See Fast goto definition with low memory footprint | Metals for more details.
- The unit test (
ScalaToplevelSuite.scala
) is a good resource to see which symbols it extracts.
- To see which symbols ScalaMtags extracts, refer to the unit tests (
JavaMtags
parses the given Java file usingqdox
.JavaToplevelMtags
tokenize and parses Java code in the same way asScalaToplevelMtags
. We use our own custom tokenizer and parser instead ofqdox
for fast indexing.