Inside the IDE: Untangling the magic behind the scenes
What happens inside the IDE ?
case class Test(fieldA: Int, fieldB: Int)
val x = Test(1, 2)
x.fie@@
Completion
/** Very good documentation */
case class Test(fieldA: Int, fieldB: Int)
val <<x>> = Test(1, 2)
Hover
case class Test(fieldA: Int, fieldB: Int)
val x = <<Test>>(1, 2)
Go-to Definition
How hard is it really to create an IDE ?
IDE
Editor + Language Support
Editor
Language Support
Editor
- I/O operations
- Performance
- Interactive features
- Debugger
Language Support
Editor
- I/O operations
- Performance
- Interactive features
- Debugger
Language Support
???
Language syntax and complexity
class Animal:
def __init__(self, species):
self.species = species
def speak(self):
print(f"I am a {self.species}")
cat = Animal("cat")
cat.speak()
given Conversion[Int, Long] =
(x: Int) => x.toLong
val str: Long = 42
Dynamic vs. Static Typing
def add(a, b):
return a + b
def add(a: Number, b: Number) =
a + b
Interoperability with Other Languages
new java.util.ArrayList[Int]()
class BetterJavaList extends java.util.List
Frequency of Language Changes
List(1, 2, 3).map { x =>
x + x
}
List(1, 2, 3).map: x =>
x + x
def test(implicit ctx: Context) = ???
def test(using ctx: Context) = ???
Approaches in different languages
Tool | Parser | Type Checker |
---|---|---|
Python Language Server | built-in 'ast' module | custom based on type hints or heuristics |
Rust analyzer | custom incremental parsing | custom type inference engine |
TS language server | custom parser in the compiler | compiler's built-in type checker |
Gopls | standard parser from Go stdlib | go type checker from Go stdlib |
Kotlin | compiler parser | compiler's type checker |
IntelliJ Scala Plugin | custom parser | own type inference engine |
Scala Metals | compiler parser + scalameta parser | compiler's type checker |
Custom vs Built-in
- easy IDE related optimizations
- has to be updated by maintainers
- results may diverge from the official language type checker
- duplicated work
- requires more resources
- customization is harder as it has to be done directly in the compiler
- updated by the compiler team
- 1-1 alignment with the compiler
- strictly bound to the compiler version
Custom vs Built-in
- easy IDE related optimizations
- has to be updated by maintainers
- results may diverge from the official language type checker
- duplicated work
- requires more resources
- customization is harder as it has to be done directly in the compiler
- updated by the compiler team
- 1-1 alignment with the compiler
- strictly bound to the compiler version
Metals
EDITOR - LSP - BSP
- -
https://slides.com/tomekgodzik/metals-heaven
Lets go deeper
Presentation Compiler
What the F[_] is the Presentation Compiler ???
- compiler designed to be used by IDE
- provides high level API
The API
public abstract class PresentationCompiler {
// type CF = CompletableFuture
CF<List<Node>> semanticTokens(VirtualFileParams params);
CF<CompletionList> complete(OffsetParams params);
CF<CompletionItem> completionItemResolve(CompletionItem item, String symbol);
CF<SignatureHelp> signatureHelp(OffsetParams params);
CF<Optional<HoverSignature>> hover(OffsetParams params);
CF<Optional<Range>> prepareRename(OffsetParams params);
CF<List<TextEdit>> rename(OffsetParams params, String name);
CF<DefinitionResult> definition(OffsetParams params);
CF<DefinitionResult> typeDefinition(OffsetParams params);
CF<List<DocumentHighlight>> documentHighlight(OffsetParams params);
CF<String> getTasty(URI targetUri, boolean isHttpEnabled);
CF<List<AutoImportsResult>> autoImports(String name, OffsetParams params, Boolean isExtension);
CF<List<TextEdit>> implementAbstractMembers(OffsetParams params);
CF<List<TextEdit>> insertInferredType(OffsetParams params);
CF<List<TextEdit>> inlineValue(OffsetParams params)
CF<List<TextEdit>> extractMethod(RangeParams range, OffsetParams extractionPos);
CF<List<TextEdit>> convertToNamedArguments(OffsetParams params, List<Integer> argIndices);
CF<List<Diagnostic>> didChange(VirtualFileParams params);
void didClose(URI uri);
CF<byte[]> semanticdbTextDocument(URI filename, String code);
CF<List<SelectionRange>> selectionRange(List<OffsetParams> params);
}
The beginnings
Scala IDE
First Presentation Compiler
May. 2009
Feb. 2012
First Scala 3 commit
May 2017
Scala 3 Presentation Compiler with Dotty Language Server
May 2020
Scala 3 support in Metals
The beginnings
Scala IDE
May 2017
Scala 3 Presentation Compiler with Dotty Language Server
May 2020
Scala 3 support in Metals
ctrl + c, ctrl + v
First Presentation Compiler
May. 2009
Feb. 2012
First Scala 3 commit
API implementation
override def hover(params: TextDocumentPositionParams) = computeAsync { cancelToken =>
val uri = new URI(params.getTextDocument.getUri)
val driver = driverFor(uri)
implicit def ctx: Context = driver.currentCtx
val pos = sourcePosition(driver, uri, params.getPosition)
val trees = driver.openedTrees(uri)
val path = Interactive.pathTo(trees, pos)
val tp = Interactive.enclosingType(trees, pos)
val tpw = tp.widenTermRefExpr
if (tp.isError || tpw == NoType) null
// null here indicates that no response should be sent
else {
Interactive.enclosingSourceSymbols(path, pos) match {
case Nil => null
case symbols =>
val docComments = symbols.flatMap(ParsedComment.docOf)
val content = hoverContent(Some(tpw.show), docComments)
new Hover(content, null)
}
}
API implementation
override def completion(params: CompletionParams) = computeAsync { cancelToken =>
val uri = new URI(params.getTextDocument.getUri)
val driver = driverFor(uri)
implicit def ctx: Context = driver.currentCtx
val pos = sourcePosition(driver, uri, params.getPosition)
val items = driver.compilationUnits.get(uri) match {
case Some(unit) =>
val freshCtx = ctx.fresh.setCompilationUnit(unit)
Completion.completions(pos)(using freshCtx)._2
case None => Nil
}
JEither.forRight(new CompletionList(
/*isIncomplete = */ false, items.map(completionItem).asJava))
}
override def hover(params: TextDocumentPositionParams) = computeAsync { cancelToken =>
val uri = new URI(params.getTextDocument.getUri)
val driver = driverFor(uri)
implicit def ctx: Context = driver.currentCtx
val pos = sourcePosition(driver, uri, params.getPosition)
val trees = driver.openedTrees(uri)
val path = Interactive.pathTo(trees, pos)
val tp = Interactive.enclosingType(trees, pos)
val tpw = tp.widenTermRefExpr
if (tp.isError || tpw == NoType) null
// null here indicates that no response should be sent
else {
Interactive.enclosingSourceSymbols(path, pos) match {
case Nil => null
case symbols =>
val docComments = symbols.flatMap(ParsedComment.docOf)
val content = hoverContent(Some(tpw.show), docComments)
new Hover(content, null)
}
}
Lets go even deeper
Interactive Compiler
What the F[_] is the Interactive Compiler ???
- asynchronous
- interruptible
- targeted
- stop after type-checking
Parser
Typer
PostTyper
SetRootTree
Frontend
Pickler
Transform
Backend
InteractiveCompiler
class InteractiveCompiler extends Compiler {
// TODO: Figure out what phases should be run in IDEs
// More phases increase latency but allow us to report more errors.
// This could be improved by reporting errors back to the IDE
// after each phase group instead of waiting for the pipeline to finish.
override def phases: List[List[Phase]] = List(
List(new Parser),
List(new TyperPhase),
List(new transform.SetRootTree),
List(new transform.CookComments)
)
}
InteractiveDriver
/** A Driver subclass designed to be used from IDEs */
class InteractiveDriver(val settings: List[String]) extends Driver:
import tpd._
override def sourcesRequired: Boolean = false
private val compiler: Compiler = new InteractiveCompiler
def openedFiles: Map[URI, SourceFile] = myOpenedFiles
def openedTrees: Map[URI, List[SourceTree]] = myOpenedTrees
def compilationUnits: Map[URI, CompilationUnit] = myCompilationUnits
def run(uri: URI, source: SourceFile): List[Diagnostic] =
compiler.run(uri, source) // simplification
Interactive
object Interactive {
import ast.tpd._
def isDefinition(tree: Tree): Boolean
def enclosingType(trees: List[SourceTree], pos: SourcePosition)(using Context): Type
def enclosingTree(trees: List[SourceTree], pos: SourcePosition)(using Context): Tree
def enclosingTree(path: List[Tree])(using Context): Tree
def enclosingSourceSymbols(path: List[Tree], pos: SourcePosition)(using Context): List[Symbol]
def matchSymbol(tree: Tree, sym: Symbol, include: Include.Set)(using Context): Boolean
def namedTrees(trees: List[SourceTree], include: Include.Set, sym: Symbol)(using Context): List[SourceTree]
def pathTo(trees: List[SourceTree], pos: SourcePosition)(using Context): List[Tree]
def contextOfStat(stats: List[Tree], stat: Tree, exprOwner: Symbol, ctx: Context): Context
def contextOfPath(path: List[Tree])(using Context): Context
def enclosingDefinitionInPath(path: List[Tree])(using Context): Tree
def findDefinitions(path: List[Tree], pos: SourcePosition, driver: InteractiveDriver): List[SourceTree]
def localize(symbol: Symbol, sourceDriver: InteractiveDriver, targetDriver: InteractiveDriver): Symbol
def implementationFilter(sym: Symbol)(using Context): NameTree => Boolean
def isRenamed(tree: NameTree)(using Context): Boolean
def sameName(n0: Name, n1: Name): Boolean
}
case class Test(fieldA: Int, fieldB: Int)
val x = Test(1, 2)
x.fie@@
Completion example
override def completion(params: CompletionParams) = computeAsync { cancelToken =>
val uri = new URI(params.getTextDocument.getUri)
val driver = driverFor(uri)
implicit def ctx: Context = driver.currentCtx
val pos = sourcePosition(driver, uri, params.getPosition)
val items = driver.compilationUnits.get(uri) match {
case Some(unit) =>
val freshCtx = ctx.fresh.setCompilationUnit(unit)
Completion.completions(pos)(using freshCtx)._2
case None => Nil
}
JEither.forRight(new CompletionList(
/*isIncomplete = */ false, items.map(completionItem).asJava))
}
def completions(pos: SourcePosition)(using Context): (Int, List[Completion]) =
val tpdPath = Interactive
.pathTo(ctx.compilationUnit.tpdTree, pos.span)
val completionContext = Interactive
.contextOfPath(tpdPath).withPhase(Phases.typerPhase)
computeCompletions(pos, mode, rawPrefix, tpdPath)(using completionContext)
Completions
case class Test(fieldA: Int, fieldB: Int)
val x = Test(1, 2)
x.fie@@
case class Test(fieldA: Int, fieldB: Int)
val x = Test(1, 2)
x.fie@@
package <empty> { [0..86]
case class Test(fieldA: Int, fieldB: Int) extends Object(), <17..60>
_root_.scala.Product, _root_.scala.Serializable {
val fieldA: Int
val fieldB: Int
def copy(fieldA: Int, fieldB: Int): Test.Test =
new Test(fieldA, fieldB)
def copy$default$1: Int @uncheckedVariance = Test.this.fieldA
def copy$default$2: Int @uncheckedVariance = Test.this.fieldB
def _1: Int = this.fieldA
def _2: Int = this.fieldB
}
final lazy module val Test: Test = new Test() <60..68>
final module class Test() extends AnyRef() { this: Test.type => <68..75>
def apply(fieldA: Int, fieldB: Int): Test =
new Test(fieldA, fieldB)
def unapply(x$1: Test): Test = x$1
override def toString: String = "Test"
}
val x: Test = Test.apply(1, 2) <75..81>
x.fie [81..83..86]
}
We want to find tree at `@@` (position 86)
case class Test(fieldA: Int, fieldB: Int)
val x = Test(1, 2)
x.fie@@
List(
x.fie,
package <empty> {
case class Test(val fieldA: Int, val fieldB: Int) {}
val x = Test(1, 2)
x.fie
}
)
List(
Select(Ident(x),fie),
PackageDef(Ident(<empty>), {
case class def + ValDef(x, ...) + Select(Ident(x), fie)
}
)
Pretty Printed pathTo(query)
val query = SrcPos(86)
pathTo(query)
case class Test(fieldA: Int, fieldB: Int)
val x = Test(1, 2)
x.fie@@
List(
x.fie,
package <empty> {
case class Test(val fieldA: Int, val fieldB: Int) {}
val x = Test(1, 2)
x.fie
}
)
List(
Select(Ident(x),fie),
PackageDef(Ident(<empty>), {
case class def + ValDef(x, ...) + Select(Ident(x), fie)
}
)
Pretty Printed pathTo(query)
val query = SrcPos(86)
pathTo(query)
case class Test(fieldA: Int, fieldB: Int)
val x = Test(1, 2)
x.fie@@
List(
x.fie,
package <empty> {
case class Test(val fieldA: Int, val fieldB: Int) {}
val x = Test(1, 2)
x.fie
}
)
List(
Select(Ident(x),fie),
PackageDef(Ident(<empty>), {
case class def + ValDef(x, ...) + Select(Ident(x), fie)
}
)
Pretty Printed pathTo(query)
val query = SrcPos(86)
pathTo(query)
def completions(pos: SourcePosition)(using Context): (Int, List[Completion]) =
val tpdPath = Interactive
.pathTo(ctx.compilationUnit.tpdTree, pos.span)
val completionContext = Interactive
.contextOfPath(tpdPath).withPhase(Phases.typerPhase)
computeCompletions(pos, tpdPath)(using completionContext)
Completions
private def computeCompletions(pos: SourcePosition, path: List[Tree])
(using Context): (Int, List[Completion]) = {
val path0 = pathBeforeDesugaring(path, pos)
// List(Select(Ident(x),fie), PackageDef(Ident(<empty>), ...), ...)
val mode = completionMode(path0, pos) // Mode.Term
val rawPrefix = completionPrefix(path0, pos) // "fie"
val hasBackTick = rawPrefix.headOption.contains('`')
val prefix = if hasBackTick then rawPrefix.drop(1) else rawPrefix
val completer = new Completer(mode, prefix, pos)
val completions = path0 match {
case Select(qual @ This(_), _) :: _ if qual.span.isSynthetic =>
completer.scopeCompletions
//qual.tpe = TypeDef("Test" ...)
case Select(qual, _) :: _ if qual.tpe.hasSimpleKind =>
completer.selectionCompletions(qual)
case Select(qual, _) :: _ =>
Map.empty
case (tree: ImportOrExport) :: _ =>
completer.directMemberCompletions(tree.expr)
case (_: untpd.ImportSelector) :: Import(expr, _) :: _ =>
completer.directMemberCompletions(expr)
case _ =>
completer.scopeCompletions
}
describeCompletions(completions)
Completions
def selectionCompletions(qual: Tree)(using Context): CompletionMap =
val adjustedQual = widenQualifier(qual)
implicitConversionMemberCompletions(adjustedQual) ++
extensionCompletions(adjustedQual) ++
directMemberCompletions(adjustedQual)
Completions
/** @param site The type to inspect.
* @return The members of `site` that are accessible and pass the include filter.
*/ // type of x => Test
private def accessibleMembers(site: Type)(using Context): Seq[SingleDenotation] = {
def appendMemberSyms(name: Name, buf: mutable.Buffer[SingleDenotation]): Unit =
try
val member = site.member(name)
// case class Test(fieldA: Int, fieldB: Int)
if member.symbol.is(ParamAccessor) && !member.symbol.isAccessibleFrom(site) then
buf ++= site.nonPrivateMember(name).alternatives
else
buf ++= member.alternatives
catch
case ex: TypeError =>
val members = site.memberDenots(completionsFilter, appendMemberSyms).collect {
case mbr if include(mbr, mbr.name)
&& mbr.symbol.isAccessibleFrom(site) => mbr
}
val refinements = extractRefinements(site).filter(mbr => include(mbr, mbr.name))
members ++ refinements
}
Completions
private def computeCompletions(pos: SourcePosition, path: List[Tree])
(using Context): (Int, List[Completion]) = {
{ ... }
val completions = path0 match {
case Select(qual @ This(_), _) :: _ if qual.span.isSynthetic =>
completer.scopeCompletions
//qual = TypeDef("Test" ...)
case Select(qual, _) :: _ if qual.tpe.hasSimpleKind =>
completer.selectionCompletions(qual)
case Select(qual, _) :: _ =>
Map.empty
case (tree: ImportOrExport) :: _ =>
completer.directMemberCompletions(tree.expr)
case (_: untpd.ImportSelector) :: Import(expr, _) :: _ =>
completer.directMemberCompletions(expr)
case _ =>
completer.scopeCompletions
}
describeCompletions(completions)
Completions
def describeCompletions(completions: CompletionMap)(using Context): List[Completion] =
for
(name, denots) <- completions.toList
denot <- denots
yield
Completion(name.show, description(denot), List(denot.symbol))
Completions
Metals
Presentation Compiler
Interactive Compiler
Compiler
Compiler
Metals
Presentation Compiler
Interactive Compiler
Compiler
lampepfl/dotty
Compiler
scalameta/metals
Presentation Compiler Tradeoffs
Benefits | Drawbacks |
---|---|
1 - 1 relation with the compiler | strictly bound to the version |
bugs can be fixed in the old Scala versions | released from another repository |
it can be broken by internal API changes | |
latest syntax and features may require additional changes thus may not be released immediately |
Stable Presentation Compiler
Metals
Presentation Compiler
Interactive Compiler
Compiler
Compiler
lampepfl/dotty
scalameta/metals
Metals
Presentation Compiler
Interactive Compiler
Compiler
Compiler
lampepfl/dotty
scalameta/metals
Stable Presentation Compiler
May 2017
Scala 3 Presentation Compiler with Dotty Language Server
May 2020
Scala 3 support in Metals
ctrl + c, ctrl + v
First Presentation Compiler
May. 2009
Feb. 2012
First Scala 3 commit
Stable Presentation Compiler
May 2017
Scala 3 Presentation Compiler with Dotty Language Server
May 2020
Scala 3 support in Metals
ctrl + c, ctrl + v
June 2023
Scala 3 Presentation Compiler provided by Dotty
First Presentation Compiler
May. 2009
Feb. 2012
First Scala 3 commit
Stable Presentation Compiler
May 2017
Scala 3 Presentation Compiler with Dotty Language Server
May 2020
Scala 3 support in Metals
ctrl + c, ctrl + v
June 2023
Scala 3 Presentation Compiler provided by Dotty
ctrl + c, ctrl + v
First Presentation Compiler
May. 2009
Feb. 2012
First Scala 3 commit
What's the difference ?
Benefits | Drawbacks |
---|---|
support for every new Scala version including nightly releases is now supported ! | iterations are slower as everything happens in the compiler |
1 - 1 relation with the compiler | bugfixing / adding new features to already released version is very expensive |
always supports latest syntax | |
internal compiler changes won't break the IDE | |
API is stabilized and tested with MiMA | |
easier maintanence |
Thank you !
for your contributions
https://www.scala-lang.org/blog/2023/10/17/feedback-wanted-error-messages.html
Slides
https://slides.com/rochala
Do you have any questions ?
Copy of Inside the IDE: Untangling the magic behind the scenes
By rochala
Copy of Inside the IDE: Untangling the magic behind the scenes
- 93