Metals Notes
Table of Contents
Presentation compilers are used for completions, hovers, backup definition provider (when it's not found in SemanticDB), diagnostics (?), see PresentationCompiler.scala for more info.
SemanticDB is used for definition navigation and find references, etc within a project, for external sources Interactive SemanticDB is used to produce SemanticDB on demand using the presentation compiler.
To get the presentation compiler for a particular file path, loadCompiler
is used throughout scala.meta.internal.metals.Compilers.scala
.
1 Links
- A Dive into how Metals works by Chris Kipp
- BSP link to ScalaBuildTargets, you can find information on a particular build target by clicking on it in Metals Doctor
- Short description of how presentation compilers are created: https://github.com/scalameta/metals/issues/1653
2 Custom Class Loading
We need a presentation compiler for each build target since different build targets can have different classpaths and compiler settings.
The presentation compiler APIs are compiled against exact Scala versions of the compiler while Metals only runs in a single Scala version. In order to communicate between Metals and the reflectively loaded compiler, a custom classloader, PresentationCompilerClassLoader,
is used by scala.meta.internal.metals.Embedded
which is in turn used by Compilers
.
3 Diagnostics
- https://github.com/scalameta/metals/blob/main/docs/editors/overview.md
- Diagnostics are triggered on compile, compilation is triggered on file save for the build target (project/module) containing the focused text file.
- Syntax errors are published as you type but type errors are handled by the build tool
- About scalafix:
3.1 Flow
- metals sends a
buildTarget/compile
(link) request to the BSP server (inBuildServerConnection.compile
), to ask the build server to compile a list of build targets. - The BSP will start compilation, sending intermittent
build/publishDiagnostics
notifications back to metals. This is caught byDiagnostics.onBuildPublishDiagnostics
which callsDiagnostics.onBuildPublishDiagnostics
. This adds the diagnostics in the request to a queue for each path (Diagnostics.diagnostics
is aMap[AbsolutePath, Queue[Diagnostic]]
). The path is also added todiagnosticsBuffer
. No diagnostics are sent to the LSP client at this stage, but they may be cleared if the sent notification had thereset
parameter set to true. build/taskFinish
is sent by the BSP server to indicate that compilation has finished, which is caught byForwardingMetalsBuildClient.buildTaskFinish
, which then callsDiagnostics.onFinishCompileBuildTarget
, this clearsdiagnosticsBuffer
, and finally sends the diagnostics the LSP client.
4 Build Servers
Switch occurs in BspConnector.switchBuildServer
. Build servers are generated by BuildServerProvider
(an extension of the BuildTool
trait) instances.
5 Random Objects Passed Around
val buffers: Buffers // holds currently loaded files val trees: Trees // manages parsing of source files to Scalameta ASTs val tables: Tables // Used to persist chosen build server, etc to a h2 db val semanticdbs: Semanticdbs //
6 Semanticdb
A semanticdb payload associated with a file contains:
Uri
the uri of the source file, iemetals/src/main/scala/scala/meta/internal/metals/codelenses/RunTestCodeLens.scala
Symbols
which contains information on definitions in the source file, e.g.local36 => val local dataKind: String local37 => val local data: JsonElement scala/meta/internal/metals/codelenses/RunTestCodeLens# => final class RunTestCodeLens extends AnyRef with CodeLens { +17 decls }
Ocurrences
a list of identifiers from the source file with their line/column-based positions and unique identifiers pointing to corresponding definitions resolved by the compiler:[75:13..75:22) => scala/Option#getOrElse(). [75:23..75:26) => scala/package.Seq. [75:27..75:32) => scala/collection/SeqFactory.Delegate#empty(). [82:14..82:26) <= scala/meta/internal/metals/codelenses/RunTestCodeLens#isMainMethod().
For example,
[2:4..2:11): println => scala/Predef.println(+1).
says that the identifier println on line 3 (zero-based numbering scheme!) refers to the second overload of println from scala/Predef
7 Symbol Indexing Flow
- Indexer.scala -> MetalsLanguageServer.connectToNewBuildServer() -> profiledIndexWorkspace() -> indexWorkspace() -> indexDependencySources()
- DefinitionProvider.scala -> Uses a GlobalSymbolIndex, but only index.definitions (also has access to the raw Mtags?)
- OnDemandSymbolIndex.scala -> GlobalSymbolIndex implementation, holds buckets of symbols
8 Code Lenses
Code lenses are refreshed whenever:
- There is a reindexing (
Indexer.indexWorkspace
callslanguageClient.refreshModel
which in turn callsworkspace/codeLens/refresh
(?), which prompts the client to calltextDocument/codeLens
- There is a (re)compilation (
Compilations.compile
callslanguageClient.refreshModel
), ie a user saves their document.
9 Heirarchy
MetalsLanguageServer.scala
->
Compilers.scala
holds a list of ScalaPresentationCompiler.scala
->
Which can create a new MetalsGlobal.scala
object using newCompiler()
->
Completions call tree:
//////////////////////////////// // MetalsLanguageServer.scala // //////////////////////////////// @JsonRequest("textDocument/completion") def completion(params: CompletionParams): CompletableFuture[CompletionList] = CancelTokens.future { token => compilers.completions(params, token) } ///////////////////// // Compilers.scala // ///////////////////// def completions( params: CompletionParams, token: CancelToken, ): Future[CompletionList] = withPCAndAdjustLsp(params) { (pc, pos, adjust) => val offsetParams = CompilerOffsetParams.fromPos(pos, token) pc.complete(offsetParams) .asScala .map { list => adjust.adjustCompletionListInPlace(list) list } }.getOrElse(Future.successful(new CompletionList(Nil.asJava))) ///////////////////////////////////// // ScalaPresentationCompiler.scala // ///////////////////////////////////// override def complete( params: OffsetParams ): CompletableFuture[CompletionList] = compilerAccess.withInterruptableCompiler( EmptyCompletionList(), params.token ) { pc => new CompletionProvider(pc.compiler(), params).completions() } ////////////////////////////// // CompletionProvider.scala // ////////////////////////////// def completions(): CompletionList = { val filename = params.uri().toString() val unit = addCompilationUnit( code = params.text, filename = filename, cursor = Some(params.offset), cursorName = cursorName ) val pos = unit.position(params.offset) val isSnippet = isSnippetEnabled(pos, params.text()) val (i, completion, editRange, query) = safeCompletionsAt(pos, params.uri()) // ... } private def safeCompletionsAt( pos: Position, source: URI ): (InterestingMembers, CompletionPosition, l.Range, String) = { // ... // Call to the presentation compiler is here val completions = completionsAt(pos) match { case CompletionResult.NoResults => new DynamicFallbackCompletions(pos).print() case r => r } // ... val completion = completionPosition( pos, source, params.text(), editRange, completions, latestParentTrees ) // ... finally the return value (items, completion, editRange, query) } /////////////////////// // Completions.scala // /////////////////////// // the implementation of completionPositionUnsafe does a lot of `typedTreeAt(pos).tpe` // which often causes null pointer exceptions, it's easier to catch the error in // completePosition def completionPositionUnsafe( pos: Position, source: URI, text: String, editRange: l.Range, completions: CompletionResult, latestEnclosingArg: List[Tree] ): CompletionPosition = { }