Skip to content

Commit

Permalink
Fix completion of keywored-starting-idents (e.g. formatted) and add C…
Browse files Browse the repository at this point in the history
…TRL-T type/tree printer
  • Loading branch information
retronym committed Feb 3, 2021
1 parent 28362b8 commit 21168b4
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 91 deletions.
95 changes: 75 additions & 20 deletions src/repl-frontend/scala/tools/nsc/interpreter/jline/Reader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,21 @@
package scala.tools.nsc.interpreter
package jline

import java.util.{List => JList}
import org.jline.reader.{Candidate, Completer, CompletingParsedLine, EOFError, EndOfFileException, History, LineReader, ParsedLine, Parser, SyntaxError, UserInterruptException}
import java.util.{Collections, List => JList}
import org.jline.reader.{Candidate, Completer, CompletingParsedLine, EOFError, EndOfFileException, History, LineReader, ParsedLine, Parser, Reference, SyntaxError, UserInterruptException}
import org.jline.reader.impl.{CompletionMatcherImpl, DefaultParser, LineReaderImpl}
import org.jline.terminal.Terminal

import shell.{Accumulator, ShellConfig}
import Parser.ParseContext
import org.jline.console.{CmdDesc, CmdLine}
import org.jline.keymap.KeyMap
import org.jline.utils.AttributedString
import org.jline.widget.TailTipWidgets.TipType
import org.jline.widget.{TailTipWidgets}

import java.{lang, util}
import scala.reflect.internal.Chars

/** A Reader that delegates to JLine3.
*/
Expand Down Expand Up @@ -95,21 +101,67 @@ object Reader {
.option(Option.DISABLE_EVENT_EXPANSION, true) // Otherwise `scala> println(raw"\n".toList)` gives `List(n)` !!
.option(Option.COMPLETE_MATCHER_CAMELCASE, true)
}
builder.completionMatcher(new CompletionMatcherImpl {
object customCompletionMatcher extends CompletionMatcherImpl {
override def compile(options: util.Map[LineReader.Option, lang.Boolean], prefix: Boolean, line: CompletingParsedLine, caseInsensitive: Boolean, errors: Int, originalGroupName: String): Unit = {
super.compile(options, prefix, line, caseInsensitive, errors, originalGroupName)
// TODO Use Option.COMPLETION_MATCHER_TYPO(false) in once https://github.com/jline/jline3/pull/646
matchers.remove(matchers.size() - 2)
// TODO add SNAKE_CASE completion matcher.
}
})

override def matches(candidates: JList[Candidate]): JList[Candidate] = {
val matching = super.matches(candidates)
matching
}
}

builder.completionMatcher(customCompletionMatcher)

val reader = builder.build()

val desc: java.util.function.Function[CmdLine, CmdDesc] = (cmdLine) => new CmdDesc(util.Arrays.asList(new AttributedString("demo")), Collections.emptyList(), Collections.emptyMap())
new TailTipWidgets(reader, desc, 1, TipType.COMPLETER)
val keyMap = reader.getKeyMaps.get("main")

object ScalaShowType {
val Name = "scala-show-type"
private var lastInvokeLocation: Option[(String, Int)] = None
def apply(): Boolean = {
val nextInvokeLocation = Some((reader.getBuffer.toString, reader.getBuffer.cursor()))
val cursor = reader.getBuffer.cursor()
val text = reader.getBuffer.toString
val result = completer.complete(text, cursor, filter = true)
if (lastInvokeLocation == nextInvokeLocation) {
showTree(result)
lastInvokeLocation = None
} else {
showType(result)
lastInvokeLocation = nextInvokeLocation
}
true
}
def showType(result: shell.CompletionResult): Unit = {
reader.getTerminal.writer.println()
reader.getTerminal.writer.println(result.typeAtCursor)
reader.callWidget(LineReader.REDRAW_LINE)
reader.callWidget(LineReader.REDISPLAY)
reader.getTerminal.flush()
}
def showTree(result: shell.CompletionResult): Unit = {
reader.getTerminal.writer.println()
reader.getTerminal.writer.println(Naming.unmangle(result.typedTree))
reader.callWidget(LineReader.REDRAW_LINE)
reader.callWidget(LineReader.REDISPLAY)
reader.getTerminal.flush()
}
}
reader.getWidgets().put(ScalaShowType.Name, () => ScalaShowType())

