diff --git a/.gitignore b/.gitignore index d7ada0d72bec..523fbee8b25f 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 @@ -28,6 +31,11 @@ project/local-plugins.sbt classes/ */bin/ +# Dotty IDE +/.dotty-ide-dev-port +/.dotty-ide-artifact +/.dotty-ide.json + # idea .idea .idea_modules diff --git a/build.sbt b/build.sbt index 4a07c81babb6..38b5e4f50c8b 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` @@ -21,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/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() } } 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/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 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/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index cb137d46476a..84825f5f96b9 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) @@ -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) } diff --git a/compiler/src/dotty/tools/dotc/ast/NavigateAST.scala b/compiler/src/dotty/tools/dotc/ast/NavigateAST.scala index 33aa87d8e8e5..b6ff80277d35 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 { @@ -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,8 +80,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.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. + p match { + case p: WithLazyField[_] => + p.forceIfLazy + case _ => + } + childPath(p.productIterator, p :: path) + } else path singlePath(from, Nil) } -} \ No newline at end of file +} 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) { 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/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 } 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/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 diff --git a/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala b/compiler/src/dotty/tools/dotc/core/classfile/ClassfileParser.scala index c23ff6d2affe..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)))) @@ -827,7 +828,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.exists && sym.isAbsent) + module = sym.scalacLinkedClass + module.info.member(name).symbol + } else if (sym == classRoot.symbol) instanceScope.lookup(name) else sym.info.member(name).symbol @@ -1040,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) { @@ -1048,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 => 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/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/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) } 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), diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 644bcd7cbe0a..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 } @@ -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) @@ -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 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. */ 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) 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 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..7f0c059b9b84 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -10,12 +10,15 @@ 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. */ @@ -82,9 +85,19 @@ 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 + lazy val unpublish = taskKey[Unit]("Unpublish a package") + + lazy val commonSettings = publishSettings ++ Seq( organization := dottyOrganization, @@ -128,8 +141,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 := @@ -170,9 +181,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() }, @@ -237,6 +248,7 @@ object Build { settings( triggeredMessage in ThisBuild := Watched.clearWhenTriggered, submoduleChecks, + addCommandAlias("run", "dotty-compiler/run") ++ addCommandAlias("legacyTests", "dotty-compiler/testOnly dotc.tests") ) @@ -244,6 +256,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`). @@ -314,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" ) ) @@ -434,7 +447,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"), @@ -690,7 +703,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. @@ -760,6 +773,36 @@ 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` + ), + 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. * * This sandbox is compiled with dotty with support for Scala.js. It can be @@ -873,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, @@ -891,6 +940,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/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 new file mode 100644 index 000000000000..5ccc77fc2783 --- /dev/null +++ b/project/inject-sbt-dotty.sbt @@ -0,0 +1,10 @@ +// 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" + +// 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 + ) +} 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" + ] +}