From 5e2c8692553ea31605f396e0ad0bb89c592512f9 Mon Sep 17 00:00:00 2001 From: Chris Kipp Date: Sun, 6 Mar 2022 09:14:19 +0100 Subject: [PATCH 1/3] feat(repl): autocomplete repl commands This adds in completion for the available commands in the repl just like there is for Scala 2. For example you can do `:@@` which will give you back all the commands. --- .../src/dotty/tools/repl/ParseResult.scala | 2 +- .../src/dotty/tools/repl/ReplDriver.scala | 34 +++++++++++-------- .../dotty/tools/repl/TabcompleteTests.scala | 16 +++++++++ 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/compiler/src/dotty/tools/repl/ParseResult.scala b/compiler/src/dotty/tools/repl/ParseResult.scala index 5d414ecf9b34..effd7740517f 100644 --- a/compiler/src/dotty/tools/repl/ParseResult.scala +++ b/compiler/src/dotty/tools/repl/ParseResult.scala @@ -127,7 +127,7 @@ object ParseResult { stats } - private val commands: List[(String, String => ParseResult)] = List( + private[repl] val commands: List[(String, String => ParseResult)] = List( Quit.command -> (_ => Quit), Quit.alias -> (_ => Quit), Help.command -> (_ => Help), diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index 2ae38e2d0c84..741e64824dfa 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -205,7 +205,7 @@ class ReplDriver(settings: Array[String], label /** Extract possible completions at the index of `cursor` in `expr` */ - protected final def completions(cursor: Int, expr: String, state0: State): List[Candidate] = { + protected final def completions(cursor: Int, expr: String, state0: State): List[Candidate] = def makeCandidate(label: String) = { new Candidate( @@ -218,20 +218,24 @@ class ReplDriver(settings: Array[String], /* complete = */ false // if true adds space when completing ) } - implicit val state = newRun(state0) - compiler - .typeCheck(expr, errorsAllowed = true) - .map { tree => - val file = SourceFile.virtual("", expr, maybeIncomplete = true) - val unit = CompilationUnit(file)(using state.context) - unit.tpdTree = tree - given Context = state.context.fresh.setCompilationUnit(unit) - val srcPos = SourcePosition(file, Span(cursor)) - val (_, completions) = Completion.completions(srcPos) - completions.map(_.label).distinct.map(makeCandidate) - } - .getOrElse(Nil) - } + + if expr.startsWith(":") then + ParseResult.commands.map(command => makeCandidate(command._1)) + else + given state: State = newRun(state0) + compiler + .typeCheck(expr, errorsAllowed = true) + .map { tree => + val file = SourceFile.virtual("", expr, maybeIncomplete = true) + val unit = CompilationUnit(file)(using state.context) + unit.tpdTree = tree + given Context = state.context.fresh.setCompilationUnit(unit) + val srcPos = SourcePosition(file, Span(cursor)) + val (_, completions) = Completion.completions(srcPos) + completions.map(_.label).distinct.map(makeCandidate) + } + .getOrElse(Nil) + end completions private def interpret(res: ParseResult)(implicit state: State): State = { res match { diff --git a/compiler/test/dotty/tools/repl/TabcompleteTests.scala b/compiler/test/dotty/tools/repl/TabcompleteTests.scala index 61257f99a9c3..35ee7fb6d6d6 100644 --- a/compiler/test/dotty/tools/repl/TabcompleteTests.scala +++ b/compiler/test/dotty/tools/repl/TabcompleteTests.scala @@ -191,4 +191,20 @@ class TabcompleteTests extends ReplTest { |Foo.`bac"""stripMargin)) } + @Test def commands = initially { + assertEquals( + List( + ":doc", + ":exit", + ":help", + ":imports", + ":load", + ":quit", + ":reset", + ":settings", + ":type" + ), + tabComplete(":") + ) + } } From 4577255fbcb374a9bfe2fa69f464061003a23151 Mon Sep 17 00:00:00 2001 From: Chris Kipp Date: Mon, 7 Mar 2022 10:36:59 +0100 Subject: [PATCH 2/3] fix: take into account preface for repl commands --- compiler/src/dotty/tools/repl/ReplDriver.scala | 10 +++++++++- compiler/test/dotty/tools/repl/TabcompleteTests.scala | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index 741e64824dfa..913017750662 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -220,7 +220,15 @@ class ReplDriver(settings: Array[String], } if expr.startsWith(":") then - ParseResult.commands.map(command => makeCandidate(command._1)) + ParseResult.commands.collect { + // If expr is only : then we return the commands with : since jline + // correctly handles them + case command if expr == ":" => makeCandidate(command._1) + // However if there is more than just the : we filter by it and then + // drop the : since jline will correctly complete it but you'll end up + // with ::import for example instead of :import + case command if command._1.startsWith(expr) => makeCandidate(command._1.drop(1)) + } else given state: State = newRun(state0) compiler diff --git a/compiler/test/dotty/tools/repl/TabcompleteTests.scala b/compiler/test/dotty/tools/repl/TabcompleteTests.scala index 35ee7fb6d6d6..c29a7c23c980 100644 --- a/compiler/test/dotty/tools/repl/TabcompleteTests.scala +++ b/compiler/test/dotty/tools/repl/TabcompleteTests.scala @@ -207,4 +207,12 @@ class TabcompleteTests extends ReplTest { tabComplete(":") ) } + + @Test def commandPreface = initially { + // This looks odd, but if we return :doc here it will result in ::doc in the REPL + assertEquals( + List("doc"), + tabComplete(":d") + ) + } } From 3323d5d690bdc5cbfdfada8b87cc8578587b1b52 Mon Sep 17 00:00:00 2001 From: Chris Kipp Date: Tue, 8 Mar 2022 18:48:01 +0100 Subject: [PATCH 3/3] fix: don't split : into two tokens. In the situation where we have `:im` normally `:` and `im` would be split into two tokens and it would result in `::imports`. Instead, we detect early if we are about to complete a command and instead of parsing it normally like Scala we just grab the entire thing as the word. --- compiler/src/dotty/tools/repl/JLineTerminal.scala | 8 ++++++++ compiler/src/dotty/tools/repl/ReplDriver.scala | 8 +------- compiler/test/dotty/tools/repl/TabcompleteTests.scala | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/repl/JLineTerminal.scala b/compiler/src/dotty/tools/repl/JLineTerminal.scala index 69a9b1119c39..9da12ae955d1 100644 --- a/compiler/src/dotty/tools/repl/JLineTerminal.scala +++ b/compiler/src/dotty/tools/repl/JLineTerminal.scala @@ -152,6 +152,14 @@ final class JLineTerminal extends java.io.Closeable { // using dummy values, resulting parsed input is probably unused defaultParsedLine + // In the situation where we have a partial command that we want to + // complete we need to ensure that the : isn't split into + // 2 tokens, but rather the entire thing is treated as the "word", in + // order to insure the : is replaced in the completion. + case ParseContext.COMPLETE if + ParseResult.commands.exists(command => command._1.startsWith(input)) => + parsedLine(input, cursor) + case ParseContext.COMPLETE => // Parse to find completions (typically after a Tab). def isCompletable(token: Token) = isIdentifier(token) || isKeyword(token) diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index 913017750662..337f87725762 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -221,13 +221,7 @@ class ReplDriver(settings: Array[String], if expr.startsWith(":") then ParseResult.commands.collect { - // If expr is only : then we return the commands with : since jline - // correctly handles them - case command if expr == ":" => makeCandidate(command._1) - // However if there is more than just the : we filter by it and then - // drop the : since jline will correctly complete it but you'll end up - // with ::import for example instead of :import - case command if command._1.startsWith(expr) => makeCandidate(command._1.drop(1)) + case command if command._1.startsWith(expr) => makeCandidate(command._1) } else given state: State = newRun(state0) diff --git a/compiler/test/dotty/tools/repl/TabcompleteTests.scala b/compiler/test/dotty/tools/repl/TabcompleteTests.scala index c29a7c23c980..555137226bcb 100644 --- a/compiler/test/dotty/tools/repl/TabcompleteTests.scala +++ b/compiler/test/dotty/tools/repl/TabcompleteTests.scala @@ -211,7 +211,7 @@ class TabcompleteTests extends ReplTest { @Test def commandPreface = initially { // This looks odd, but if we return :doc here it will result in ::doc in the REPL assertEquals( - List("doc"), + List(":doc"), tabComplete(":d") ) }