Previous: Hax, Next: RepositoriesUp: Scala

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

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

3.1 Flow

  1. metals sends a buildTarget/compile (link) request to the BSP server (in BuildServerConnection.compile), to ask the build server to compile a list of build targets.
  2. The BSP will start compilation, sending intermittent build/publishDiagnostics notifications back to metals. This is caught by Diagnostics.onBuildPublishDiagnostics which calls Diagnostics.onBuildPublishDiagnostics. This adds the diagnostics in the request to a queue for each path (Diagnostics.diagnostics is a Map[AbsolutePath, Queue[Diagnostic]]). The path is also added to diagnosticsBuffer. No diagnostics are sent to the LSP client at this stage, but they may be cleared if the sent notification had the reset parameter set to true.
  3. build/taskFinish is sent by the BSP server to indicate that compilation has finished, which is caught by ForwardingMetalsBuildClient.buildTaskFinish, which then calls Diagnostics.onFinishCompileBuildTarget, this clears diagnosticsBuffer, 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, ie metals/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 calls languageClient.refreshModel which in turn calls workspace/codeLens/refresh (?), which prompts the client to call textDocument/codeLens
  • There is a (re)compilation (Compilations.compile calls languageClient.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 = {

}  

Author: root

Created: 2024-03-23 Sat 11:44

Validate