Skip to content

Commit 21168b4

Browse files
committed
Fix completion of keywored-starting-idents (e.g. formatted) and add CTRL-T type/tree printer
1 parent 28362b8 commit 21168b4

File tree

9 files changed

+151
-91
lines changed

9 files changed

+151
-91
lines changed

src/repl-frontend/scala/tools/nsc/interpreter/jline/Reader.scala

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,21 @@
1313
package scala.tools.nsc.interpreter
1414
package jline
1515

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

2121
import shell.{Accumulator, ShellConfig}
2222
import Parser.ParseContext
23+
import org.jline.console.{CmdDesc, CmdLine}
24+
import org.jline.keymap.KeyMap
25+
import org.jline.utils.AttributedString
26+
import org.jline.widget.TailTipWidgets.TipType
27+
import org.jline.widget.{TailTipWidgets}
2328

2429
import java.{lang, util}
30+
import scala.reflect.internal.Chars
2531

2632
/** A Reader that delegates to JLine3.
2733
*/
@@ -95,21 +101,67 @@ object Reader {
95101
.option(Option.DISABLE_EVENT_EXPANSION, true) // Otherwise `scala> println(raw"\n".toList)` gives `List(n)` !!
96102
.option(Option.COMPLETE_MATCHER_CAMELCASE, true)
97103
}
98-
builder.completionMatcher(new CompletionMatcherImpl {
104+
object customCompletionMatcher extends CompletionMatcherImpl {
99105
override def compile(options: util.Map[LineReader.Option, lang.Boolean], prefix: Boolean, line: CompletingParsedLine, caseInsensitive: Boolean, errors: Int, originalGroupName: String): Unit = {
100106
super.compile(options, prefix, line, caseInsensitive, errors, originalGroupName)
101107
// TODO Use Option.COMPLETION_MATCHER_TYPO(false) in once https://github.com/jline/jline3/pull/646
102108
matchers.remove(matchers.size() - 2)
103-
// TODO add SNAKE_CASE completion matcher.
104109
}
105-
})
110+
111+
override def matches(candidates: JList[Candidate]): JList[Candidate] = {
112+
val matching = super.matches(candidates)
113+
matching
114+
}
115+
}
116+
117+
builder.completionMatcher(customCompletionMatcher)
106118