locally {
import LineReader._
// VIINS, VICMD, EMACS
val keymap = if (config.viMode) VIINS else EMACS
reader.getKeyMaps.put(MAIN, reader.getKeyMaps.get(keymap));
keyMap.bind(new Reference(ScalaShowType.Name), KeyMap.ctrl('T'))
}
def secure(p: java.nio.file.Path): Unit = {
try scala.reflect.internal.util.OwnerOnlyChmod.chmodFileOrCreateEmpty(p)
Expand Down Expand Up @@ -177,6 +229,12 @@ object Reader {
val (wordCursor, wordIndex) = current match {
case Some(t) if t.isIdentifier =>
(cursor - t.start, tokens.indexOf(t))
case Some(t) =>
val isIdentifierStartKeyword = (t.start until t.end).forall(i => Chars.isIdentifierPart(line.charAt(i)))
if (isIdentifierStartKeyword)
(cursor - t.start, tokens.indexOf(t))
else
(0, -1)
case _ =>
(0, -1)
}
Expand Down Expand Up @@ -233,15 +291,16 @@ object Reader {
* It delegates both interfaces to an underlying `Completion`.
*/
class Completion(delegate: shell.Completion) extends shell.Completion with Completer {
var lastPrefix: String = ""
require(delegate != null)
// REPL Completion
def complete(buffer: String, cursor: Int, filter: Boolean): shell.CompletionResult = delegate.complete(buffer, cursor, filter)

// JLine Completer
def complete(lineReader: LineReader, parsedLine: ParsedLine, newCandidates: JList[Candidate]): Unit = {
def candidateForResult(cc: CompletionCandidate): Candidate = {
val value = cc.defString
val displayed = cc.defString + (cc.arity match {
val value = cc.name
val displayed = cc.name + (cc.arity match {
case CompletionCandidate.Nullary => ""
case CompletionCandidate.Nilary => "()"
case _ => "("
Expand All @@ -257,23 +316,19 @@ class Completion(delegate: shell.Completion) extends shell.Completion with Compl
new Candidate(value, displayed, group, descr, suffix, key, complete)
}
val result = complete(parsedLine.line, parsedLine.cursor, filter = false)
result.candidates.map(_.defString) match {
// the presence of the empty string here is a signal that the symbol
// is already complete and so instead of completing, we want to show
// the user the method signature. there are various JLine 3 features
// one might use to do this instead; sticking to basics for now
case "" :: defStrings if defStrings.nonEmpty =>
// specifics here are cargo-culted from Ammonite
for (cc <- result.candidates)
newCandidates.add(candidateForResult(cc))

val parsedLineWord = parsedLine.word()
result.candidates.filter(_.name == parsedLineWord) match {
case Nil =>
case exacts =>
lineReader.getTerminal.writer.println()
for (cc <- result.candidates.tail)
lineReader.getTerminal.writer.println(cc.defString)
for (cc <- exacts)
lineReader.getTerminal.writer.println(cc.declString())
lineReader.callWidget(LineReader.REDRAW_LINE)
lineReader.callWidget(LineReader.REDISPLAY)
lineReader.getTerminal.flush()
// normal completion
case _ =>
for (cc <- result.candidates)
newCandidates.add(candidateForResult(cc))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ object NoCompletion extends Completion {
def complete(buffer: String, cursor: Int, filter: Boolean) = NoCompletions
}

case class CompletionResult(cursor: Int, candidates: List[CompletionCandidate]) {
case class CompletionResult(cursor: Int, candidates: List[CompletionCandidate], typeAtCursor: String = "", typedTree: String = "") {
final def orElse(other: => CompletionResult): CompletionResult =
if (candidates.nonEmpty) this else other
}
object CompletionResult {
val empty: CompletionResult = NoCompletions
}
object NoCompletions extends CompletionResult(-1, Nil)
object NoCompletions extends CompletionResult(-1, Nil, "", "")

case class MultiCompletion(underlying: Completion*) extends Completion {
override def complete(buffer: String, cursor: Int, filter: Boolean) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null,
val maybes = intp.visibleSettings.filter(x => if (filter) x.name.startsWith(s) else true).map(_.name)
.filterNot(cond(_) { case "-"|"-X"|"-Y" => true }).sorted
if (maybes.isEmpty) NoCompletions
else CompletionResult(cursor - s.length, maybes.map(CompletionCandidate(_)))
else CompletionResult(cursor - s.length, maybes.map(CompletionCandidate(_)), "", "")
case _ => NoCompletions
}
}
Expand Down Expand Up @@ -554,13 +554,13 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null,
// condition here is a bit weird because of the weird hack we have where
// the first candidate having an empty defString means it's not really
// completion, but showing the method signature instead
if (candidates.headOption.exists(_.defString.nonEmpty)) {
if (candidates.headOption.exists(_.name.nonEmpty)) {
val prefix =
if (completions == NoCompletions) ""
else what.substring(0, completions.cursor)
// hvesalai (emacs sbt-mode maintainer) says it's important to echo only once and not per-line
echo(
candidates.map(c => s"[completions] $prefix${c.defString}")
candidates.map(c => s"[completions] $prefix${c.name}")
.mkString("\n")
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,12 @@ trait LoopCommands {
val completion = ":" + cmd.name
new Completion {
def complete(buffer: String, cursor: Int, filter: Boolean) =
CompletionResult(cursor, List(CompletionCandidate(completion)))
CompletionResult(cursor, List(CompletionCandidate(completion)), "", "")
}
case cmd :: rest =>
new Completion {
def complete(buffer: String, cursor: Int, filter: Boolean) =
CompletionResult(cursor, cmds.map(cmd => CompletionCandidate(":" + cmd.name)))
CompletionResult(cursor, cmds.map(cmd => CompletionCandidate(":" + cmd.name)), "", "")
}
}
case _ => NoCompletion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,16 @@ class ReplCompletion(intp: Repl, val accumulator: Accumulator = new Accumulator)
case Right(result) => try {
buf match {
case slashPrint() if cursor == buf.length =>
CompletionResult(cursor, CompletionCandidate.fromStrings("" :: Naming.unmangle(result.print) :: Nil))
CompletionResult(cursor, CompletionCandidate.fromStrings("" :: Naming.unmangle(result.print) :: Nil), "", "")
case slashPrintRaw() if cursor == buf.length =>
CompletionResult(cursor, CompletionCandidate.fromStrings("" :: result.print :: Nil))
CompletionResult(cursor, CompletionCandidate.fromStrings("" :: result.print :: Nil), "", "")
case slashTypeAt(start, end) if cursor == buf.length =>
CompletionResult(cursor, CompletionCandidate.fromStrings("" :: result.typeAt(start.toInt, end.toInt) :: Nil))
CompletionResult(cursor, CompletionCandidate.fromStrings("" :: result.typeAt(start.toInt, end.toInt) :: Nil), "", "")
case _ =>
// under JLine 3, we no longer use the tabCount concept, so tabCount is always 1
// which always gives us all completions
val (c, r) = result.completionCandidates(filter, tabCount = 1)
CompletionResult(c, r)
CompletionResult(c, r, result.typeAt(cursor, cursor), result.print)
}
} finally result.cleanup()
}
Expand Down
11 changes: 7 additions & 4 deletions src/repl/scala/tools/nsc/interpreter/IMain.scala
Original file line number Diff line number Diff line change
Expand Up @@ -784,9 +784,12 @@ class IMain(val settings: Settings, parentClassLoaderOverride: Option[ClassLoade
// The source file contents only has the code originally input by the user,
// with unit's body holding the synthetic trees.
// When emitting errors, be careful not to refer to the synthetic code
private val unit = new CompilationUnit(new BatchSourceFile(if (synthetic) "<synthetic>" else label, line))
// pad with a trailing " " so that the synthetic position for enclosing trees does not exactly coincide with the
// position of the user-written code, these seems to confuse the presentation compiler.
private val paddedLine = line + " "
private val unit = new CompilationUnit(new BatchSourceFile(if (synthetic) "<synthetic>" else label, paddedLine))
// a dummy position used for synthetic trees (needed for pres compiler to locate the trees for user input)
private val wholeUnit = Position.range(unit.source, 0, 0, line.length)
private val wholeUnit = Position.range(unit.source, 0, 0, paddedLine.length)

private def storeInVal(tree: Tree): Tree = {
val resName = newTermName(if (synthetic) freshInternalVarName() else freshUserVarName())
Expand Down Expand Up @@ -882,13 +885,13 @@ class IMain(val settings: Settings, parentClassLoaderOverride: Option[ClassLoade
else ModuleDef(NoMods, readName, wrapperTempl))

if (isClassBased)
stats += q"""object $readName { val INSTANCE = new ${tq"""${readName.toTypeName}"""} }"""
stats += atPos(wholeUnit.focus)(q"""object $readName { val INSTANCE = new ${tq"""${readName.toTypeName}"""} }""")

val unspliced = PackageDef(atPos(wholeUnit.focus)(Ident(lineRep.packageName)), stats.toList)
unit.body = spliceUserCode.transform(unspliced)
unit.encounteredXml(firstXmlPos)

// settings.Xprintpos.value = true
// settings.Xprintpos.value = true
showCode(asCompactString(unit.body))

unit
Expand Down
7 changes: 4 additions & 3 deletions src/repl/scala/tools/nsc/interpreter/Interface.scala
Original file line number Diff line number Diff line change
Expand Up @@ -323,18 +323,19 @@ trait PresentationCompilationResult {
def candidates(tabCount: Int): (Int, List[String]) =
completionCandidates(tabCount) match {
case (cursor, cands) =>
(cursor, cands.map(_.defString))
(cursor, cands.map(_.name))
}

final def completionCandidates(tabCount: Int = -1): (Int, List[CompletionCandidate]) = completionCandidates(filter = true, tabCount)
def completionCandidates(filter: Boolean, tabCount: Int): (Int, List[CompletionCandidate])
}

case class CompletionCandidate(
defString: String,
name: String,
arity: CompletionCandidate.Arity = CompletionCandidate.Nullary,
isDeprecated: Boolean = false,
isUniversal: Boolean = false)
isUniversal: Boolean = false,
declString: () => String = () => "")
object CompletionCandidate {
sealed trait Arity
case object Nullary extends Arity
Expand Down
Loading

0 comments on commit 21168b4

Please sign in to comment.