Contributing to Metals
Whenever you are stuck or unsure, please open an issue or ask us on Discord or on our Matrix bridge. This project follows Scalameta's contribution guidelines and the Scala CoC.
Requirements
You will need the following applications installed:
- Java 17, 11 or 8 - Make sure
JAVA_HOME
points to a Java 17, 11 or 8 installation. Metals will need to build and run on all of them, with support for 8 probably being dropped in the near future. git
sbt
(for building a local version of the server)
Project structure
metals
the main project with sources of the Metals language server.mtags
Scala version specific module used to interact with the Scala presentation compiler. It's a dependency of themetals
project and can additionally be used by viamtags-interfaces
to support multiple Scala versions inside the Metals server. It's also used by other projects like Metabrowse.mtags-interfaces
- java interfaces for the presentation compiler.tests/unit
moderately fast-running unit tests.tests/cross
- tests targeting cross builds for common features such as hover, completions, signatures etc.tests/input
example Scala code that is used as testing data for unit tests.tests/slow
slow integration tests.sbt-metals
the sbt plugin used when users are using the BSP support from sbt to ensure semanticDB is being produced by sbt.docs
documentation markdown for the Metals website.metals-docs
methods used for generating documentation across multiple pages indocs
.website
holds the static site configuration, style and blogs posts for the Metals website.
Below diagram shows project structure and dependencies among modules. Note that
default-<suffix>
is a default root project
created implicitly by sbt.
Related projects
The improvement you are looking to contribute may belong in a separate repository:
- scalameta/metals-vscode: the Visual Studio Code extension for Metals.
- scalameta/nvim-metals: the Neovim extension for Metals using the built-in LSP support of Neovim.
- scalameta/scalameta: SemanticDB, parsing, tokenization.
- scalameta/munit: Test framework used in the main Metals repository
- scalacenter/bloop: build server for compilation.
- scala/scala: Scala 2 presentation compiler.
- lampepfl/dotty: Scala 3 presentation compiler.
- scalameta/scalafmt: code formatting.
- scalacenter/scalafix: code refactoring and linting.
Common development workflow
Most of the time development in Metals looks like:
- do some changes
- write tests which check if your changes work
- publish Metals server locally and test changes manually
When diving into part of the code without any prior knowledge it might be hard to comprehend what's going on and what part of the code is responsible for specific behavior. There are several ways to debug Metals, but most popular are:
- debugging through logging (recommended option)
- classic debugging with breakpoints
Commit messages
Try to follow the conventional commits specification, which means your commits should be of form:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
It's important to over-communicate in your commit messages. Be thorough. It's important to ensure you outline the problem you're trying to solve, or the feature you're introducing, and then explain the fix or implementation. This will help the reviewers with their review and also future contributors to get the full context of your changes.
For example, this message can be take a form of:
Previously, this bug was happening due to invalid handling of URIs. Now, we handle them correctly using a dedicated class.
Ideally, each PR would only have one commit and the title with body would be the same for the pull requests. However, sometimes a PR might require more changes and commits, in that case please try to keep them self contained, which means each change pertains to a specific bug or feature and can be reverted separately.
Debugging through logging
This approach provides very quick iterations and short feedback loop.
It depends on placing multiple pprint.log()
calls which will log
messages in .metals.log
file. Logged output can be watched by tail -f .metals/metals.log
.
MetalsLanguageServer.scala:1841 params: DebugSessionParams [
targets = SingletonList (
BuildTargetIdentifier [
uri = "file:/HappyMetalsUser/metals/#metals/Compile"
]
)
dataKind = "scala-attach-remote"
data = {}
]
See workspace logs for more information.
This approach can be used in 2 variants:
- together with manual testing when it's hard to write test for some changes.
- with unit tests
Classic debugging
Classic debugging is possible by the JVM debugging mechanism. Publish Metals locally, open a new project and configure debug settings. Then you can attach IDE with opened Metals repository to the debugged instance:
-
VSCode - add attach configuration to yours launch.json
{
"version": "0.2.0",
"configurations": [
// Attach debugger when running via:
// `-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=localhost:5005`
{
"type": "scala",
"request": "attach",
"name": "Attach debugger to Metals server",
"buildTarget": "metals",
"hostName": "localhost",
"port": 5005
}
]
}Then pick such a defined configuration and run debug.
-
IntelliJ - Select Attach to the process and pick proper process from the list
Unit tests
To run the unit tests open an sbt shell and run unit/test
. However, this command will
run all of the unit tests declared in unit
module.
sbt
# (recommended) run a specific test suite, great for edit/test/debug workflows.
> unit/testOnly tests.DefinitionSuite
# run a specific test case inside the suite.
> unit/testOnly tests.DefinitionSuite -- <exact-test-name>
# run unit tests, moderately fast but still a bit too slow for edit/test/debug workflows.
> unit/test
# run slow integration tests, takes several minutes.
> slow/test
# (not recommended) run all tests, slow. It's better to target individual projects.
> test
Manually testing an LspSuite
Every test suite that extends LspSuite
generates a workspace directory under
tests/unit/target/e2e/<suitename>/<testname>
. To debug why a LspSuite
might be
failing, run the test once and then open it directly in the editor. For
example, for the test case "deprecated-scala"
in WarningsLspSuite
run the
following command:
code tests/unit/target/e2e/warnings/deprecated-scala
This will open Visual Studio Code in directory with test project and it'll be possible to investigate why test is failing manually.
Cross tests
These tests check common features such as hover, completions or signatures for different Scala version.
sbt
# run presentation compiler tests, these are the quickest tests to run.
> cross/test
run presentation compiler tests for all Scala versions.
> +cross/test
Manual tests
Some functionality is best to manually test through an editor. A common workflow
while iterating on a new feature is to run publishLocal
and then open an
editor in a small demo build.
It's important to note that sbt publishLocal
will create artifacts only for
the Scala version currently used in Metals and trying to use the snapshot
version with any other Scala version will not work. This may be fine if you're
working on a generic feature that isn't using the presentation compiler
(anything in mtags), if not then you need to publish the specific version of
mtags that you're trying to test
`publishLocal; ++3.1.1 mtags/publishLocal`
You can also do a full cross publish with sbt +publishLocal
, however this will
take quite some time, so it's often better to only target the version you need.
Visual Studio Code
Install the Metals extension from the Marketplace by searching for "Metals".
Click here to install the Metals VS Code plugin
Next, update the "Server version" setting under preferences to point to the
version you published locally via sbt publishLocal
. You'll notice that version has the format
<version>-SNAPSHOT
.
When you make changes in the Metals Scala codebase
- publish metals binary as described above.
- execute the "Metals: Restart server" command in Visual Studio Code (via command palette)
Vim/Neovim
If using nvim-metals
:
You'll want to make sure to read the docs here and take a look at the example configuration here if you haven't already set everything up.
- publish the metals binary as described above.
- set the
serverVersion
in yoursettings
table that you pass in to your metals config. - Open your workspace and trigger a
:MetalsUpdate
followed by a:MetalsRestart
. NOTE: that every time you publish locally you'll want to trigger this again.
If you are using another Vim client, write a new-metals-vim
script that builds
a new metals-vim
bootstrap script using the locally published version.
coursier bootstrap \
--java-opt -Dmetals.client=<<NAME_OF_CLIENT>> \
org.scalameta:metals_2.13:1.4.1-SNAPSHOT \ # double-check version here
-o /usr/local/bin/metals-vim -f
NOTE if you're able to configure your client using initialization options,
then the client
property is not necessary. You can see all the options
here.
Finally, start Vim with the local Metals version
cd test-workspace # any directory you want to manually test Metals
new-metals-vim && vim build.sbt # remember to have the script in your $PATH
When you make changes in the Metals Scala codebase, run sbt publishLocal
, quit
vim and re-run new-metals-vim && vim build.sbt
.
Workspace logs
Metals logs workspace-specific information to the
$WORKSPACE/.metals/metals.log
file.
tail -f .metals/metals.log
These logs contain information that may be relevant for regular users.
JSON-RPC trace
To see the trace of incoming/outgoing JSON communication with the text editor
or build server, create empty files in $WORKSPACE/.metals/
or your machine cache
directory.
However, we do not recommend using your machine cache directory because
trace files located there are shared between all Metals instances, hence multiple
servers can override the same file. Using $WORKSPACE/.metals/
solves this issue and
also allows user to have more precise control over which metals instances log
their JSON-RPC communication.
# Linux and macOS
touch $WORKSPACE/.metals/lsp.trace.json # text editor
touch $WORKSPACE/.metals/bsp.trace.json # build server
touch $WORKSPACE/.metals/dap-server.trace.json # debug adapter
touch $WORKSPACE/.metals/dap-client.trace.json # debug adapter
# Windows
type nul > $WORKSPACE/.metals/lsp.trace.json # text editor
type nul > $WORKSPACE/.metals/bsp.trace.json # build server
type nul > $WORKSPACE/.metals/dap-server.trace.json # debug adapter
type nul > $WORKSPACE/.metals/dap-client.trace.json # debug adapter
Next when you start Metals, watch the logs with tail -f
.
# Linux and macOS
tail -f $WORKSPACE/.metals/lsp.trace.json
The traces are very verbose so it's recommended to delete the files if you are not interested in debugging the JSON communication.
JVM Debugging
To debug the JVM with the Metals server, add a property to your
Server Properties
with the usual Java debugging flags, making sure you have
the quiet
option on. It's important to remember about the flag, as the server
uses standard input/output to communicate with the client, and the default
output of the debugger interferes with that.
This property will make your server run in debug mode on port 5005 without waiting for the debugger to connect:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005,quiet=y
Updating build tool launcher/wrappers
Metals uses various wrappers or launchers for each build tool that it supports.
This makes sure that when your in a workspace for you build tool that metals is
able to correctly launch that build tool, even if it doesn't exist on the users
$PATH
. You can see their usages in <BuildToolName>BuildTool.scala
.
Updating sbt-launcher
The easiest way to update the sbt-launcher is with the following coursier command:
cp "$(cs fetch org.scala-sbt:sbt-launch:<version>)" sbt-launch.jar
This will allow you to not have to do some of the manual steps with the launcher properties file listed here.
Updating maven wrappers
For Maven we use the Maven Wrapper. In order to update this you'll want to do the following:
- Run the
./bin/update-maven-wrapper.sh
script - Update the
def version
inMavenBuildTool.scala
to the latest version that you just updated to. - Run the specific maven tests and ensure they pass:
./bin/test.sh 'slow/testOnly -- tests.maven.*
Git hooks
This git repository has a pre-push hook to run Scalafmt.
The CI also uses Scalafix to assert that there a no unused imports. To
automatically remove unused imports run sbt scalafixAll
. We don't run Scalafix
as a pre-push git hook since starting sbt takes a long time.