From 291656c629ff6e24079bf59b4ad1f030c264947e Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Wed, 26 Oct 2016 19:30:54 +0200 Subject: [PATCH 01/21] Compiler#rootContext: do not overwrite initial modes --- compiler/src/dotty/tools/dotc/Compiler.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 22c6c7f342fe..b0b521f30819 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -129,7 +129,7 @@ class Compiler { val start = bootstrap.fresh .setOwner(defn.RootClass) .setTyper(new Typer) - .setMode(Mode.ImplicitsEnabled) + .addMode(Mode.ImplicitsEnabled) .setTyperState(new MutableTyperState(ctx.typerState, ctx.typerState.reporter, isCommittable = true)) .setFreshNames(new FreshNameCreator.Default) ctx.initialize()(start) // re-initialize the base context with start From e9f4a0781ab5f3ac748f006bfefb533937af31ba Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Fri, 25 Nov 2016 23:36:00 +0100 Subject: [PATCH 02/21] Fix navigation with lazy trees FIXME: This is too easy to forget, some sort of Traverser should be used instead of using productIterator by hand. --- .../src/dotty/tools/dotc/ast/NavigateAST.scala | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/NavigateAST.scala b/compiler/src/dotty/tools/dotc/ast/NavigateAST.scala index 33aa87d8e8e5..7c1fd5ed3a2e 100644 --- a/compiler/src/dotty/tools/dotc/ast/NavigateAST.scala +++ b/compiler/src/dotty/tools/dotc/ast/NavigateAST.scala @@ -4,7 +4,7 @@ package ast import core.Contexts.Context import core.Decorators._ import util.Positions._ -import Trees.{MemberDef, DefTree} +import Trees.{MemberDef, DefTree, WithLazyField} /** Utility functions to go from typed to untyped ASTs */ object NavigateAST { @@ -75,8 +75,17 @@ object NavigateAST { path } def singlePath(p: Positioned, path: List[Positioned]): List[Positioned] = - if (p.pos contains pos) childPath(p.productIterator, p :: path) - else path + if (p.pos contains pos) { + // FIXME: We shouldn't be manually forcing trees here, we should replace + // our usage of `productIterator` by something in `Positioned` that takes + // care of low-level details like this for us. + p match { + case p: WithLazyField[_] => + p.forceIfLazy + case _ => + } + childPath(p.productIterator, p :: path) + } else path singlePath(from, Nil) } -} \ No newline at end of file +} From e29195d9587373b552346bc3d3fb9454b76fd28e Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Fri, 28 Oct 2016 01:24:18 +0200 Subject: [PATCH 03/21] Template denotations should stay valid if their owner is valid --- compiler/src/dotty/tools/dotc/core/SymDenotations.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index d3865d1e0ce1..debd4a5eeb4d 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -62,7 +62,8 @@ trait SymDenotations { this: Context => || owner.isRefinementClass || owner.is(Scala2x) || (owner.unforcedDecls.lookupAll(denot.name) contains denot.symbol) - || denot.isSelfSym) + || denot.isSelfSym + || denot.isLocalDummy) } catch { case ex: StaleSymbol => false } From 30bd0a7d06d14017304cd0d89def1b98cc175d83 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Sun, 1 Jan 2017 00:12:50 +0100 Subject: [PATCH 04/21] Fix exception when calling makePackageObjPrefixExplicit with the root package --- compiler/src/dotty/tools/dotc/core/TypeOps.scala | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index 99e52eae6d41..816a3b48f356 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -314,11 +314,14 @@ trait TypeOps { this: Context => // TODO: Make standalone object. case _ => tpe } - tpe.prefix match { - case pre: ThisType if pre.cls is Package => tryInsert(pre.cls) - case pre: TermRef if pre.symbol is Package => tryInsert(pre.symbol.moduleClass) - case _ => tpe - } + if (tpe.symbol.isRoot) + tpe + else + tpe.prefix match { + case pre: ThisType if pre.cls is Package => tryInsert(pre.cls) + case pre: TermRef if pre.symbol is Package => tryInsert(pre.symbol.moduleClass) + case _ => tpe + } } /** Normalize a list of parent types of class `cls` that may contain refinements From b2a61c1ad81d47232d53f77e9bc263dd30b2d1bc Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Thu, 11 May 2017 17:11:43 +0200 Subject: [PATCH 05/21] Add ClassSymbol#tree to get the tree of a top-level class When typechecking a class from source or when unpickling it from tasty, we normally do not keep a reference to the class tree, but there's a wealth of information only present in the tree that we may wish to use. In an IDE for example, this makes it easy to provide features like "jump to definition". This commit unlocks this potential by adding a `ClassSymbol#tree` method that, when used together with the `-Yretain-trees` flag, returns the tree corresponding to a top-level class. This also replaces the `unpicklers` map in `CompilationUnit` by `ClassSymbol#unpickler`. --- .../dotty/tools/dotc/CompilationUnit.scala | 2 -- compiler/src/dotty/tools/dotc/FromTasty.scala | 10 +++--- .../tools/dotc/config/ScalaSettings.scala | 1 + .../dotty/tools/dotc/core/SymbolLoaders.scala | 10 ++++-- .../src/dotty/tools/dotc/core/Symbols.scala | 31 ++++++++++++++++++- .../dotc/core/tasty/DottyUnpickler.scala | 9 +++++- .../src/dotty/tools/dotc/typer/Typer.scala | 3 ++ 7 files changed, 54 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/CompilationUnit.scala b/compiler/src/dotty/tools/dotc/CompilationUnit.scala index 491c2bd9b144..da8b3923be1e 100644 --- a/compiler/src/dotty/tools/dotc/CompilationUnit.scala +++ b/compiler/src/dotty/tools/dotc/CompilationUnit.scala @@ -19,6 +19,4 @@ class CompilationUnit(val source: SourceFile) { /** Pickled TASTY binaries, indexed by class. */ var pickled: Map[ClassSymbol, Array[Byte]] = Map() - - var unpicklers: Map[ClassSymbol, TastyUnpickler] = Map() } diff --git a/compiler/src/dotty/tools/dotc/FromTasty.scala b/compiler/src/dotty/tools/dotc/FromTasty.scala index f211462e09b6..6f74a5d46ea2 100644 --- a/compiler/src/dotty/tools/dotc/FromTasty.scala +++ b/compiler/src/dotty/tools/dotc/FromTasty.scala @@ -83,17 +83,15 @@ object FromTasty extends Driver { case clsd: ClassDenotation => clsd.infoOrCompleter match { case info: ClassfileLoader => - info.load(clsd) match { - case Some(unpickler: DottyUnpickler) => - val List(unpickled) = unpickler.body(ctx.addMode(Mode.ReadPositions)) + info.load(clsd) + val unpickled = clsd.symbol.asClass.tree + if (unpickled != null) { val unit1 = new CompilationUnit(new SourceFile(clsd.symbol.sourceFile, Seq())) unit1.tpdTree = unpickled - unit1.unpicklers += (clsd.classSymbol -> unpickler.unpickler) force.traverse(unit1.tpdTree) unit1 - case _ => + } else cannotUnpickle(s"its class file ${info.classfile} does not have a TASTY attribute") - } case info => cannotUnpickle(s"its info of type ${info.getClass} is not a ClassfileLoader") } diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 3900255bba9a..8eeaf9e9f976 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -98,6 +98,7 @@ class ScalaSettings extends Settings.SettingGroup { val YforceSbtPhases = BooleanSetting("-Yforce-sbt-phases", "Run the phases used by sbt for incremental compilation (ExtractDependencies and ExtractAPI) even if the compiler is ran outside of sbt, for debugging.") val YdumpSbtInc = BooleanSetting("-Ydump-sbt-inc", "For every compiled foo.scala, output the API representation and dependencies used for sbt incremental compilation in foo.inc, implies -Yforce-sbt-phases.") val YcheckAllPatmat = BooleanSetting("-Ycheck-all-patmat", "Check exhaustivity and redundancy of all pattern matching (used for testing the algorithm)") + val YretainTrees = BooleanSetting("-Yretain-trees", "Retain trees for top-level classes, accessible from ClassSymbol#tree") /** Area-specific debug output */ val Yexplainlowlevel = BooleanSetting("-Yexplain-lowlevel", "When explaining type errors, show types at a lower level.") diff --git a/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala index b6f5cde3b029..c17cc00892ab 100644 --- a/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala +++ b/compiler/src/dotty/tools/dotc/core/SymbolLoaders.scala @@ -17,6 +17,7 @@ import Decorators.{PreNamedString, StringInterpolators} import classfile.ClassfileParser import util.Stats import scala.util.control.NonFatal +import ast.Trees._ object SymbolLoaders { /** A marker trait for a completer that replaces the original @@ -320,9 +321,14 @@ class ClassfileLoader(val classfile: AbstractFile) extends SymbolLoader { override def doComplete(root: SymDenotation)(implicit ctx: Context): Unit = load(root) - def load(root: SymDenotation)(implicit ctx: Context): Option[ClassfileParser.Embedded] = { + def load(root: SymDenotation)(implicit ctx: Context): Unit = { val (classRoot, moduleRoot) = rootDenots(root.asClass) - new ClassfileParser(classfile, classRoot, moduleRoot)(ctx).run() + (new ClassfileParser(classfile, classRoot, moduleRoot)(ctx)).run() match { + case Some(unpickler: tasty.DottyUnpickler) if ctx.settings.YretainTrees.value => + classRoot.symbol.asClass.unpickler = unpickler + moduleRoot.symbol.asClass.unpickler = unpickler + case _ => + } } } diff --git a/compiler/src/dotty/tools/dotc/core/Symbols.scala b/compiler/src/dotty/tools/dotc/core/Symbols.scala index e0d9aca2bbc0..283a6bb645ad 100644 --- a/compiler/src/dotty/tools/dotc/core/Symbols.scala +++ b/compiler/src/dotty/tools/dotc/core/Symbols.scala @@ -20,7 +20,8 @@ import DenotTransformers._ import StdNames._ import NameOps._ import NameKinds.LazyImplicitName -import ast.tpd.Tree +import ast.tpd +import tpd.Tree import ast.TreeTypeMap import Constants.Constant import reporting.diagnostic.Message @@ -552,6 +553,34 @@ object Symbols { type ThisName = TypeName + /** If this is a top-level class, and if `-Yretain-trees` is set, return the TypeDef tree + * for this class, otherwise EmptyTree. + */ + def tree(implicit ctx: Context): tpd.Tree /* tpd.TypeDef | tpd.EmptyTree */ = { + // TODO: Consider storing this tree like we store lazy trees for inline functions + if (unpickler != null && !denot.isAbsent) { + assert(myTree.isEmpty) + + import ast.Trees._ + + def findTree(tree: tpd.Tree): Option[tpd.TypeDef] = tree match { + case PackageDef(_, stats) => + stats.flatMap(findTree).headOption + case tree: tpd.TypeDef if tree.symbol == this => + Some(tree) + case _ => + None + } + val List(unpickledTree) = unpickler.body(ctx.addMode(Mode.ReadPositions)) + unpickler = null + + myTree = findTree(unpickledTree).get + } + myTree + } + private[dotc] var myTree: tpd.Tree = tpd.EmptyTree + private[dotc] var unpickler: tasty.DottyUnpickler = _ + /** The source or class file from which this class was generated, null if not applicable. */ override def associatedFile(implicit ctx: Context): AbstractFile = if (assocFile != null || (this.owner is PackageClass) || this.isEffectiveRoot) assocFile diff --git a/compiler/src/dotty/tools/dotc/core/tasty/DottyUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/DottyUnpickler.scala index 28916a781921..8178019aa033 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/DottyUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/DottyUnpickler.scala @@ -46,8 +46,15 @@ class DottyUnpickler(bytes: Array[Byte]) extends ClassfileParser.Embedded { def enter(roots: Set[SymDenotation])(implicit ctx: Context): Unit = treeUnpickler.enterTopLevel(roots) + /** Only used if `-Yretain-trees` is set. */ + private[this] var myBody: List[Tree] = _ /** The unpickled trees, and the source file they come from. */ def body(implicit ctx: Context): List[Tree] = { - treeUnpickler.unpickle() + def computeBody() = treeUnpickler.unpickle() + if (ctx.settings.YretainTrees.value) { + if (myBody == null) + myBody = computeBody() + myBody + } else computeBody() } } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 644bcd7cbe0a..7c7102567a5b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1341,6 +1341,9 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit // check value class constraints checkDerivedValueClass(cls, body1) + if (ctx.settings.YretainTrees.value) { + cls.myTree = cdef1 + } cdef1 // todo later: check that From 13f6a912f974d7e446558c13db7894b8807b17cd Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Thu, 11 May 2017 17:24:38 +0200 Subject: [PATCH 06/21] Make "new StoreReporter(null)" work --- compiler/src/dotty/tools/dotc/reporting/StoreReporter.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/reporting/StoreReporter.scala b/compiler/src/dotty/tools/dotc/reporting/StoreReporter.scala index 34b1098825ec..343e6a517712 100644 --- a/compiler/src/dotty/tools/dotc/reporting/StoreReporter.scala +++ b/compiler/src/dotty/tools/dotc/reporting/StoreReporter.scala @@ -40,5 +40,5 @@ class StoreReporter(outer: Reporter) extends Reporter { if (infos != null) try infos.toList finally infos = null else Nil - override def errorsReported = hasErrors || outer.errorsReported + override def errorsReported = hasErrors || (outer != null && outer.errorsReported) } From 3948861fb20ad6c626e50d65f6e19eb5041e9949 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Fri, 25 Nov 2016 20:39:43 +0100 Subject: [PATCH 07/21] Fix unpickling an inner class in a Scala2 object without companion When we unpickle a Scala2 module we might not have a companion class to unpickle, when this happen we call markAbsent() on the denotation of the companion class, which means that calls to companionModule will not work. Fixed by falling back to the more robust scalacLinkedClass. --- .../dotty/tools/dotc/core/classfile/ClassfileParser.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala index c23ff6d2affe..92941f3eb145 100644 --- a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala +++ b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala @@ -827,7 +827,12 @@ class ClassfileParser( def getMember(sym: Symbol, name: Name)(implicit ctx: Context): Symbol = if (static) if (sym == classRoot.symbol) staticScope.lookup(name) - else sym.companionModule.info.member(name).symbol + else { + var module = sym.companionModule + if (module == NoSymbol && sym.isAbsent) + module = sym.scalacLinkedClass + module.info.member(name).symbol + } else if (sym == classRoot.symbol) instanceScope.lookup(name) else sym.info.member(name).symbol From b3befee6f4b9ce09babfc577c39fdbd6d2b4f299 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Fri, 26 May 2017 19:49:56 +0200 Subject: [PATCH 08/21] NavigateAST#pathTo: Add an option to skip over zero-extent nodes --- compiler/src/dotty/tools/dotc/ast/NavigateAST.scala | 9 +++++++-- compiler/src/dotty/tools/dotc/util/Positions.scala | 3 +++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/NavigateAST.scala b/compiler/src/dotty/tools/dotc/ast/NavigateAST.scala index 7c1fd5ed3a2e..b6ff80277d35 100644 --- a/compiler/src/dotty/tools/dotc/ast/NavigateAST.scala +++ b/compiler/src/dotty/tools/dotc/ast/NavigateAST.scala @@ -61,8 +61,13 @@ object NavigateAST { /** The reverse path from node `from` to the node that closest encloses position `pos`, * or `Nil` if no such path exists. If a non-empty path is returned it starts with * the node closest enclosing `pos` and ends with `from`. + * + * @param skipZeroExtent If true, skip over zero-extent nodes in the search. These nodes + * do not correspond to code the user wrote since their start and + * end point are the same, so this is useful when trying to reconcile + * nodes with source code. */ - def pathTo(pos: Position, from: Positioned)(implicit ctx: Context): List[Positioned] = { + def pathTo(pos: Position, from: Positioned, skipZeroExtent: Boolean = false)(implicit ctx: Context): List[Positioned] = { def childPath(it: Iterator[Any], path: List[Positioned]): List[Positioned] = { while (it.hasNext) { val path1 = it.next match { @@ -75,7 +80,7 @@ object NavigateAST { path } def singlePath(p: Positioned, path: List[Positioned]): List[Positioned] = - if (p.pos contains pos) { + if (p.pos.exists && !(skipZeroExtent && p.pos.isZeroExtent) && p.pos.contains(pos)) { // FIXME: We shouldn't be manually forcing trees here, we should replace // our usage of `productIterator` by something in `Positioned` that takes // care of low-level details like this for us. diff --git a/compiler/src/dotty/tools/dotc/util/Positions.scala b/compiler/src/dotty/tools/dotc/util/Positions.scala index c3890cc9a463..349933f5db7b 100644 --- a/compiler/src/dotty/tools/dotc/util/Positions.scala +++ b/compiler/src/dotty/tools/dotc/util/Positions.scala @@ -79,6 +79,9 @@ object Positions { /** Is this position source-derived? */ def isSourceDerived = !isSynthetic + /** Is this a zero-extent position? */ + def isZeroExtent = start == end + /** A position where all components are shifted by a given `offset` * relative to this position. */ From 2ada3f69208e633dd4b22a056dc25f52497b6ec1 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Sat, 20 May 2017 22:35:43 +0200 Subject: [PATCH 09/21] Preserve zero-extent positions when setting children positions If a tree has no position or a zero-extent position, it should be synthetic. We can preserve this invariant by always setting a zero-extent position for these trees here. --- compiler/src/dotty/tools/dotc/ast/Positioned.scala | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Positioned.scala b/compiler/src/dotty/tools/dotc/ast/Positioned.scala index 51949c6fefb5..faa51d8a2eda 100644 --- a/compiler/src/dotty/tools/dotc/ast/Positioned.scala +++ b/compiler/src/dotty/tools/dotc/ast/Positioned.scala @@ -72,8 +72,16 @@ abstract class Positioned extends DotClass with Product { // is known, from left to right. def fillIn(ps: List[Positioned], start: Int, end: Int): Unit = ps match { case p :: ps1 => - p.setPos(Position(start, end)) - fillIn(ps1, end, end) + // If a tree has no position or a zero-extent position, it should be + // synthetic. We can preserve this invariant by always setting a + // zero-extent position for these trees here. + if (!p.pos.exists || p.pos.isZeroExtent) { + p.setPos(Position(start, start)) + fillIn(ps1, start, end) + } else { + p.setPos(Position(start, end)) + fillIn(ps1, end, end) + } case nil => } while (true) { From f24190d5c60c4e019fba97425b6f3b10167a039d Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Thu, 25 May 2017 17:03:32 +0200 Subject: [PATCH 10/21] Fix position of implicit conversions, eta-expansion and module vals Synthetic trees should get zero-extent positions so that they don't get returned by Interactive#pathTo. This makes it easier to show information related to non-synthetic trees in IDEs. --- compiler/src/dotty/tools/dotc/ast/Desugar.scala | 2 +- compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala | 4 ++-- compiler/src/dotty/tools/dotc/typer/Implicits.scala | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index cb137d46476a..ac5f0b53daa9 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -548,7 +548,7 @@ object desugar { val clsRef = Ident(clsName) val modul = ValDef(moduleName, clsRef, New(clsRef, Nil)) .withMods(mods | ModuleCreationFlags | mods.flags & AccessFlags) - .withPos(mdef.pos) + .withPos(mdef.pos.startPos) val ValDef(selfName, selfTpt, _) = impl.self val selfMods = impl.self.mods if (!selfTpt.isEmpty) ctx.error(ObjectMayNotHaveSelfType(mdef), impl.self.pos) diff --git a/compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala b/compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala index e5480c98d911..77f1006ffc47 100644 --- a/compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala +++ b/compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala @@ -139,8 +139,8 @@ object EtaExpansion { if (mt.paramInfos.length == xarity) mt.paramInfos map (_ => TypeTree()) else mt.paramInfos map TypeTree val params = (mt.paramNames, paramTypes).zipped.map((name, tpe) => - ValDef(name, tpe, EmptyTree).withFlags(Synthetic | Param).withPos(tree.pos)) - var ids: List[Tree] = mt.paramNames map (name => Ident(name).withPos(tree.pos)) + ValDef(name, tpe, EmptyTree).withFlags(Synthetic | Param).withPos(tree.pos.startPos)) + var ids: List[Tree] = mt.paramNames map (name => Ident(name).withPos(tree.pos.startPos)) if (mt.paramInfos.nonEmpty && mt.paramInfos.last.isRepeatedParam) ids = ids.init :+ repeated(ids.last) var body: Tree = Apply(lifted, ids) diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index f18d4dedeee5..eeaabcdc3812 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -743,7 +743,7 @@ trait Implicits { self: Typer => def typedImplicit(cand: Candidate)(implicit ctx: Context): SearchResult = track("typedImplicit") { ctx.traceIndented(i"typed implicit ${cand.ref}, pt = $pt, implicitsEnabled == ${ctx.mode is ImplicitsEnabled}", implicits, show = true) { assert(constr eq ctx.typerState.constraint) val ref = cand.ref - var generated: Tree = tpd.ref(ref).withPos(pos) + var generated: Tree = tpd.ref(ref).withPos(pos.startPos) if (!argument.isEmpty) generated = typedUnadapted( untpd.Apply(untpd.TypedSplice(generated), untpd.TypedSplice(argument) :: Nil), From 49e18fdc7e3bb6204cdc47b9fb9c497972211169 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Thu, 25 May 2017 17:00:29 +0200 Subject: [PATCH 11/21] Fix position of class parents Previously, the position of class parents was the position of the whole class, which means that NavigateAST#pathTo always returned the class parents when looking up a position in the body of the class. Fixed by using the position of the name of the class instead. --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 7c7102567a5b..65bdd540c6fe 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1312,7 +1312,7 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit completeAnnotations(cdef, cls) val constr1 = typed(constr).asInstanceOf[DefDef] - val parentsWithClass = ensureFirstIsClass(parents mapconserve typedParent, cdef.pos.toSynthetic) + val parentsWithClass = ensureFirstIsClass(parents mapconserve typedParent, cdef.namePos) val parents1 = ensureConstrCall(cls, parentsWithClass)(superCtx) val self1 = typed(self)(ctx.outer).asInstanceOf[ValDef] // outer context where class members are not visible val dummy = localDummy(cls, impl) From bfdf732d037845f0eac08b4c83dc4c34a88c95b8 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Wed, 24 May 2017 17:50:25 +0200 Subject: [PATCH 12/21] Fix position of Bind nodes The position of a tree should contain the position of all its children, but that was not the case before this commit for Bind nodes created from Typed nodes. --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 65bdd540c6fe..d87f627b5982 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -484,7 +484,7 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit if (id.name == nme.WILDCARD || id.name == nme.WILDCARD_STAR) ifPat else { import untpd._ - typed(Bind(id.name, Typed(Ident(wildName), tree.tpt)).withPos(id.pos), pt) + typed(Bind(id.name, Typed(Ident(wildName), tree.tpt)).withPos(tree.pos), pt) } case _ => ifExpr } From 1293e2f49b320873c58552ab86f49ef7999b91f8 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Thu, 11 May 2017 22:45:10 +0200 Subject: [PATCH 13/21] Emit empty .tasty files by default Before this commit, we only created .tasty files under -YemitTasty, we haven't decided yet if this should be the default, but even if we don't end up doing that, it is still useful to emit empty .tasty files to signal that the corresponding .class file has a tasty section, this is much simpler to check than parsing classfiles to look for a tasty section. Also, fix -YemitTasty tof work with AbstractFile who are not backed by java.io.File, previously this lead to NullPointerException. --- .../src/dotty/tools/backend/jvm/GenBCode.scala | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/backend/jvm/GenBCode.scala b/compiler/src/dotty/tools/backend/jvm/GenBCode.scala index c1100f4f1e6b..e916329c2101 100644 --- a/compiler/src/dotty/tools/backend/jvm/GenBCode.scala +++ b/compiler/src/dotty/tools/backend/jvm/GenBCode.scala @@ -25,7 +25,7 @@ import Symbols._ import Denotations._ import Phases._ import java.lang.AssertionError -import java.io.{FileOutputStream, File => JFile} +import java.io.{DataOutputStream, File => JFile} import scala.tools.asm import scala.tools.asm.tree._ @@ -205,12 +205,16 @@ class GenBCodePipeline(val entryPoints: List[Symbol], val int: DottyBackendInter val dataAttr = new CustomAttr(nme.TASTYATTR.mangledString, binary) val store = if (mirrorC ne null) mirrorC else plainC store.visitAttribute(dataAttr) + val outTastyFile = getFileForClassfile(outF, store.name, ".tasty") if (ctx.settings.emitTasty.value) { - val outTastyFile = getFileForClassfile(outF, store.name, ".tasty").file - val fos = new FileOutputStream(outTastyFile, false) - fos.write(binary) - fos.close() - + val outstream = new DataOutputStream(outTastyFile.bufferedOutput) + + try outstream.write(binary) + finally outstream.close() + } else if (!outTastyFile.isVirtual) { + // Create an empty file to signal that a tasty section exist in the corresponding .class + // This is much cheaper and simpler to check than doing classfile parsing + outTastyFile.create() } } From 3b882c295dafd45c88022e5419df63875fc4636a Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Fri, 19 May 2017 00:33:02 +0200 Subject: [PATCH 14/21] ClassfileParser: correct types for annotation arguments As specified in https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.16.1, an annotation argument of type boolean, byte, char or short will be represented as a CONSTANT_Integer in the classfile, so when we parse it we get an Int, but before this commit we did not convert this Int to the correct type for the annotation argument, thus creating ill-typed trees. --- .../dotc/core/classfile/ClassfileParser.scala | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala index 92941f3eb145..330362290a26 100644 --- a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala +++ b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala @@ -430,8 +430,9 @@ class ClassfileParser( tag match { case STRING_TAG => if (skip) None else Some(Literal(Constant(pool.getName(index).toString))) - case BOOL_TAG | BYTE_TAG | CHAR_TAG | SHORT_TAG | INT_TAG | - LONG_TAG | FLOAT_TAG | DOUBLE_TAG => + case BOOL_TAG | BYTE_TAG | CHAR_TAG | SHORT_TAG => + if (skip) None else Some(Literal(pool.getConstant(index, tag))) + case INT_TAG | LONG_TAG | FLOAT_TAG | DOUBLE_TAG => if (skip) None else Some(Literal(pool.getConstant(index))) case CLASS_TAG => if (skip) None else Some(Literal(Constant(pool.getType(index)))) @@ -829,7 +830,7 @@ class ClassfileParser( if (sym == classRoot.symbol) staticScope.lookup(name) else { var module = sym.companionModule - if (module == NoSymbol && sym.isAbsent) + if (!module.exists && sym.isAbsent) module = sym.scalacLinkedClass module.info.member(name).symbol } @@ -1045,7 +1046,7 @@ class ClassfileParser( getClassSymbol(index) } - def getConstant(index: Int)(implicit ctx: Context): Constant = { + def getConstant(index: Int, tag: Int = -1)(implicit ctx: Context): Constant = { if (index <= 0 || len <= index) errorBadIndex(index) var value = values(index) if (value eq null) { @@ -1053,6 +1054,20 @@ class ClassfileParser( value = (in.buf(start).toInt: @switch) match { case CONSTANT_STRING => Constant(getName(in.getChar(start + 1).toInt).toString) + case CONSTANT_INTEGER if tag != -1 => + val value = in.getInt(start + 1) + (tag: @switch) match { + case BOOL_TAG => + Constant(value != 0) + case BYTE_TAG => + Constant(value.toByte) + case CHAR_TAG => + Constant(value.toChar) + case SHORT_TAG => + Constant(value.toShort) + case _ => + errorBadTag(tag) + } case CONSTANT_INTEGER => Constant(in.getInt(start + 1)) case CONSTANT_FLOAT => From 526b9c3705696b889550869921340546bea642ed Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Sat, 20 May 2017 21:09:16 +0200 Subject: [PATCH 15/21] Desugar: Mark label defs as Synthetic This way they won't show up in the IDE --- compiler/src/dotty/tools/dotc/ast/Desugar.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index ac5f0b53daa9..84825f5f96b9 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -778,7 +778,7 @@ object desugar { /** { label def lname(): Unit = rhs; call } */ def labelDefAndCall(lname: TermName, rhs: Tree, call: Tree) = { - val ldef = DefDef(lname, Nil, ListOfNil, TypeTree(defn.UnitType), rhs).withFlags(Label) + val ldef = DefDef(lname, Nil, ListOfNil, TypeTree(defn.UnitType), rhs).withFlags(Label | Synthetic) Block(ldef, call) } From 7611a5c831311e831dfeea4df6e0c7a8c963537d Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Wed, 24 May 2017 23:29:40 +0200 Subject: [PATCH 16/21] Add experimental APIs to query the compiler interactively This commit adds a set of APIs in dotty.tools.dotc.interactive._ designed for interactive uses of the compiler like in IDEs. This API is just a first draft driven by the requirements of the Dotty Language Server and will evolve based on usage. Usage is roughly as follow: - Create an instance of InteractiveDriver with the compiler flags you need - Call InteractiveDriver#run(uri, code) to typecheck `code` and keep a reference to it via the identifier `uri`. The return value of this method are the errors/warnings/infos generated during the compilation - Use InteractiveDriver#openedTrees(uri) to get all top-level class typechecked trees generated for `uri`, or InteractiveDriver#allTrees to get all top-level trees opened in this driver instance and available on the classpath (unpickled from the TASTY section of classfiles) - Query the trees using one of the many methods in the Interactive object. See the code in language-server/ added in the following commit for a concrete example. --- .../tools/dotc/interactive/Interactive.scala | 183 ++++++++++++++++ .../interactive/InteractiveCompiler.scala | 17 ++ .../dotc/interactive/InteractiveDriver.scala | 207 ++++++++++++++++++ .../tools/dotc/interactive/SourceTree.scala | 53 +++++ .../tools/dotc/util/SourcePosition.scala | 5 + 5 files changed, 465 insertions(+) create mode 100644 compiler/src/dotty/tools/dotc/interactive/Interactive.scala create mode 100644 compiler/src/dotty/tools/dotc/interactive/InteractiveCompiler.scala create mode 100644 compiler/src/dotty/tools/dotc/interactive/InteractiveDriver.scala create mode 100644 compiler/src/dotty/tools/dotc/interactive/SourceTree.scala diff --git a/compiler/src/dotty/tools/dotc/interactive/Interactive.scala b/compiler/src/dotty/tools/dotc/interactive/Interactive.scala new file mode 100644 index 000000000000..a2a0047bfb2d --- /dev/null +++ b/compiler/src/dotty/tools/dotc/interactive/Interactive.scala @@ -0,0 +1,183 @@ +package dotty.tools +package dotc +package interactive + +import scala.annotation.tailrec +import scala.collection._ + +import ast.{NavigateAST, Trees, tpd, untpd} +import core._, core.Decorators.{sourcePos => _, _} +import Contexts._, Flags._, Names._, NameOps._, Symbols._, SymDenotations._, Trees._, Types._ +import util.Positions._, util.SourcePosition +import NameKinds.SimpleNameKind + +/** High-level API to get information out of typed trees, designed to be used by IDEs. + * + * @see `InteractiveDriver` to get typed trees from code. + */ +object Interactive { + import ast.tpd._ + + /** Does this tree define a symbol ? */ + def isDefinition(tree: Tree) = + tree.isInstanceOf[DefTree with NameTree] + + /** The type of the closest enclosing tree with a type containing position `pos`. */ + def enclosingType(trees: List[SourceTree], pos: SourcePosition)(implicit ctx: Context): Type = { + val path = pathTo(trees, pos) + if (path.isEmpty) NoType + else path.head.tpe + } + + /** The source symbol of the closest enclosing tree with a symbol containing position `pos`. + * + * @see sourceSymbol + */ + def enclosingSourceSymbol(trees: List[SourceTree], pos: SourcePosition)(implicit ctx: Context): Symbol = { + pathTo(trees, pos).dropWhile(!_.symbol.exists).headOption match { + case Some(tree) => + sourceSymbol(tree.symbol) + case None => + NoSymbol + } + } + + /** A symbol related to `sym` that is defined in source code. + * + * @see enclosingSourceSymbol + */ + @tailrec def sourceSymbol(sym: Symbol)(implicit ctx: Context): Symbol = + if (!sym.exists) + sym + else if (sym.is(ModuleVal)) + sourceSymbol(sym.moduleClass) // The module val always has a zero-extent position + else if (sym.is(Synthetic)) { + val linked = sym.linkedClass + if (linked.exists && !linked.is(Synthetic)) + linked + else + sourceSymbol(sym.owner) + } + else if (sym.isPrimaryConstructor) + sourceSymbol(sym.owner) + else sym + + /** Possible completions at position `pos` */ + def completions(trees: List[SourceTree], pos: SourcePosition)(implicit ctx: Context): List[Symbol] = { + val path = pathTo(trees, pos) + val boundary = enclosingDefinitionInPath(path).symbol + + path.take(1).flatMap { + case Select(qual, _) => + // When completing "`a.foo`, return the members of `a` + completions(qual.tpe, boundary) + case _ => + // FIXME: Get all declarations available in the current scope, not just + // those from the enclosing class + boundary.enclosingClass match { + case csym: ClassSymbol => + val classRef = csym.classInfo.typeRef + completions(classRef, boundary) + case _ => + Nil + } + } + } + + /** Possible completions of members of `prefix` which are accessible when called inside `boundary` */ + def completions(prefix: Type, boundary: Symbol)(implicit ctx: Context): List[Symbol] = { + val boundaryCtx = ctx.withOwner(boundary) + prefix.memberDenots(completionsFilter, (name, buf) => + buf ++= prefix.member(name).altsWith(_.symbol.isAccessibleFrom(prefix)(boundaryCtx)) + ).map(_.symbol).toList + } + + /** Filter for names that should appear when looking for completions. */ + private[this] object completionsFilter extends NameFilter { + def apply(pre: Type, name: Name)(implicit ctx: Context): Boolean = + !name.isConstructorName && name.is(SimpleNameKind) + } + + /** Find named trees with a non-empty position whose symbol match `sym` in `trees`. + * + * Note that nothing will be found for symbols not defined in source code, + * use `sourceSymbol` to get a symbol related to `sym` that is defined in + * source code. + * + * @param includeReferences If true, include references and not just definitions + * @param includeOverriden If true, include trees whose symbol is overriden by `sym` + */ + def namedTrees(trees: List[SourceTree], includeReferences: Boolean, includeOverriden: Boolean, sym: Symbol) + (implicit ctx: Context): List[SourceTree] = + if (!sym.exists) + Nil + else + namedTrees(trees, includeReferences, matchSymbol(_, sym, includeOverriden)) + + /** Find named trees with a non-empty position whose name contains `nameSubstring` in `trees`. + * + * @param includeReferences If true, include references and not just definitions + */ + def namedTrees(trees: List[SourceTree], includeReferences: Boolean, nameSubstring: String) + (implicit ctx: Context): List[SourceTree] = + namedTrees(trees, includeReferences, _.show.toString.contains(nameSubstring)) + + /** Find named trees with a non-empty position satisfying `treePredicate` in `trees`. + * + * @param includeReferences If true, include references and not just definitions + */ + def namedTrees(trees: List[SourceTree], includeReferences: Boolean, treePredicate: NameTree => Boolean) + (implicit ctx: Context): List[SourceTree] = { + val buf = new mutable.ListBuffer[SourceTree] + + trees foreach { case SourceTree(topTree, source) => + (new TreeTraverser { + override def traverse(tree: Tree)(implicit ctx: Context) = { + tree match { + case _: Inlined => + // Skip inlined trees + case tree: NameTree + if tree.symbol.exists + && !tree.symbol.is(Synthetic) + && tree.pos.exists + && !tree.pos.isZeroExtent + && (includeReferences || isDefinition(tree)) + && treePredicate(tree) => + buf += SourceTree(tree, source) + case _ => + } + traverseChildren(tree) + } + }).traverse(topTree) + } + + buf.toList + } + + /** Check if `tree` matches `sym`. + * This is the case if `sym` is the symbol of `tree` or, if `includeOverriden` + * is true, if `sym` is overriden by `tree`. + */ + def matchSymbol(tree: Tree, sym: Symbol, includeOverriden: Boolean)(implicit ctx: Context): Boolean = + (sym == tree.symbol) || (includeOverriden && tree.symbol.allOverriddenSymbols.contains(sym)) + + + /** The reverse path to the node that closest encloses position `pos`, + * or `Nil` if no such path exists. If a non-empty path is returned it starts with + * the tree closest enclosing `pos` and ends with an element of `trees`. + */ + def pathTo(trees: List[SourceTree], pos: SourcePosition)(implicit ctx: Context): List[Tree] = + trees.find(_.pos.contains(pos)) match { + case Some(tree) => + // FIXME: We shouldn't need a cast. Change NavigateAST.pathTo to return a List of Tree? + val path = NavigateAST.pathTo(pos.pos, tree.tree, skipZeroExtent = true).asInstanceOf[List[untpd.Tree]] + + path.dropWhile(!_.hasType).asInstanceOf[List[tpd.Tree]] + case None => + Nil + } + + /** The first tree in the path that is a definition. */ + def enclosingDefinitionInPath(path: List[Tree])(implicit ctx: Context): Tree = + path.find(_.isInstanceOf[DefTree]).getOrElse(EmptyTree) +} diff --git a/compiler/src/dotty/tools/dotc/interactive/InteractiveCompiler.scala b/compiler/src/dotty/tools/dotc/interactive/InteractiveCompiler.scala new file mode 100644 index 000000000000..5f049aadb970 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/interactive/InteractiveCompiler.scala @@ -0,0 +1,17 @@ +package dotty.tools +package dotc +package interactive + +import core._ +import Phases._ +import typer._ + +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 FrontEnd) + ) +} diff --git a/compiler/src/dotty/tools/dotc/interactive/InteractiveDriver.scala b/compiler/src/dotty/tools/dotc/interactive/InteractiveDriver.scala new file mode 100644 index 000000000000..4fd24637a4b2 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/interactive/InteractiveDriver.scala @@ -0,0 +1,207 @@ +package dotty.tools +package dotc +package interactive + +import java.net.URI +import java.io._ +import java.nio.file._ +import java.nio.file.attribute.BasicFileAttributes +import java.util.stream._ +import java.util.zip._ +import java.util.function._ + +import scala.collection._ +import JavaConverters._ +import scala.io.Codec + +import dotty.tools.io.{ ClassPath, ClassRepresentation, PlainFile, VirtualFile } + +import ast.{Trees, tpd} +import core._, core.Decorators._ +import Contexts._, Flags._, Names._, NameOps._, Symbols._, SymDenotations._, Trees._, Types._ +import classpath._ +import reporting._, reporting.diagnostic.MessageContainer +import util._ + +/** A Driver subclass designed to be used from IDEs */ +class InteractiveDriver(settings: List[String]) extends Driver { + import tpd._ + import InteractiveDriver._ + + // FIXME: Change the Driver API to not require implementing this method + override protected def newCompiler(implicit ctx: Context): Compiler = ??? + override def sourcesRequired = false + + private val myInitCtx: Context = { + val rootCtx = initCtx.fresh.addMode(Mode.ReadPositions) + rootCtx.setSetting(rootCtx.settings.YretainTrees, true) + val ctx = setup(settings.toArray, rootCtx)._2 + ctx.initialize()(ctx) + ctx + } + + private var myCtx: Context = myInitCtx + + def currentCtx: Context = myCtx + + private val myOpenedFiles = new mutable.LinkedHashMap[URI, SourceFile] + private val myOpenedTrees = new mutable.LinkedHashMap[URI, List[SourceTree]] + + def openedFiles: Map[URI, SourceFile] = myOpenedFiles + def openedTrees: Map[URI, List[SourceTree]] = myOpenedTrees + + def allTrees(implicit ctx: Context): List[SourceTree] = { + val fromSource = openedTrees.values.flatten.toList + val fromClassPath = (dirClassPathClasses ++ zipClassPathClasses).flatMap { cls => + val className = cls.toTypeName + List(tree(className), tree(className.moduleClassName)).flatten + } + (fromSource ++ fromClassPath).distinct + } + + private def tree(className: TypeName)(implicit ctx: Context): Option[SourceTree] = { + val clsd = ctx.base.staticRef(className) + clsd match { + case clsd: ClassDenotation => + SourceTree.fromSymbol(clsd.symbol.asClass) + case _ => + sys.error(s"class not found: $className") + } + } + + private def classNames(cp: ClassPath, packageName: String): List[String] = { + def className(classSegments: List[String]) = + classSegments.mkString(".").stripSuffix(".class") + + val ClassPathEntries(pkgs, classReps) = cp.list(packageName) + + classReps + .filter((classRep: ClassRepresentation) => classRep.binary match { + case None => + true + case Some(binFile) => + val prefix = + if (binFile.name.endsWith(".class")) + binFile.name.stripSuffix(".class") + else + null + prefix != null && { + val tastyFile = prefix + ".tasty" + binFile match { + case pf: PlainFile => + val tastyPath = pf.givenPath.parent / tastyFile + tastyPath.exists + case _ => + sys.error(s"Unhandled file type: $binFile [getClass = ${binFile.getClass}]") + } + } + }) + .map(classRep => (packageName ++ (if (packageName != "") "." else "") ++ classRep.name)).toList ++ + pkgs.flatMap(pkg => classNames(cp, pkg.name)) + } + + // FIXME: All the code doing classpath handling is very fragile and ugly, + // improving this requires changing the dotty classpath APIs to handle our usecases. + // We also need something like sbt server-mode to be informed of changes on + // the classpath. + + private val (zipClassPaths, dirClassPaths) = currentCtx.platform.classPath(currentCtx) match { + case AggregateClassPath(cps) => + val (zipCps, dirCps) = cps.partition(_.isInstanceOf[ZipArchiveFileLookup[_]]) + // This will be wrong if any other subclass of ClassPath is either used, + // like `JrtClassPath` once we get Java 9 support + (zipCps.asInstanceOf[Seq[ZipArchiveFileLookup[_]]], dirCps.asInstanceOf[Seq[JFileDirectoryLookup[_]]]) + case _ => + (Seq(), Seq()) + } + + // Like in `ZipArchiveFileLookup` we assume that zips are immutable + private val zipClassPathClasses: Seq[String] = zipClassPaths.flatMap { zipCp => + // Working with Java 8 stream without SAMs and scala-java8-compat is awful. + val entries = new ZipFile(zipCp.zipFile) + .stream + .toArray(new IntFunction[Array[ZipEntry]] { def apply(size: Int) = new Array(size) }) + .toSeq + entries.filter(_.getName.endsWith(".tasty")) + .map(_.getName.replace("/", ".").stripSuffix(".tasty")) + } + + // FIXME: classfiles in directories may change at any point, so we retraverse + // the directories each time, if we knew when classfiles changed (sbt + // server-mode might help here), we could do cache invalidation instead. + private def dirClassPathClasses: Seq[String] = { + val names = new mutable.ListBuffer[String] + dirClassPaths.foreach { dirCp => + val root = dirCp.dir.toPath + Files.walkFileTree(root, new SimpleFileVisitor[Path] { + override def visitFile(path: Path, attrs: BasicFileAttributes) = { + if (!attrs.isDirectory && path.getFileName.toString.endsWith(".tasty")) { + names += root.relativize(path).toString.replace("/", ".").stripSuffix(".tasty") + } + FileVisitResult.CONTINUE + } + }) + } + names.toList + } + + private def topLevelClassTrees(topTree: Tree, source: SourceFile): List[SourceTree] = { + val trees = new mutable.ListBuffer[SourceTree] + + def addTrees(tree: Tree): Unit = tree match { + case PackageDef(_, stats) => + stats.foreach(addTrees) + case tree: TypeDef => + trees += SourceTree(tree, source) + case _ => + } + addTrees(topTree) + + trees.toList + } + + private val compiler: Compiler = new InteractiveCompiler + + def run(uri: URI, sourceCode: String): List[MessageContainer] = { + val previousCtx = myCtx + try { + val reporter = + new StoreReporter(null) with UniqueMessagePositions with HideNonSensicalMessages + + val run = compiler.newRun(myInitCtx.fresh.setReporter(reporter)) + myCtx = run.runContext + + implicit val ctx = myCtx + + val virtualFile = new VirtualFile(uri.toString, Paths.get(uri).toString) + val writer = new BufferedWriter(new OutputStreamWriter(virtualFile.output, "UTF-8")) + writer.write(sourceCode) + writer.close() + val source = new SourceFile(virtualFile, Codec.UTF8) + myOpenedFiles(uri) = source + + run.compileSources(List(source)) + run.printSummary() + val t = run.units.head.tpdTree + myOpenedTrees(uri) = topLevelClassTrees(t, source) + + reporter.removeBufferedMessages + } + catch { + case ex: FatalError => + myCtx = previousCtx + close(uri) + Nil + } + } + + def close(uri: URI): Unit = { + myOpenedFiles.remove(uri) + myOpenedTrees.remove(uri) + } +} + +object InteractiveDriver { + def toUri(source: SourceFile) = Paths.get(source.file.path).toUri +} + diff --git a/compiler/src/dotty/tools/dotc/interactive/SourceTree.scala b/compiler/src/dotty/tools/dotc/interactive/SourceTree.scala new file mode 100644 index 000000000000..8cdcc9729ff6 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/interactive/SourceTree.scala @@ -0,0 +1,53 @@ +package dotty.tools +package dotc +package interactive + +import scala.io.Codec + +import ast.tpd +import core._, core.Decorators.{sourcePos => _, _} +import Contexts._, NameOps._, Symbols._ +import util._, util.Positions._ + +/** A typechecked named `tree` coming from `source` */ +case class SourceTree(tree: tpd.NameTree, source: SourceFile) { + /** The position of `tree` */ + def pos(implicit ctx: Context): SourcePosition = source.atPos(tree.pos) + + /** The position of the name in `tree` */ + def namePos(implicit ctx: Context): SourcePosition = { + // FIXME: Merge with NameTree#namePos ? + val treePos = tree.pos + if (treePos.isZeroExtent) + NoSourcePosition + else { + val nameLength = tree.name.stripModuleClassSuffix.show.toString.length + val position = { + // FIXME: This is incorrect in some cases, like with backquoted identifiers, + // see https://github.com/lampepfl/dotty/pull/1634#issuecomment-257079436 + val (start, end) = + if (!treePos.isSynthetic) + (treePos.point, treePos.point + nameLength) + else + // If we don't have a point, we need to find it + (treePos.end - nameLength, treePos.end) + Position(start, end, start) + } + source.atPos(position) + } + } +} +object SourceTree { + def fromSymbol(sym: ClassSymbol)(implicit ctx: Context): Option[SourceTree] = { + if (sym == defn.SourceFileAnnot) None // FIXME: No SourceFile annotation on SourceFile itself + else { + sym.tree match { + case tree: tpd.TypeDef => + val sourceFile = new SourceFile(sym.sourceFile, Codec.UTF8) + Some(SourceTree(tree, sourceFile)) + case _ => + None + } + } + } +} diff --git a/compiler/src/dotty/tools/dotc/util/SourcePosition.scala b/compiler/src/dotty/tools/dotc/util/SourcePosition.scala index 85c2f42d53d8..af9ab537e11d 100644 --- a/compiler/src/dotty/tools/dotc/util/SourcePosition.scala +++ b/compiler/src/dotty/tools/dotc/util/SourcePosition.scala @@ -7,6 +7,11 @@ import Positions.{Position, NoPosition} /** A source position is comprised of a position in a source file */ case class SourcePosition(source: SourceFile, pos: Position, outer: SourcePosition = NoSourcePosition) extends interfaces.SourcePosition { + /** Is `that` a source position contained in this source position ? + * `outer` is not taken into account. */ + def contains(that: SourcePosition): Boolean = + this.source == that.source && this.pos.contains(that.pos) + def exists = pos.exists def lineContent: String = source.lineContent(point) From 252e9899cb042ba34aff6bf170d93d377fc4e1b2 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Wed, 24 May 2017 23:32:18 +0200 Subject: [PATCH 17/21] Add the Dotty Language Server This is an implementation of the Language Server Protocol for Dotty, which allows us to provide rich IDE features in every editor supporting this protocol, a Visual Studio Code extension demonstrating this is part of the next commit. For more information on the LSP, see https://github.com/Microsoft/language-server-protocol Fully supported features: - Typechecking as you type to show compiler errors/warnings - Type information on hover - Go to definition (in the current project) - Find all references Partially working features: - Completion - Renaming - Go to definition in external projects Unimplemented features: - Documentation on hover - Formatting code (requires integrating with scalafmt) - Quick fixes (probably by integrating with scalafix) Current limitations, to be fixed: - Projects should be compiled with sbt before starting the IDE, this is automatically done for you if you run `sbt launchIDE`. - Once the IDE is started, source files that are not opened in the IDE should not be modified in some other editor, the IDE won't pick up these changes. - Not all compiler errors/warnings are displayed, just those occuring during typechecking. --- build.sbt | 1 + .../languageserver/DottyLanguageServer.scala | 438 ++++++++++++++++++ .../src/dotty/tools/languageserver/Main.scala | 75 +++ .../languageserver/config/ProjectConfig.java | 30 ++ project/Build.scala | 18 + 5 files changed, 562 insertions(+) create mode 100644 language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala create mode 100644 language-server/src/dotty/tools/languageserver/Main.scala create mode 100644 language-server/src/dotty/tools/languageserver/config/ProjectConfig.java diff --git a/build.sbt b/build.sbt index 4a07c81babb6..275940f54039 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,7 @@ val `dotty-library-bootstrapped` = Build.`dotty-library-bootstrapped` val `dotty-sbt-bridge` = Build.`dotty-sbt-bridge` val `dotty-sbt-bridge-bootstrapped` = Build.`dotty-sbt-bridge-bootstrapped` val `dotty-sbt-scripted-tests` = Build.`dotty-sbt-scripted-tests` +val `dotty-language-server` = Build.`dotty-language-server` val sjsSandbox = Build.sjsSandbox val `dotty-bench` = Build.`dotty-bench` val `scala-library` = Build.`scala-library` diff --git a/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala new file mode 100644 index 000000000000..0fb8bdcdb0c9 --- /dev/null +++ b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala @@ -0,0 +1,438 @@ +package dotty.tools +package languageserver + +import java.net.URI +import java.io._ +import java.nio.file._ +import java.util.concurrent.CompletableFuture + +import com.fasterxml.jackson.databind.ObjectMapper + +import org.eclipse.lsp4j + +import scala.collection._ +import scala.collection.JavaConverters._ +import scala.util.control.NonFatal +import scala.io.Codec + +import dotc._ +import ast.{Trees, tpd} +import core._, core.Decorators.{sourcePos => _, _} +import Contexts._, Flags._, Names._, NameOps._, Symbols._, SymDenotations._, Trees._, Types._ +import classpath.ClassPathEntries +import reporting._, reporting.diagnostic.MessageContainer +import util._ +import interactive._, interactive.InteractiveDriver._ + +import config.ProjectConfig + +import lsp4j.services._ + +/** An implementation of the Language Server Protocol for Dotty. + * + * You should not have to directly this class, instead see `dotty.tools.languageserver.Main`. + * + * For more information see: + * - The LSP is defined at https://github.com/Microsoft/language-server-protocol + * - This implementation is based on the LSP4J library: https://github.com/eclipse/lsp4j + */ +class DottyLanguageServer extends LanguageServer + with LanguageClientAware with TextDocumentService with WorkspaceService { thisServer => + import ast.tpd._ + + import DottyLanguageServer._ + import InteractiveDriver._ + + import lsp4j.jsonrpc.{CancelChecker, CompletableFutures} + import lsp4j.jsonrpc.messages.{Either => JEither} + import lsp4j._ + + + private[this] var rootUri: String = _ + private[this] var client: LanguageClient = _ + + private[this] var myDrivers: mutable.Map[ProjectConfig, InteractiveDriver] = _ + + def drivers: Map[ProjectConfig, InteractiveDriver] = thisServer.synchronized { + if (myDrivers == null) { + assert(rootUri != null, "`drivers` cannot be called before `initialize`") + val configFile = new File(new URI(rootUri + '/' + IDE_CONFIG_FILE)) + val configs: List[ProjectConfig] = (new ObjectMapper).readValue(configFile, classOf[Array[ProjectConfig]]).toList + + val defaultFlags = List(/*"-Yplain-printer","-Yprintpos"*/) + + myDrivers = new mutable.HashMap + for (config <- configs) { + val classpathFlags = List("-classpath", (config.classDirectory +: config.dependencyClasspath).mkString(":")) + val settings = defaultFlags ++ config.compilerArguments.toList ++ classpathFlags + myDrivers.put(config, new InteractiveDriver(settings)) + } + } + myDrivers + } + + /** The driver instance responsible for compiling `uri` */ + def driverFor(uri: URI): InteractiveDriver = { + val matchingConfig = + drivers.keys.find(config => config.sourceDirectories.exists(sourceDir => + uri.getRawPath.startsWith(sourceDir.getAbsolutePath.toString))) + matchingConfig match { + case Some(config) => + drivers(config) + case None => + val config = drivers.keys.head + println(s"No configuration contains $uri as a source file, arbitrarily choosing ${config.id}") + drivers(config) + } + } + + override def connect(client: LanguageClient): Unit = { + this.client = client + } + + override def exit(): Unit = { + System.exit(0) + } + + override def shutdown(): CompletableFuture[Object] = { + CompletableFuture.completedFuture(new Object) + } + + private[this] def computeAsync[R](fun: CancelChecker => R): CompletableFuture[R] = + CompletableFutures.computeAsync({(cancelToken: CancelChecker) => + // We do not support any concurrent use of the compiler currently. + thisServer.synchronized { + cancelToken.checkCanceled() + try { + fun(cancelToken) + } catch { + case NonFatal(ex) => + ex.printStackTrace + throw ex + } + } + }) + + override def initialize(params: InitializeParams) = computeAsync { cancelToken => + rootUri = params.getRootUri + assert(rootUri != null) + + val c = new ServerCapabilities + c.setTextDocumentSync(TextDocumentSyncKind.Full) + c.setDocumentHighlightProvider(true) + c.setDocumentSymbolProvider(true) + c.setDefinitionProvider(true) + c.setRenameProvider(true) + c.setHoverProvider(true) + c.setWorkspaceSymbolProvider(true) + c.setReferencesProvider(true) + c.setCompletionProvider(new CompletionOptions( + /* resolveProvider = */ false, + /* triggerCharacters = */ List(".").asJava)) + + // Do most of the initialization asynchronously so that we can return early + // from this method and thus let the client know our capabilities. + CompletableFuture.supplyAsync(() => drivers) + + new InitializeResult(c) + } + + override def didOpen(params: DidOpenTextDocumentParams): Unit = thisServer.synchronized { + val document = params.getTextDocument + val uri = new URI(document.getUri) + val driver = driverFor(uri) + + val text = document.getText + val diags = driver.run(uri, text) + + client.publishDiagnostics(new PublishDiagnosticsParams( + document.getUri, + diags.flatMap(diagnostic).asJava)) + } + + override def didChange(params: DidChangeTextDocumentParams): Unit = thisServer.synchronized { + val document = params.getTextDocument + val uri = new URI(document.getUri) + val driver = driverFor(uri) + + val change = params.getContentChanges.get(0) + assert(change.getRange == null, "TextDocumentSyncKind.Incremental support is not implemented") + + val text = change.getText + val diags = driver.run(uri, text) + + client.publishDiagnostics(new PublishDiagnosticsParams( + document.getUri, + diags.flatMap(diagnostic).asJava)) + } + + override def didClose(params: DidCloseTextDocumentParams): Unit = thisServer.synchronized { + val document = params.getTextDocument + val uri = new URI(document.getUri) + + driverFor(uri).close(uri) + } + + override def didChangeConfiguration(params: DidChangeConfigurationParams): Unit = + /*thisServer.synchronized*/ {} + + override def didChangeWatchedFiles(params: DidChangeWatchedFilesParams): Unit = + /*thisServer.synchronized*/ {} + + override def didSave(params: DidSaveTextDocumentParams): Unit = + /*thisServer.synchronized*/ {} + + + // FIXME: share code with messages.NotAMember + override def completion(params: TextDocumentPositionParams) = computeAsync { cancelToken => + val uri = new URI(params.getTextDocument.getUri) + val driver = driverFor(uri) + implicit val ctx = driver.currentCtx + + val pos = sourcePosition(driver, uri, params.getPosition) + val items = Interactive.completions(driver.openedTrees(uri), pos) + + JEither.forRight(new CompletionList( + /*isIncomplete = */ false, items.map(completionItem).asJava)) + } + + override def definition(params: TextDocumentPositionParams) = computeAsync { cancelToken => + val uri = new URI(params.getTextDocument.getUri) + val driver = driverFor(uri) + implicit val ctx = driver.currentCtx + + val pos = sourcePosition(driver, uri, params.getPosition) + val sym = Interactive.enclosingSourceSymbol(driver.openedTrees(uri), pos) + + if (sym == NoSymbol) Nil.asJava + else { + // This returns the position of sym as well as the overrides of sym, but + // for performance we only look for overrides in the file where sym is + // defined. + // We need a configuration option to choose how "go to definition" should + // behave with respect to overriding and overriden definitions, ideally + // this should be part of the LSP protocol. + val trees = SourceTree.fromSymbol(sym.topLevelClass.asClass).toList + val defs = Interactive.namedTrees(trees, includeReferences = false, includeOverriden = true, sym) + defs.map(d => location(d.namePos)).asJava + } + } + + override def references(params: ReferenceParams) = computeAsync { cancelToken => + val uri = new URI(params.getTextDocument.getUri) + val driver = driverFor(uri) + implicit val ctx = driver.currentCtx + + val includeDeclaration = params.getContext.isIncludeDeclaration + val pos = sourcePosition(driver, uri, params.getPosition) + val sym = Interactive.enclosingSourceSymbol(driver.openedTrees(uri), pos) + + if (sym == NoSymbol) Nil.asJava + else { + // FIXME: this will search for references in all trees on the classpath, but we really + // only need to look for trees in the target directory if the symbol is defined in the + // current project + val trees = driver.allTrees + val refs = Interactive.namedTrees(trees, includeReferences = true, (tree: tpd.NameTree) => + (includeDeclaration || !Interactive.isDefinition(tree)) + && Interactive.matchSymbol(tree, sym, includeOverriden = true)) + + refs.map(ref => location(ref.namePos)).asJava + } + } + + override def rename(params: RenameParams) = computeAsync { cancelToken => + val uri = new URI(params.getTextDocument.getUri) + val driver = driverFor(uri) + implicit val ctx = driver.currentCtx + + val pos = sourcePosition(driver, uri, params.getPosition) + val sym = Interactive.enclosingSourceSymbol(driver.openedTrees(uri), pos) + + if (sym == NoSymbol) new WorkspaceEdit() + else { + val trees = driver.allTrees + val linkedSym = sym.linkedClass + val newName = params.getNewName + + val refs = Interactive.namedTrees(trees, includeReferences = true, tree => + (Interactive.matchSymbol(tree, sym, includeOverriden = true) + || (linkedSym != NoSymbol && Interactive.matchSymbol(tree, linkedSym, includeOverriden = true)))) + + val changes = refs.groupBy(ref => toUri(ref.source).toString).mapValues(_.map(ref => new TextEdit(range(ref.namePos), newName)).asJava) + + new WorkspaceEdit(changes.asJava) + } + } + + override def documentHighlight(params: TextDocumentPositionParams) = computeAsync { cancelToken => + val uri = new URI(params.getTextDocument.getUri) + val driver = driverFor(uri) + implicit val ctx = driver.currentCtx + + val pos = sourcePosition(driver, uri, params.getPosition) + val uriTrees = driver.openedTrees(uri) + val sym = Interactive.enclosingSourceSymbol(uriTrees, pos) + + if (sym == NoSymbol) Nil.asJava + else { + val refs = Interactive.namedTrees(uriTrees, includeReferences = true, includeOverriden = true, sym) + refs.map(ref => new DocumentHighlight(range(ref.namePos), DocumentHighlightKind.Read)).asJava + } + } + + override def hover(params: TextDocumentPositionParams) = computeAsync { cancelToken => + val uri = new URI(params.getTextDocument.getUri) + val driver = driverFor(uri) + implicit val ctx = driver.currentCtx + + val pos = sourcePosition(driver, uri, params.getPosition) + val tp = Interactive.enclosingType(driver.openedTrees(uri), pos) + val tpw = tp.widenTermRefExpr + + if (tpw == NoType) new Hover + else { + val str = tpw.show.toString + new Hover(List(JEither.forLeft(str)).asJava, null) + } + } + + override def documentSymbol(params: DocumentSymbolParams) = computeAsync { cancelToken => + val uri = new URI(params.getTextDocument.getUri) + val driver = driverFor(uri) + implicit val ctx = driver.currentCtx + + val uriTrees = driver.openedTrees(uri) + + val defs = Interactive.namedTrees(uriTrees, includeReferences = false, _ => true) + defs.map(d => symbolInfo(d.tree.symbol, d.namePos)).asJava + } + + override def symbol(params: WorkspaceSymbolParams) = computeAsync { cancelToken => + val query = params.getQuery + + drivers.values.toList.flatMap { driver => + implicit val ctx = driver.currentCtx + + val trees = driver.allTrees + val defs = Interactive.namedTrees(trees, includeReferences = false, nameSubstring = query) + defs.map(d => symbolInfo(d.tree.symbol, d.namePos)) + }.asJava + } + + override def getTextDocumentService: TextDocumentService = this + override def getWorkspaceService: WorkspaceService = this + + // Unimplemented features. If you implement one of them, you may need to add a + // capability in `initialize` + override def codeAction(params: CodeActionParams) = null + override def codeLens(params: CodeLensParams) = null + override def formatting(params: DocumentFormattingParams) = null + override def rangeFormatting(params: DocumentRangeFormattingParams) = null + override def onTypeFormatting(params: DocumentOnTypeFormattingParams) = null + override def resolveCodeLens(params: CodeLens) = null + override def resolveCompletionItem(params: CompletionItem) = null + override def signatureHelp(params: TextDocumentPositionParams) = null +} + +object DottyLanguageServer { + /** Configuration file normally generated by sbt-dotty */ + final val IDE_CONFIG_FILE = ".dotty-ide.json" + + /** Convert an lsp4j.Position to a SourcePosition */ + def sourcePosition(driver: InteractiveDriver, uri: URI, pos: lsp4j.Position): SourcePosition = { + val source = driver.openedFiles(uri) // might throw exception + val p = Positions.Position(source.lineToOffset(pos.getLine) + pos.getCharacter) + new SourcePosition(source, p) + } + + /** Convert a SourcePosition to an lsp4j.Range */ + def range(p: SourcePosition): lsp4j.Range = + new lsp4j.Range( + new lsp4j.Position(p.startLine, p.startColumn), + new lsp4j.Position(p.endLine, p.endColumn) + ) + + /** Convert a SourcePosition to an lsp4.Location */ + def location(p: SourcePosition): lsp4j.Location = + new lsp4j.Location(toUri(p.source).toString, range(p)) + + /** Convert a MessageContainer to an lsp4j.Diagnostic */ + def diagnostic(mc: MessageContainer): Option[lsp4j.Diagnostic] = + if (!mc.pos.exists) + None // diagnostics without positions are not supported: https://github.com/Microsoft/language-server-protocol/issues/249 + else { + def severity(level: Int): lsp4j.DiagnosticSeverity = { + import interfaces.{Diagnostic => D} + import lsp4j.{DiagnosticSeverity => DS} + + level match { + case D.INFO => + DS.Information + case D.WARNING => + DS.Warning + case D.ERROR => + DS.Error + } + } + + val code = mc.contained().errorId.errorNumber.toString + Some(new lsp4j.Diagnostic( + range(mc.pos), mc.message, severity(mc.level), /*source =*/ "", code)) + } + + /** Create an lsp4j.CompletionItem from a Symbol */ + def completionItem(sym: Symbol)(implicit ctx: Context): lsp4j.CompletionItem = { + def completionItemKind(sym: Symbol)(implicit ctx: Context): lsp4j.CompletionItemKind = { + import lsp4j.{CompletionItemKind => CIK} + + if (sym.is(Package)) + CIK.Module // No CompletionItemKind.Package (https://github.com/Microsoft/language-server-protocol/issues/155) + else if (sym.isConstructor) + CIK.Constructor + else if (sym.isClass) + CIK.Class + else if (sym.is(Mutable)) + CIK.Variable + else if (sym.is(Method)) + CIK.Method + else + CIK.Field + } + + val label = sym.name.show.toString + val item = new lsp4j.CompletionItem(label) + item.setDetail(sym.info.widenTermRefExpr.show.toString) + item.setKind(completionItemKind(sym)) + item + } + + /** Create an lsp4j.SymbolInfo from a Symbol and a SourcePosition */ + def symbolInfo(sym: Symbol, pos: SourcePosition)(implicit ctx: Context): lsp4j.SymbolInformation = { + def symbolKind(sym: Symbol)(implicit ctx: Context): lsp4j.SymbolKind = { + import lsp4j.{SymbolKind => SK} + + if (sym.is(Package)) + SK.Package + else if (sym.isConstructor) + SK.Constructor + else if (sym.isClass) + SK.Class + else if (sym.is(Mutable)) + SK.Variable + else if (sym.is(Method)) + SK.Method + else + SK.Field + } + + val name = sym.name.show.toString + val containerName = + if (sym.owner.exists && !sym.owner.isEmptyPackage) + sym.owner.name.show.toString + else + null + + new lsp4j.SymbolInformation(name, symbolKind(sym), location(pos), containerName) + } +} diff --git a/language-server/src/dotty/tools/languageserver/Main.scala b/language-server/src/dotty/tools/languageserver/Main.scala new file mode 100644 index 000000000000..fbdf9e4efdbb --- /dev/null +++ b/language-server/src/dotty/tools/languageserver/Main.scala @@ -0,0 +1,75 @@ +package dotty.tools +package languageserver + +import java.util.function.Consumer + +import java.io.{ File => JFile, InputStream, OutputStream, PrintWriter } +import java.net._ +import java.nio.channels._ + +import org.eclipse.lsp4j._ +import org.eclipse.lsp4j.services._ +import org.eclipse.lsp4j.launch._ + +/** Run the Dotty Language Server. + * + * This is designed to be started from an editor supporting the Language Server + * Protocol, the easiest way to fetch and run this is to use `coursier`: + * + * coursier launch $artifact -M dotty.tools.languageserver.Main -- -stdio + * + * Where $artifact comes from the `.dotty-ide-artifact` file in the current project, this file + * can be created by the sbt-dotty plugin by running `sbt configureIDE`. + * + * See vscode-dotty/ for an example integration of the Dotty Language Server into Visual Studio Code. + */ +object Main { + def main(args: Array[String]): Unit = { + args.toList match { + case List("-stdio") => + val serverIn = System.in + val serverOut = System.out + System.setOut(System.err) + scala.Console.withOut(scala.Console.err) { + startServer(serverIn, serverOut) + } + case "-client_command" :: clientCommand => + val serverSocket = new ServerSocket(0) + Runtime.getRuntime().addShutdownHook(new Thread( + new Runnable { + def run: Unit = { + serverSocket.close() + } + })); + + println("Starting client: " + clientCommand) + val clientPB = new java.lang.ProcessBuilder(clientCommand: _*) + clientPB.environment.put("DLS_DEV_MODE", "1") + + val pw = new PrintWriter("../.dotty-ide-dev-port") + pw.write(serverSocket.getLocalPort.toString) + pw.close() + + clientPB.inheritIO().start() + + val clientSocket = serverSocket.accept() + + startServer(clientSocket.getInputStream, clientSocket.getOutputStream) + case _ => + Console.err.println("Invalid arguments: expected \"-stdio\" or \"-client_command ...\"") + System.exit(1) + } + } + + def startServer(in: InputStream, out: OutputStream) = { + val server = new DottyLanguageServer + + println("Starting server") + // For debugging JSON messages: + // val launcher = LSPLauncher.createServerLauncher(server, in, out, false, new java.io.PrintWriter(System.err, true)) + val launcher = LSPLauncher.createServerLauncher(server, in, out) + val client = launcher.getRemoteProxy() + server.connect(client) + launcher.startListening() + } +} diff --git a/language-server/src/dotty/tools/languageserver/config/ProjectConfig.java b/language-server/src/dotty/tools/languageserver/config/ProjectConfig.java new file mode 100644 index 000000000000..7d8499b2d2ea --- /dev/null +++ b/language-server/src/dotty/tools/languageserver/config/ProjectConfig.java @@ -0,0 +1,30 @@ +package dotty.tools.languageserver.config; + +import java.io.File; + +import com.fasterxml.jackson.annotation.*; + +public class ProjectConfig { + public final String id; + public final String compilerVersion; + public final String[] compilerArguments; + public final File[] sourceDirectories; + public final File[] dependencyClasspath; + public final File classDirectory; + + @JsonCreator + public ProjectConfig( + @JsonProperty("id") String id, + @JsonProperty("compilerVersion") String compilerVersion, + @JsonProperty("compilerArguments") String[] compilerArguments, + @JsonProperty("sourceDirectories") File[] sourceDirectories, + @JsonProperty("dependencyClasspath") File[] dependencyClasspath, + @JsonProperty("classDirectory") File classDirectory) { + this.id = id; + this.compilerVersion = compilerVersion; + this.compilerArguments = compilerArguments; + this.sourceDirectories = sourceDirectories; + this.dependencyClasspath = dependencyClasspath; + this.classDirectory =classDirectory; + } +} diff --git a/project/Build.scala b/project/Build.scala index 68af3dae4cea..a32d2527cbb8 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -244,6 +244,7 @@ object Build { // Same as `dotty` but using bootstrapped projects. lazy val `dotty-bootstrapped` = project. aggregate(`dotty-interfaces`, `dotty-library-bootstrapped`, `dotty-compiler-bootstrapped`, `dotty-doc-bootstrapped`, + `dotty-language-server`, dottySbtBridgeBootstrappedRef, `scala-library`, `scala-compiler`, `scala-reflect`, scalap). dependsOn(`dotty-compiler-bootstrapped`). @@ -760,6 +761,23 @@ object DottyInjectedPlugin extends AutoPlugin { */ ) + lazy val `dotty-language-server` = project.in(file("language-server")). + dependsOn(`dotty-compiler-bootstrapped`). + settings(commonBootstrappedSettings). + settings( + // Sources representing the shared configuration file used to communicate between the sbt-dotty + // plugin and the language server + unmanagedSourceDirectories in Compile += baseDirectory.value / "../sbt-dotty/src/dotty/tools/sbtplugin/config", + + // fork so that the shutdown hook in Main is run when we ctrl+c a run + // (you need to have `cancelable in Global := true` in your global sbt config to ctrl+c a run) + fork in run := true, + libraryDependencies ++= Seq( + "org.eclipse.lsp4j" % "org.eclipse.lsp4j" % "0.2.0", + Dependencies.`jackson-databind` + ) + ) + /** A sandbox to play with the Scala.js back-end of dotty. * * This sandbox is compiled with dotty with support for Scala.js. It can be From 0faa6ca79257dafdf88b903f8051873c275575fa Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Thu, 25 May 2017 00:45:01 +0200 Subject: [PATCH 18/21] Visual Studio Code extension for Dotty This extension uses the Dotty Language Server to provide IDE features in Visual Studio Code. --- .gitignore | 3 + build.sbt | 1 + project/Build.scala | 121 ++++++++++++++++++++- vscode-dotty/.vscodeignore | 10 ++ vscode-dotty/README.md | 20 ++++ vscode-dotty/images/dotty-logo.svg | 142 +++++++++++++++++++++++++ vscode-dotty/package.json | 54 ++++++++++ vscode-dotty/src/extension.ts | 106 ++++++++++++++++++ vscode-dotty/src/passthrough-server.ts | 53 +++++++++ vscode-dotty/tsconfig.json | 16 +++ 10 files changed, 525 insertions(+), 1 deletion(-) create mode 100644 vscode-dotty/.vscodeignore create mode 100644 vscode-dotty/README.md create mode 100644 vscode-dotty/images/dotty-logo.svg create mode 100644 vscode-dotty/package.json create mode 100644 vscode-dotty/src/extension.ts create mode 100644 vscode-dotty/src/passthrough-server.ts create mode 100644 vscode-dotty/tsconfig.json diff --git a/.gitignore b/.gitignore index d7ada0d72bec..6ba72f4666fc 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ project/local-plugins.sbt .ensime .ensime_cache/ +# npm +node_modules + # Scala-IDE specific .scala_dependencies .cache diff --git a/build.sbt b/build.sbt index 275940f54039..38b5e4f50c8b 100644 --- a/build.sbt +++ b/build.sbt @@ -22,5 +22,6 @@ val scalap = Build.scalap val dist = Build.dist val `sbt-dotty` = Build.`sbt-dotty` +val `vscode-dotty` = Build.`vscode-dotty` inThisBuild(Build.thisBuildSettings) diff --git a/project/Build.scala b/project/Build.scala index a32d2527cbb8..59dcf0ae72da 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -85,6 +85,10 @@ object Build { bootstrapFromPublishedJars := false ) + // Only available in vscode-dotty + lazy val unpublish = taskKey[Unit]("Unpublish a package") + + lazy val commonSettings = publishSettings ++ Seq( organization := dottyOrganization, @@ -775,7 +779,20 @@ object DottyInjectedPlugin extends AutoPlugin { libraryDependencies ++= Seq( "org.eclipse.lsp4j" % "org.eclipse.lsp4j" % "0.2.0", Dependencies.`jackson-databind` - ) + ), + javaOptions := (javaOptions in `dotty-compiler-bootstrapped`).value, + + run := Def.inputTaskDyn { + val inputArgs = spaceDelimited("").parsed + + val mainClass = "dotty.tools.languageserver.Main" + val extensionPath = (baseDirectory in `vscode-dotty`).value.getAbsolutePath + + val codeArgs = if (inputArgs.isEmpty) List((baseDirectory.value / "..").getAbsolutePath) else inputArgs + val allArgs = List("-client_command", "code", s"--extensionDevelopmentPath=$extensionPath") ++ codeArgs + + runTask(Runtime, mainClass, allArgs: _*) + }.dependsOn(compile in (`vscode-dotty`, Compile)).evaluated ) /** A sandbox to play with the Scala.js back-end of dotty. @@ -909,6 +926,108 @@ object DottyInjectedPlugin extends AutoPlugin { }).evaluated ) + lazy val `vscode-dotty` = project.in(file("vscode-dotty")). + settings(commonSettings). + settings( + EclipseKeys.skipProject := true, + + version := "0.0.1", // Keep in sync with package.json + + autoScalaLibrary := false, + publishArtifact := false, + includeFilter in unmanagedSources := NothingFilter | "*.ts" | "**.json", + watchSources in Global ++= (unmanagedSources in Compile).value, + compile in Compile := { + val coursier = baseDirectory.value / "out/coursier" + val packageJson = baseDirectory.value / "package.json" + if (!coursier.exists || packageJson.lastModified > coursier.lastModified) { + val exitCode = new java.lang.ProcessBuilder("npm", "run", "update-all") + .directory(baseDirectory.value) + .inheritIO() + .start() + .waitFor() + if (exitCode != 0) + throw new FeedbackProvidedException { + override def toString = "'npm run update-all' in vscode-dotty failed" + } + } + val tsc = baseDirectory.value / "node_modules" / ".bin" / "tsc" + val exitCodeTsc = new java.lang.ProcessBuilder(tsc.getAbsolutePath, "--pretty", "--project", baseDirectory.value.getAbsolutePath) + .inheritIO() + .start() + .waitFor() + if (exitCodeTsc != 0) + throw new FeedbackProvidedException { + override def toString = "tsc in vscode-dotty failed" + } + + // Currently, vscode-dotty depends on daltonjorge.scala for syntax highlighting, + // this is not automatically installed when starting the extension in development mode + // (--extensionDevelopmentPath=...) + val exitCodeInstall = new java.lang.ProcessBuilder("code", "--install-extension", "daltonjorge.scala") + .inheritIO() + .start() + .waitFor() + if (exitCodeInstall != 0) + throw new FeedbackProvidedException { + override def toString = "Installing dependency daltonjorge.scala failed" + } + + sbt.inc.Analysis.Empty + }, + sbt.Keys.`package`:= { + val exitCode = new java.lang.ProcessBuilder("vsce", "package") + .directory(baseDirectory.value) + .inheritIO() + .start() + .waitFor() + if (exitCode != 0) + throw new FeedbackProvidedException { + override def toString = "vsce package failed" + } + + baseDirectory.value / s"dotty-${version.value}.vsix" + }, + unpublish := { + val exitCode = new java.lang.ProcessBuilder("vsce", "unpublish") + .directory(baseDirectory.value) + .inheritIO() + .start() + .waitFor() + if (exitCode != 0) + throw new FeedbackProvidedException { + override def toString = "vsce unpublish failed" + } + }, + publish := { + val exitCode = new java.lang.ProcessBuilder("vsce", "publish") + .directory(baseDirectory.value) + .inheritIO() + .start() + .waitFor() + if (exitCode != 0) + throw new FeedbackProvidedException { + override def toString = "vsce publish failed" + } + }, + run := Def.inputTask { + val inputArgs = spaceDelimited("").parsed + val codeArgs = if (inputArgs.isEmpty) List((baseDirectory.value / "..").getAbsolutePath) else inputArgs + val extensionPath = baseDirectory.value.getAbsolutePath + val processArgs = List("code", s"--extensionDevelopmentPath=${extensionPath}") ++ codeArgs + + val exitCode = new java.lang.ProcessBuilder(processArgs: _*) + .inheritIO() + .start() + .waitFor() + if (exitCode != 0) + throw new FeedbackProvidedException { + override def toString = "Running Visual Studio Code failed" + } + }.dependsOn(compile in Compile).evaluated + ) + + lazy val publishSettings = Seq( publishMavenStyle := true, isSnapshot := version.value.contains("SNAPSHOT"), diff --git a/vscode-dotty/.vscodeignore b/vscode-dotty/.vscodeignore new file mode 100644 index 000000000000..560bfc0d5a8e --- /dev/null +++ b/vscode-dotty/.vscodeignore @@ -0,0 +1,10 @@ +## This is used to exclude files from "vsce publish" +target/** +.vscode/** +.vscode-test/** +out/test/** +test/** +src/** +**/*.map +.gitignore +tsconfig.json diff --git a/vscode-dotty/README.md b/vscode-dotty/README.md new file mode 100644 index 000000000000..316a1e8ce82f --- /dev/null +++ b/vscode-dotty/README.md @@ -0,0 +1,20 @@ +# IDE support for Dotty, the experimental Scala compiler + +## Prerequisites +To use this in your own Scala project, you must first get it to compile with +Dotty, please follow the instructions at https://github.com/lampepfl/dotty-example-project + +## Starting Visual Studio Code from sbt +First, make sure `code`, the binary for Visual Studio Code, is on your `$PATH`, +this is the case if you can start the IDE by running `code` in a terminal. + +If this is the case and your project succesfully compiles with dotty, you can +simply use the `launchIDE` command provided by the sbt-dotty plugin: + +```shell +sbt launchIDE +``` + +## More information + +See http://dotty.epfl.ch/docs/usage/ide-support.html diff --git a/vscode-dotty/images/dotty-logo.svg b/vscode-dotty/images/dotty-logo.svg new file mode 100644 index 000000000000..a004372b1ee7 --- /dev/null +++ b/vscode-dotty/images/dotty-logo.svg @@ -0,0 +1,142 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vscode-dotty/package.json b/vscode-dotty/package.json new file mode 100644 index 000000000000..ccd7d289b2c7 --- /dev/null +++ b/vscode-dotty/package.json @@ -0,0 +1,54 @@ +{ + "name": "dotty", + "displayName": "Dotty Language Server", + "description": "IDE integration for Dotty, the experimental Scala compiler", + "version": "0.1.0", + "license": "BSD-3-Clause", + "publisher": "lampepfl", + "repository": { + "type": "git", + "url": "https://github.com/lampepfl/dotty.git" + }, + "icon": "images/dotty-logo.svg", + "engines": { + "vscode": "^1.12.0" + }, + "categories": [ + "Languages" + ], + "main": "./out/src/extension", + "activationEvents": [ + "onLanguage:scala" + ], + "languages": [ + { + "id": "scala", + "extensions": [ + ".scala" + ], + "aliases": [ + "Scala" + ] + } + ], + "scripts": { + "tsc": "./node_modules/.bin/tsc", + "vscode:prepublish": "npm run update-all && ./node_modules/.bin/tsc -p ./", + "compile": "./node_modules/.bin/tsc -p ./", + "update-all": "npm install && node ./node_modules/vscode/bin/install && mkdir -p out && curl -L -o out/coursier https://github.com/coursier/coursier/raw/v1.0.0-RC3/coursier", + "test": "node ./node_modules/vscode/bin/test" + }, + "extensionDependencies": [ + "daltonjorge.scala" + ], + "dependencies": { + "child-process-promise": "^2.2.1", + "vscode-languageclient": "^3.2.1", + "vscode-languageserver": "^3.2.1" + }, + "devDependencies": { + "typescript": "^2.3.2", + "vscode": "^1.1.0", + "@types/node": "^7.0.12" + } +} diff --git a/vscode-dotty/src/extension.ts b/vscode-dotty/src/extension.ts new file mode 100644 index 000000000000..994441ddfa25 --- /dev/null +++ b/vscode-dotty/src/extension.ts @@ -0,0 +1,106 @@ +'use strict'; + +import * as fs from 'fs'; +import * as path from 'path'; +import { spawn } from 'child_process'; + +import * as cpp from 'child-process-promise'; + +import { commands, workspace, Disposable, ExtensionContext, Uri } from 'vscode'; +import { Executable, LanguageClient, LanguageClientOptions, SettingMonitor, ServerOptions, TransportKind } from 'vscode-languageclient'; +import * as lc from 'vscode-languageclient'; +import * as vscode from 'vscode'; + +let extensionContext: ExtensionContext +let outputChannel: vscode.OutputChannel + +export function activate(context: ExtensionContext) { + extensionContext = context + outputChannel = vscode.window.createOutputChannel('Dotty Language Client'); + + const artifactFile = `${vscode.workspace.rootPath}/.dotty-ide-artifact` + fs.readFile(artifactFile, (err, data) => { + if (err) { + outputChannel.append(`Unable to parse ${artifactFile}`) + throw err + } + const artifact = data.toString().trim() + + if (process.env['DLS_DEV_MODE']) { + const portFile = `${vscode.workspace.rootPath}/.dotty-ide-dev-port` + fs.readFile(portFile, (err, port) => { + if (err) { + outputChannel.append(`Unable to parse ${portFile}`) + throw err + } + + run({ + module: context.asAbsolutePath('out/src/passthrough-server.js'), + args: [ port.toString() ] + }) + }) + } else { + fetchAndRun(artifact) + } + }) +} + +function fetchAndRun(artifact: String) { + const coursierPath = path.join(extensionContext.extensionPath, './out/coursier'); + + vscode.window.withProgress({ + location: vscode.ProgressLocation.Window, + title: 'Fetching the Dotty Language Server' + }, (progress) => { + + const coursierPromise = + cpp.spawn("java", [ + "-jar", coursierPath, + "fetch", + "-p", + artifact + ]) + const coursierProc = coursierPromise.childProcess + + let classPath = "" + + coursierProc.stdout.on('data', (data) => { + classPath += data.toString().trim() + }) + coursierProc.stderr.on('data', (data) => { + let msg = data.toString() + outputChannel.append(msg) + }) + + coursierProc.on('close', (code) => { + if (code != 0) { + let msg = "Fetching the language server failed." + outputChannel.append(msg) + throw new Error(msg) + } + + run({ + command: "java", + args: ["-classpath", classPath, "dotty.tools.languageserver.Main", "-stdio"] + }) + }) + return coursierPromise + }) +} + +function run(serverOptions: ServerOptions) { + const clientOptions: LanguageClientOptions = { + documentSelector: ['scala'], + synchronize: { + configurationSection: 'dotty' + } + } + + outputChannel.dispose() + + const client = new LanguageClient('dotty', 'Dotty Language Server', serverOptions, clientOptions); + + // Push the disposable to the context's subscriptions so that the + // client can be deactivated on extension deactivation + extensionContext.subscriptions.push(client.start()); +} diff --git a/vscode-dotty/src/passthrough-server.ts b/vscode-dotty/src/passthrough-server.ts new file mode 100644 index 000000000000..94a22392fc56 --- /dev/null +++ b/vscode-dotty/src/passthrough-server.ts @@ -0,0 +1,53 @@ +'use strict'; + +import { + IPCMessageReader, IPCMessageWriter, + createConnection, IConnection, TextDocumentSyncKind, + TextDocuments, TextDocument, Diagnostic, DiagnosticSeverity, + InitializeParams, InitializeResult, TextDocumentPositionParams, + CompletionItem, CompletionItemKind +} from 'vscode-languageserver'; + +import * as net from 'net'; + +let argv = process.argv.slice(2) +let port = argv.shift() + +let client = new net.Socket() +client.setEncoding('utf8') +process.stdout.setEncoding('utf8') +process.stdin.setEncoding('utf8') + +let isConnected = false + +client.on('data', (data) => { + process.stdout.write(data.toString()) +}) +process.stdin.on('readable', () => { + let chunk = process.stdin.read(); + if (chunk !== null) { + if (isConnected) { + client.write(chunk) + } else { + client.on('connect', () => { + client.write(chunk) + }) + } + } +}) + +client.on('error', (err) => { + if (!isConnected) { + startConnection() + } +}) + +function startConnection() { + setTimeout(() => { + client.connect(port, () => { + isConnected = true + }) + }, 1000) +} + +startConnection() diff --git a/vscode-dotty/tsconfig.json b/vscode-dotty/tsconfig.json new file mode 100644 index 000000000000..0b14cc1dbd45 --- /dev/null +++ b/vscode-dotty/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "out", + "lib": [ + "es6" + ], + "sourceMap": true, + "rootDir": "." + }, + "exclude": [ + "node_modules", + ".vscode-test" + ] +} From cce3ae576cca9f26a10f56ee59a92e07e43f66d0 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Thu, 25 May 2017 00:26:40 +0200 Subject: [PATCH 19/21] Inject the sources of the sbt-dotty plugin in the dotty build This means the dotty build can use the latest unreleased version of the sbt-dotty plugin instead of depending on a published version, in the next commit we'll add IDE support to the sbt-dotty plugin and immediately be able to use them on the dotty build itself. --- project/Build.scala | 13 ++++++------- project/inject-sbt-dotty.sbt | 5 +++++ 2 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 project/inject-sbt-dotty.sbt diff --git a/project/Build.scala b/project/Build.scala index 59dcf0ae72da..14ce1c4d21b6 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -15,6 +15,7 @@ import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ import sbt.Package.ManifestAttributes import com.typesafe.sbteclipse.plugin.EclipsePlugin._ +import dotty.tools.sbtplugin.DottyPlugin.autoImport._ /* In sbt 0.13 the Build trait would expose all vals to the shell, where you * can use them in "set a := b" like expressions. This re-exposes them. @@ -132,8 +133,6 @@ object Build { EclipseKeys.skipProject := true, version := dottyVersion, scalaVersion := dottyNonBootstrappedVersion, - scalaOrganization := dottyOrganization, - scalaBinaryVersion := "0.1", // Avoid having to run `dotty-sbt-bridge/publishLocal` before compiling a bootstrapped project scalaCompilerBridgeSource := @@ -174,9 +173,9 @@ object Build { libraryDependencies ++= { if (bootstrapFromPublishedJars.value) Seq( - dottyOrganization % "dotty-library_2.11" % dottyNonBootstrappedVersion % Configurations.ScalaTool.name, - dottyOrganization % "dotty-compiler_2.11" % dottyNonBootstrappedVersion % Configurations.ScalaTool.name - ) + dottyOrganization %% "dotty-library" % dottyNonBootstrappedVersion % Configurations.ScalaTool.name, + dottyOrganization %% "dotty-compiler" % dottyNonBootstrappedVersion % Configurations.ScalaTool.name + ).map(_.withDottyCompat()) else Seq() }, @@ -439,7 +438,7 @@ object Build { // get libraries onboard libraryDependencies ++= Seq("com.typesafe.sbt" % "sbt-interface" % sbtVersion.value, - "org.scala-lang.modules" % "scala-xml_2.11" % "1.0.1", + ("org.scala-lang.modules" %% "scala-xml" % "1.0.1").withDottyCompat(), "com.novocode" % "junit-interface" % "0.11" % "test", "org.scala-lang" % "scala-library" % scalacVersion % "test"), @@ -695,7 +694,7 @@ object Build { libraryDependencies ++= Seq( "com.typesafe.sbt" % "sbt-interface" % sbtVersion.value, "org.scala-sbt" % "api" % sbtVersion.value % "test", - "org.specs2" % "specs2_2.11" % "2.3.11" % "test" + ("org.specs2" %% "specs2" % "2.3.11" % "test").withDottyCompat() ), // The sources should be published with crossPaths := false since they // need to be compiled by the project using the bridge. diff --git a/project/inject-sbt-dotty.sbt b/project/inject-sbt-dotty.sbt new file mode 100644 index 000000000000..2f47e2151a45 --- /dev/null +++ b/project/inject-sbt-dotty.sbt @@ -0,0 +1,5 @@ +// Include the sources of the sbt-dotty plugin in the project build, +// so that we can use the current in-development version of the plugin +// in our build instead of a released version. + +unmanagedSourceDirectories in Compile += baseDirectory.value / "../sbt-dotty/src" From 5d39d2495108078e282f41e956b2881655e523f8 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Wed, 24 May 2017 23:46:31 +0200 Subject: [PATCH 20/21] sbt-dotty: Add IDE integration This adds commands to the sbt-dotty plugin to generate the .dotty-ide.json config file used by the Dotty Language Server and to start Visual Studio Code with everything set up correctly. For end-users, using the IDE is as simple as: 1. Installing vscode (https://code.visualstudio.com) 2. In your project, run `sbt launchIDE` --- .gitignore | 5 + project/Build.scala | 25 ++- project/Dependencies.scala | 12 ++ project/inject-sbt-dotty.sbt | 5 + project/project/build.sbt | 2 + .../tools/sbtplugin/DottyIDEPlugin.scala | 145 ++++++++++++++++++ 6 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 project/Dependencies.scala create mode 100644 project/project/build.sbt create mode 100644 sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala diff --git a/.gitignore b/.gitignore index 6ba72f4666fc..523fbee8b25f 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,11 @@ node_modules classes/ */bin/ +# Dotty IDE +/.dotty-ide-dev-port +/.dotty-ide-artifact +/.dotty-ide.json + # idea .idea .idea_modules diff --git a/project/Build.scala b/project/Build.scala index 14ce1c4d21b6..7f0c059b9b84 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -10,12 +10,14 @@ import scala.reflect.io.Path import sbtassembly.AssemblyKeys.assembly import xerial.sbt.Pack._ -import org.scalajs.sbtplugin.ScalaJSPlugin -import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ import sbt.Package.ManifestAttributes import com.typesafe.sbteclipse.plugin.EclipsePlugin._ + import dotty.tools.sbtplugin.DottyPlugin.autoImport._ +import dotty.tools.sbtplugin.DottyIDEPlugin.autoImport._ +import org.scalajs.sbtplugin.ScalaJSPlugin +import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ /* In sbt 0.13 the Build trait would expose all vals to the shell, where you * can use them in "set a := b" like expressions. This re-exposes them. @@ -83,7 +85,13 @@ object Build { // Used in build.sbt lazy val thisBuildSettings = Def.settings( // Change this to true if you want to bootstrap using a published non-bootstrapped compiler - bootstrapFromPublishedJars := false + bootstrapFromPublishedJars := false, + + + // Override `launchIDE` from sbt-dotty to use the language-server and + // vscode extension from the source repository of dotty instead of a + // published version. + launchIDE := (run in `dotty-language-server`).dependsOn(prepareIDE).toTask("").value ) // Only available in vscode-dotty @@ -240,6 +248,7 @@ object Build { settings( triggeredMessage in ThisBuild := Watched.clearWhenTriggered, submoduleChecks, + addCommandAlias("run", "dotty-compiler/run") ++ addCommandAlias("legacyTests", "dotty-compiler/testOnly dotc.tests") ) @@ -318,7 +327,7 @@ object Build { "com.vladsch.flexmark" % "flexmark-ext-emoji" % "0.11.1", "com.vladsch.flexmark" % "flexmark-ext-gfm-strikethrough" % "0.11.1", "com.vladsch.flexmark" % "flexmark-ext-yaml-front-matter" % "0.11.1", - "com.fasterxml.jackson.dataformat" % "jackson-dataformat-yaml" % "2.8.6", + Dependencies.`jackson-dataformat-yaml`, "nl.big-o" % "liqp" % "0.6.7" ) ) @@ -907,8 +916,14 @@ object DottyInjectedPlugin extends AutoPlugin { lazy val `sbt-dotty` = project.in(file("sbt-dotty")). settings(commonSettings). settings( + // Keep in sync with inject-sbt-dotty.sbt + libraryDependencies += Dependencies.`jackson-databind`, + unmanagedSourceDirectories in Compile += + baseDirectory.value / "../language-server/src/dotty/tools/languageserver/config", + + sbtPlugin := true, - version := "0.1.0-RC4", + version := "0.1.0-RC5", ScriptedPlugin.scriptedSettings, ScriptedPlugin.sbtTestDirectory := baseDirectory.value / "sbt-test", ScriptedPlugin.scriptedBufferLog := false, diff --git a/project/Dependencies.scala b/project/Dependencies.scala new file mode 100644 index 000000000000..b7c752ed4604 --- /dev/null +++ b/project/Dependencies.scala @@ -0,0 +1,12 @@ +import sbt._ + +/** A dependency shared between multiple projects should be put here + * to ensure the same version of the dependency is used in all projects + */ +object Dependencies { + private val jacksonVersion = "2.8.8" + val `jackson-databind` = + "com.fasterxml.jackson.core" % "jackson-databind" % jacksonVersion + val `jackson-dataformat-yaml` = + "com.fasterxml.jackson.dataformat" % "jackson-dataformat-yaml" % jacksonVersion +} diff --git a/project/inject-sbt-dotty.sbt b/project/inject-sbt-dotty.sbt index 2f47e2151a45..5ccc77fc2783 100644 --- a/project/inject-sbt-dotty.sbt +++ b/project/inject-sbt-dotty.sbt @@ -3,3 +3,8 @@ // in our build instead of a released version. unmanagedSourceDirectories in Compile += baseDirectory.value / "../sbt-dotty/src" + +// Keep in sync with `sbt-dotty` config in Build.scala +libraryDependencies += Dependencies.`jackson-databind` +unmanagedSourceDirectories in Compile += + baseDirectory.value / "../language-server/src/dotty/tools/languageserver/config" diff --git a/project/project/build.sbt b/project/project/build.sbt new file mode 100644 index 000000000000..41880f2a21f0 --- /dev/null +++ b/project/project/build.sbt @@ -0,0 +1,2 @@ +// Some dependencies are shared between the regular build and the meta-build +sources in Compile += baseDirectory.value / "../Dependencies.scala" diff --git a/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala b/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala new file mode 100644 index 000000000000..bb2a1227f97b --- /dev/null +++ b/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala @@ -0,0 +1,145 @@ +package dotty.tools.sbtplugin + +import sbt._ +import sbt.Keys._ +import java.io._ +import java.lang.ProcessBuilder +import scala.collection.mutable + +import dotty.tools.languageserver.config.ProjectConfig + +import com.fasterxml.jackson.databind.ObjectMapper +import scala.collection.mutable.ListBuffer +import DottyPlugin.autoImport._ + +object DottyIDEPlugin extends AutoPlugin { + // Adapted from scala-reflect + private[this] def distinctBy[A, B](xs: Seq[A])(f: A => B): Seq[A] = { + val buf = new mutable.ListBuffer[A] + val seen = mutable.Set[B]() + xs foreach { x => + val y = f(x) + if (!seen(y)) { + buf += x + seen += y + } + } + buf.toList + } + + private def inAllDottyConfigurations[A](key: TaskKey[A], state: State): Task[Seq[A]] = { + val struct = Project.structure(state) + val settings = struct.data + struct.allProjectRefs.flatMap { projRef => + val project = Project.getProjectForReference(projRef, struct).get + project.configurations.flatMap { config => + isDotty.in(projRef, config).get(settings) match { + case Some(true) => + key.in(projRef, config).get(settings) + case _ => + None + } + } + }.join + } + + private val projectConfig = taskKey[Option[ProjectConfig]]("") + private val configureIDE = taskKey[Unit]("Generate IDE config files") + private val compileForIDE = taskKey[Unit]("Compile all projects supported by the IDE") + private val runCode = taskKey[Unit]("") + + object autoImport { + val prepareIDE = taskKey[Unit]("Prepare for IDE launch") + val launchIDE = taskKey[Unit]("Run Visual Studio Code on this project") + } + + import autoImport._ + + override def requires: Plugins = plugins.JvmPlugin + override def trigger = allRequirements + + override def projectSettings: Seq[Setting[_]] = Seq( + // Use Def.derive so `projectConfig` is only defined in the configurations where the + // tasks/settings it depends on are defined. + Def.derive(projectConfig := { + if (sources.value.isEmpty) None + else { + val id = s"${thisProject.value.id}/${configuration.value.name}" + val compilerVersion = scalaVersion.value + .replace("-nonbootstrapped", "") // The language server is only published bootstrapped + val compilerArguments = scalacOptions.value + val sourceDirectories = unmanagedSourceDirectories.value ++ managedSourceDirectories.value + val depClasspath = Attributed.data(dependencyClasspath.value) + val classDir = classDirectory.value + + Some(new ProjectConfig( + id, + compilerVersion, + compilerArguments.toArray, + sourceDirectories.toArray, + depClasspath.toArray, + classDir + )) + } + }) + ) + + override def buildSettings: Seq[Setting[_]] = Seq( + configureIDE := { + val log = streams.value.log + + val configs0 = state.flatMap(s => + inAllDottyConfigurations(projectConfig, s) + ).value.flatten + // Drop configurations who do not define their own sources, but just + // inherit their sources from some other configuration. + val configs = distinctBy(configs0)(_.sourceDirectories.deep) + + if (configs.isEmpty) { + log.error("No Dotty project detected") + } else { + // If different versions of Dotty are used by subprojects, choose the latest one + // FIXME: use a proper version number Ordering that knows that "0.1.1-M1" < "0.1.1" + val ideVersion = configs.map(_.compilerVersion).sorted.last + // Write the version of the Dotty Language Server to use in a file by itself. + // This could be a field in the JSON config file, but that would require all + // IDE plugins to parse JSON. + val pwArtifact = new PrintWriter(".dotty-ide-artifact") + pwArtifact.println(s"ch.epfl.lamp:dotty-language-server_0.1:${ideVersion}") + pwArtifact.close() + + val mapper = new ObjectMapper + mapper.writerWithDefaultPrettyPrinter() + .writeValue(new File(".dotty-ide.json"), configs.toArray) + } + }, + + compileForIDE := { + val _ = state.flatMap(s => + inAllDottyConfigurations(compile, s) + ).value + }, + + runCode := { + val exitCode = new ProcessBuilder("code", "--install-extension", "lampepfl.dotty") + .inheritIO() + .start() + .waitFor() + if (exitCode != 0) + throw new FeedbackProvidedException { + override def toString = "Installing the Dotty support for VSCode failed" + } + + new ProcessBuilder("code", baseDirectory.value.getAbsolutePath) + .inheritIO() + .start() + }, + + prepareIDE := { + val x1 = configureIDE.value + val x2 = compileForIDE.value + }, + + launchIDE := runCode.dependsOn(prepareIDE).value + ) +} From dcdbd8e5e312443c53b751dc489255a8a35b657c Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Thu, 25 May 2017 17:44:32 +0200 Subject: [PATCH 21/21] Add documentation for the IDE support --- docs/docs/index.md | 3 +- docs/docs/usage/ide-support.md | 60 ++++++++++++++++++++++++++++++++++ docs/sidebar.yml | 6 ++-- 3 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 docs/docs/usage/ide-support.md diff --git a/docs/docs/index.md b/docs/docs/index.md index d4661a497503..18ee114344a4 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -16,8 +16,9 @@ Contents * Usage - [Migrating from Scala 2](usage/migrating.md): migration information - - [Dotty projects with cbt](usage/cbt-projects.md): using cbt - [Dotty projects with sbt](usage/sbt-projects.md): using sbt + - [IDE support for Dotty](usage/ide-support.md) + - [Dotty projects with cbt](usage/cbt-projects.md): using cbt * Contributing - [Getting Started](contributing/getting-started.md): details on how to run tests, use the cli scripts - [Workflow](contributing/workflow.md): common dev patterns and hints diff --git a/docs/docs/usage/ide-support.md b/docs/docs/usage/ide-support.md new file mode 100644 index 000000000000..26818fea42cc --- /dev/null +++ b/docs/docs/usage/ide-support.md @@ -0,0 +1,60 @@ +--- +layout: doc-page +title: "IDE support for Dotty" +--- + +Dotty comes built-in with the Dotty Language Server, an implementation of the +[Language Server Protocol](https://github.com/Microsoft/language-server-protocol), +which means that any editor that implements the LSP can be used as a Dotty IDE. +Currently, the only IDE we officially support is +[Visual Studio Code](https://code.visualstudio.com/). + +Prerequisites +============ +To use this in your own Scala project, you must first get it to compile with +Dotty, please follow the instructions at https://github.com/lampepfl/dotty-example-project + +Usage +===== +1. Install [Visual Studio Code](https://code.visualstudio.com/). +2. Make sure `code`, the binary for Visual Studio Code, is on your `$PATH`, this + is the case if you can start the IDE by running `code` in a terminal. +3. In your project, run: +```shell +sbt launchIDE +``` + +Status +====== + +## Fully supported features: +- Typechecking as you type to show compiler errors/warnings +- Type information on hover +- Go to definition (in the current project) +- Find all references + +## Partially working features: +- Completion +- Renaming +- Go to definition in external projects + +## Unimplemented features: +- Documentation on hover +- Formatting code (requires integrating with scalafmt) +- Quick fixes (probably by integrating with scalafix) + +## Current limitations, to be fixed: +- Projects should be compiled with sbt before starting the IDE, this is + automatically done for you if you run `sbt launchIDE`. +- Once the IDE is started, source files that are not opened in the IDE + should not be modified in some other editor, the IDE won't pick up + these changes. +- Not all compiler errors/warnings are displayed, just those occuring + during typechecking. + + +Feedback +======== +Please report issues on https://github.com/lampepfl/dotty/issues, +you can also come chat with use on the +[Dotty gitter channel](https://gitter.im/lampepfl/dotty)! diff --git a/docs/sidebar.yml b/docs/sidebar.yml index 90e950134636..e37a2d4d3aa7 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -3,10 +3,12 @@ sidebar: url: blog/index.html - title: Usage subsection: - - title: cbt-projects - url: docs/usage/cbt-projects.html - title: sbt-projects url: docs/usage/sbt-projects.html + - title: IDE support for Dotty + url: docs/usage/ide-support.html + - title: cbt-projects + url: docs/usage/cbt-projects.html - title: Dottydoc url: docs/usage/dottydoc.html - title: Migrating