107119
val reader = builder.build()
120+
121+
val desc: java.util.function.Function[CmdLine, CmdDesc] = (cmdLine) => new CmdDesc(util.Arrays.asList(new AttributedString("demo")), Collections.emptyList(), Collections.emptyMap())
122+
new TailTipWidgets(reader, desc, 1, TipType.COMPLETER)
123+
val keyMap = reader.getKeyMaps.get("main")
124+
125+
object ScalaShowType {
126+
val Name = "scala-show-type"
127+
private var lastInvokeLocation: Option[(String, Int)] = None
128+
def apply(): Boolean = {
129+
val nextInvokeLocation = Some((reader.getBuffer.toString, reader.getBuffer.cursor()))
130+
val cursor = reader.getBuffer.cursor()
131+
val text = reader.getBuffer.toString
132+
val result = completer.complete(text, cursor, filter = true)
133+
if (lastInvokeLocation == nextInvokeLocation) {
134+
showTree(result)
135+
lastInvokeLocation = None
136+
} else {
137+
showType(result)
138+
lastInvokeLocation = nextInvokeLocation
139+
}
140+
true
141+
}
142+
def showType(result: shell.CompletionResult): Unit = {
143+
reader.getTerminal.writer.println()
144+
reader.getTerminal.writer.println(result.typeAtCursor)
145+
reader.callWidget(LineReader.REDRAW_LINE)
146+
reader.callWidget(LineReader.REDISPLAY)
147+
reader.getTerminal.flush()
148+
}
149+
def showTree(result: shell.CompletionResult): Unit = {
150+
reader.getTerminal.writer.println()
151+
reader.getTerminal.writer.println(Naming.unmangle(result.typedTree))
152+
reader.callWidget(LineReader.REDRAW_LINE)
153+
reader.callWidget(LineReader.REDISPLAY)
154+
reader.getTerminal.flush()
155+
}
156+
}
157+
reader.getWidgets().put(ScalaShowType.Name, () => ScalaShowType())
158+
108159
locally {
109160
import LineReader._
110161
// VIINS, VICMD, EMACS
111162
val keymap = if (config.viMode) VIINS else EMACS
112163
reader.getKeyMaps.put(MAIN, reader.getKeyMaps.get(keymap));
164+
keyMap.bind(new Reference(ScalaShowType.Name), KeyMap.ctrl('T'))
113165
}
114166
def secure(p: java.nio.file.Path): Unit = {
115167
try scala.reflect.internal.util.OwnerOnlyChmod.chmodFileOrCreateEmpty(p)
@@ -177,6 +229,12 @@ object Reader {
177229
val (wordCursor, wordIndex) = current match {
178230
case Some(t) if t.isIdentifier =>
179231
(cursor - t.start, tokens.indexOf(t))
232+
case Some(t) =>
233+
val isIdentifierStartKeyword = (t.start until t.end).forall(i => Chars.isIdentifierPart(line.charAt(i)))
234+
if (isIdentifierStartKeyword)
235+
(cursor - t.start, tokens.indexOf(t))
236+
else
237+
(0, -1)
180238
case _ =>
181239
(0, -1)
182240
}
@@ -233,15 +291,16 @@ object Reader {
233291
* It delegates both interfaces to an underlying `Completion`.
234292
*/
235293
class Completion(delegate: shell.Completion) extends shell.Completion with Completer {
294+
var lastPrefix: String = ""
236295
require(delegate != null)
237296
// REPL Completion
238297
def complete(buffer: String, cursor: Int, filter: Boolean): shell.CompletionResult = delegate.complete(buffer, cursor, filter)
239298

240299
// JLine Completer
241300
def complete(lineReader: LineReader, parsedLine: ParsedLine, newCandidates: JList[Candidate]): Unit = {
242301
def candidateForResult(cc: CompletionCandidate): Candidate = {
243-
val value = cc.defString
244-
val displayed = cc.defString + (cc.arity match {
302+
val value = cc.name
303+
val displayed = cc.name + (cc.arity match {
245304
case CompletionCandidate.Nullary => ""
246305
case CompletionCandidate.Nilary => "()"
247306
case _ => "("
@@ -257,23 +316,19 @@ class Completion(delegate: shell.Completion) extends shell.Completion with Compl
257316
new Candidate(value, displayed, group, descr, suffix, key, complete)
258317
}
259318
val result = complete(parsedLine.line, parsedLine.cursor, filter = false)
260-
result.candidates.map(_.defString) match {
261-
// the presence of the empty string here is a signal that the symbol
262-
// is already complete and so instead of completing, we want to show
263-
// the user the method signature. there are various JLine 3 features
264-
// one might use to do this instead; sticking to basics for now
265-
case "" :: defStrings if defStrings.nonEmpty =>
266-
// specifics here are cargo-culted from Ammonite
319+
for (cc <- result.candidates)
320+
newCandidates.add(candidateForResult(cc))
321+
322+
val parsedLineWord = parsedLine.word()
323+
result.candidates.filter(_.name == parsedLineWord) match {
324+
case Nil =>
325+
case exacts =>
267326
lineReader.getTerminal.writer.println()
268-
for (cc <- result.candidates.tail)
269-
lineReader.getTerminal.writer.println(cc.defString)
327+
for (cc <- exacts)
328+
lineReader.getTerminal.writer.println(cc.declString())
270329
lineReader.callWidget(LineReader.REDRAW_LINE)
271330
lineReader.callWidget(LineReader.REDISPLAY)
272331
lineReader.getTerminal.flush()
273-
// normal completion
274-
case _ =>
275-
for (cc <- result.candidates)
276-
newCandidates.add(candidateForResult(cc))
277332
}
278333
}
279334
}

src/repl-frontend/scala/tools/nsc/interpreter/shell/Completion.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ object NoCompletion extends Completion {
2121
def complete(buffer: String, cursor: Int, filter: Boolean) = NoCompletions
2222
}
2323

24-
case class CompletionResult(cursor: Int, candidates: List[CompletionCandidate]) {
24+
case class CompletionResult(cursor: Int, candidates: List[CompletionCandidate], typeAtCursor: String = "", typedTree: String = "") {
2525
final def orElse(other: => CompletionResult): CompletionResult =
2626
if (candidates.nonEmpty) this else other
2727
}
2828
object CompletionResult {
2929
val empty: CompletionResult = NoCompletions
3030
}
31-
object NoCompletions extends CompletionResult(-1, Nil)
31+
object NoCompletions extends CompletionResult(-1, Nil, "", "")
3232

3333
case class MultiCompletion(underlying: Completion*) extends Completion {
3434
override def complete(buffer: String, cursor: Int, filter: Boolean) =

src/repl-frontend/scala/tools/nsc/interpreter/shell/ILoop.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null,
253253
val maybes = intp.visibleSettings.filter(x => if (filter) x.name.startsWith(s) else true).map(_.name)
254254
.filterNot(cond(_) { case "-"|"-X"|"-Y" => true }).sorted
255255
if (maybes.isEmpty) NoCompletions
256-
else CompletionResult(cursor - s.length, maybes.map(CompletionCandidate(_)))
256+
else CompletionResult(cursor - s.length, maybes.map(CompletionCandidate(_)), "", "")
257257
case _ => NoCompletions
258258
}
259259
}
@@ -554,13 +554,13 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null,
554554
// condition here is a bit weird because of the weird hack we have where
555555
// the first candidate having an empty defString means it's not really
556556
// completion, but showing the method signature instead
557-
if (candidates.headOption.exists(_.defString.nonEmpty)) {
557+
if (candidates.headOption.exists(_.name.nonEmpty)) {
558558
val prefix =
559559
if (completions == NoCompletions) ""
560560
else what.substring(0, completions.cursor)
561561
// hvesalai (emacs sbt-mode maintainer) says it's important to echo only once and not per-line
562562
echo(
563-
candidates.map(c => s"[completions] $prefix${c.defString}")
563+
candidates.map(c => s"[completions] $prefix${c.name}")
564564
.mkString("\n")
565565
)
566566
}

src/repl-frontend/scala/tools/nsc/interpreter/shell/LoopCommands.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,12 @@ trait LoopCommands {
138138
val completion = ":" + cmd.name
139139
new Completion {
140140
def complete(buffer: String, cursor: Int, filter: Boolean) =
141-
CompletionResult(cursor, List(CompletionCandidate(completion)))
141+
CompletionResult(cursor, List(CompletionCandidate(completion)), "", "")
142142
}
143143
case cmd :: rest =>
144144
new Completion {
145145
def complete(buffer: String, cursor: Int, filter: Boolean) =
146-
CompletionResult(cursor, cmds.map(cmd => CompletionCandidate(":" + cmd.name)))
146+
CompletionResult(cursor, cmds.map(cmd => CompletionCandidate(":" + cmd.name)), "", "")
147147
}
148148
}
149149
case _ => NoCompletion

src/repl-frontend/scala/tools/nsc/interpreter/shell/ReplCompletion.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,16 @@ class ReplCompletion(intp: Repl, val accumulator: Accumulator = new Accumulator)
5151
case Right(result) => try {
5252
buf match {
5353
case slashPrint() if cursor == buf.length =>
54-
CompletionResult(cursor, CompletionCandidate.fromStrings("" :: Naming.unmangle(result.print) :: Nil))
54+
CompletionResult(cursor, CompletionCandidate.fromStrings("" :: Naming.unmangle(result.print) :: Nil), "", "")
5555
case slashPrintRaw() if cursor == buf.length =>
56-
CompletionResult(cursor, CompletionCandidate.fromStrings("" :: result.print :: Nil))
56+
CompletionResult(cursor, CompletionCandidate.fromStrings("" :: result.print :: Nil), "", "")
5757
case slashTypeAt(start, end) if cursor == buf.length =>
58-
CompletionResult(cursor, CompletionCandidate.fromStrings("" :: result.typeAt(start.toInt, end.toInt) :: Nil))
58+
CompletionResult(cursor, CompletionCandidate.fromStrings("" :: result.typeAt(start.toInt, end.toInt) :: Nil), "", "")
5959
case _ =>
6060
// under JLine 3, we no longer use the tabCount concept, so tabCount is always 1
6161
// which always gives us all completions
6262
val (c, r) = result.completionCandidates(filter, tabCount = 1)
63-
CompletionResult(c, r)
63+
CompletionResult(c, r, result.typeAt(cursor, cursor), result.print)
6464
}
6565
} finally result.cleanup()
6666
}

src/repl/scala/tools/nsc/interpreter/IMain.scala

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -784,9 +784,12 @@ class IMain(val settings: Settings, parentClassLoaderOverride: Option[ClassLoade
784784
// The source file contents only has the code originally input by the user,
785785
// with unit's body holding the synthetic trees.
786786
// When emitting errors, be careful not to refer to the synthetic code
787-
private val unit = new CompilationUnit(new BatchSourceFile(if (synthetic) "<synthetic>" else label, line))
787+
// pad with a trailing " " so that the synthetic position for enclosing trees does not exactly coincide with the
788+
// position of the user-written code, these seems to confuse the presentation compiler.
789+
private val paddedLine = line + " "
790+
private val unit = new CompilationUnit(new BatchSourceFile(if (synthetic) "<synthetic>" else label, paddedLine))
788791
// a dummy position used for synthetic trees (needed for pres compiler to locate the trees for user input)
789-
private val wholeUnit = Position.range(unit.source, 0, 0, line.length)
792+
private val wholeUnit = Position.range(unit.source, 0, 0, paddedLine.length)
790793

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

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

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

891-
// settings.Xprintpos.value = true
894+
// settings.Xprintpos.value = true
892895
showCode(asCompactString(unit.body))
893896

894897
unit

src/repl/scala/tools/nsc/interpreter/Interface.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,18 +323,19 @@ trait PresentationCompilationResult {
323323
def candidates(tabCount: Int): (Int, List[String]) =
324324
completionCandidates(tabCount) match {
325325
case (cursor, cands) =>
326-
(cursor, cands.map(_.defString))
326+
(cursor, cands.map(_.name))
327327
}
328328

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

333333
case class CompletionCandidate(
334-
defString: String,
334+
name: String,
335335
arity: CompletionCandidate.Arity = CompletionCandidate.Nullary,
336336
isDeprecated: Boolean = false,
337-
isUniversal: Boolean = false)
337+
isUniversal: Boolean = false,
338+
declString: () => String = () => "")
338339
object CompletionCandidate {
339340
sealed trait Arity
340341
case object Nullary extends Arity

0 commit comments

Comments
 (0)