Skip to content

Commit fb9fa4f

Browse files
committed
Improvements to code assist in the REPL
Re-enable acronmyn-style completion, e.g. getClass.gdm` offers `getDeclaredMethod[s]`. Disable the typo-matcher in JLine which tends to offer confusing Fix completion of keywored-starting-idents (e.g. `this.for<TAB>` offers `formatted`. Register a widget on CTRL-SHIFT-T that prints the type of the expression at the cursor. A second invokation prints the desugared AST.
1 parent 074cae1 commit fb9fa4f

File tree

11 files changed

+227
-175
lines changed

11 files changed

+227
-175
lines changed

src/interactive/scala/tools/nsc/interactive/Global.scala

Lines changed: 25 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,54 +1197,36 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "")
11971197
override def positionDelta = 0
11981198
override def forImport: Boolean = false
11991199
}
1200-
private val CamelRegex = "([A-Z][^A-Z]*)".r
1201-
private def camelComponents(s: String, allowSnake: Boolean): List[String] = {
1202-
if (allowSnake && s.forall(c => c.isUpper || c == '_')) s.split('_').toList.filterNot(_.isEmpty)
1203-
else CamelRegex.findAllIn("X" + s).toList match { case head :: tail => head.drop(1) :: tail; case Nil => Nil }
1204-
}
1205-
def camelMatch(entered: Name): Name => Boolean = {
1206-
val enteredS = entered.toString
1207-
val enteredLowercaseSet = enteredS.toLowerCase().toSet
1208-
val allowSnake = !enteredS.contains('_')
1209-
1210-
{
1211-
candidate: Name =>
1212-
def candidateChunks = camelComponents(candidate.dropLocal.toString, allowSnake)
1213-
// Loosely based on IntelliJ's autocompletion: the user can just write everything in
1214-
// lowercase, as we'll let `isl` match `GenIndexedSeqLike` or `isLovely`.
1215-
def lenientMatch(entered: String, candidate: List[String], matchCount: Int): Boolean = {
1216-
candidate match {
1217-
case Nil => entered.isEmpty && matchCount > 0
1218-
case head :: tail =>
1219-
val enteredAlternatives = Set(entered, entered.capitalize)
1220-
val n = head.toIterable.lazyZip(entered).count {case (c, e) => c == e || (c.isUpper && c == e.toUpper)}
1221-
head.take(n).inits.exists(init =>
1222-
enteredAlternatives.exists(entered =>
1223-
lenientMatch(entered.stripPrefix(init), tail, matchCount + (if (init.isEmpty) 0 else 1))
1224-
)
1225-
)
1226-
}
1227-
}
1228-
val containsAllEnteredChars = {
1229-
// Trying to rule out some candidates quickly before the more expensive `lenientMatch`
1230-
val candidateLowercaseSet = candidate.toString.toLowerCase().toSet
1231-
enteredLowercaseSet.diff(candidateLowercaseSet).isEmpty
1232-
}
1233-
containsAllEnteredChars && lenientMatch(enteredS, candidateChunks, 0)
1234-
}
1235-
}
12361200
}
12371201

12381202
final def completionsAt(pos: Position): CompletionResult = {
12391203
val focus1: Tree = typedTreeAt(pos)
12401204
def typeCompletions(tree: Tree, qual: Tree, nameStart: Int, name: Name): CompletionResult = {
12411205
val qualPos = qual.pos
1242-
val allTypeMembers = typeMembers(qualPos).last
1206+
val saved = tree.tpe
1207+
// Force `typeMembers` to complete via the prefix, not the type of the Select itself.
1208+
tree.setType(ErrorType)
1209+
val allTypeMembers = try {
1210+
typeMembers(qualPos).last
1211+
} finally {
1212+
tree.setType(saved)
1213+
}
12431214
val positionDelta: Int = pos.start - nameStart
12441215
val subName: Name = name.newName(new String(pos.source.content, nameStart, pos.start - nameStart)).encodedName
12451216
CompletionResult.TypeMembers(positionDelta, qual, tree, allTypeMembers, subName)
12461217
}
12471218
focus1 match {
1219+
case Apply(Select(qual, name), _) if qual.hasAttachment[InterpolatedString.type] =>
1220+
// This special case makes CompletionTest.incompleteStringInterpolation work.
1221+
// In incomplete code, the parser treats `foo""` as a nested string interpolation, even
1222+
// though it is likely that the user wanted to complete `fooBar` before adding the closing brace.
1223+
// val fooBar = 42; s"abc ${foo<TAB>"
1224+
//
1225+
// TODO: We could also complete the selection here to expand `ra<TAB>"..."` to `raw"..."`.
1226+
val allMembers = scopeMembers(pos)
1227+
val positionDelta: Int = pos.start - focus1.pos.start
1228+
val subName = name.subName(0, positionDelta)
1229+
CompletionResult.ScopeMembers(positionDelta, allMembers, subName, forImport = false)
12481230
case imp@Import(i @ Ident(name), head :: Nil) if head.name == nme.ERROR =>
12491231
val allMembers = scopeMembers(pos)
12501232
val nameStart = i.pos.start
@@ -1259,9 +1241,13 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "")
12591241
}
12601242
case sel@Select(qual, name) =>
12611243
val qualPos = qual.pos
1262-
def fallback = qualPos.end + 2
1244+
val effectiveQualEnd = if (qualPos.isRange) qualPos.end else qualPos.point - 1
1245+
def fallback = {
1246+
effectiveQualEnd + 2
1247+
}
12631248
val source = pos.source
1264-
val nameStart: Int = (focus1.pos.end - 1 to qualPos.end by -1).find(p =>
1249+
1250+
val nameStart: Int = (focus1.pos.end - 1 to effectiveQualEnd by -1).find(p =>
12651251
source.identifier(source.position(p)).exists(_.length == 0)
12661252
).map(_ + 1).getOrElse(fallback)
12671253
typeCompletions(sel, qual, nameStart, name)

src/reflect/scala/reflect/internal/Positions.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,8 @@ trait Positions extends api.Positions { self: SymbolTable =>
345345
if (t.pos includes pos) {
346346
if (isEligible(t)) last = t
347347
super.traverse(t)
348-
} else t match {
348+
}
349+
t match {
349350
case mdef: MemberDef =>
350351
val annTrees = mdef.mods.annotations match {
351352
case Nil if mdef.symbol != null =>

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

Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -122,17 +122,72 @@ object Reader {
122122
.variable(SECONDARY_PROMPT_PATTERN, config.encolor(config.continueText)) // Continue prompt
123123
.variable(WORDCHARS, LineReaderImpl.DEFAULT_WORDCHARS.filterNot("*?.[]~=/&;!#%^(){}<>".toSet))
124124
.option(Option.DISABLE_EVENT_EXPANSION, true) // Otherwise `scala> println(raw"\n".toList)` gives `List(n)` !!
125+
.option(Option.COMPLETE_MATCHER_CAMELCASE, true)
126+
}
127+
object customCompletionMatcher extends CompletionMatcherImpl {
128+
override def compile(options: util.Map[LineReader.Option, lang.Boolean], prefix: Boolean, line: CompletingParsedLine, caseInsensitive: Boolean, errors: Int, originalGroupName: String): Unit = {
129+
super.compile(options, prefix, line, caseInsensitive, errors, originalGroupName)
130+
// TODO Use Option.COMPLETION_MATCHER_TYPO(false) in once https://github.com/jline/jline3/pull/646
131+
matchers.remove(matchers.size() - 2)
132+
}
133+
134+
override def matches(candidates: JList[Candidate]): JList[Candidate] = {
135+
val matching = super.matches(candidates)
136+
matching
137+
}
125138
}
126139

140+
builder.completionMatcher(customCompletionMatcher)
141+
127142
val reader = builder.build()
128143
try inputrcFileContents.foreach(f => InputRC.configure(reader, new ByteArrayInputStream(f))) catch {
129144
case NonFatal(_) =>
130145
} //ignore
146+
147+
val desc: java.util.function.Function[CmdLine, CmdDesc] = (cmdLine) => new CmdDesc(util.Arrays.asList(new AttributedString("demo")), Collections.emptyList(), Collections.emptyMap())
148+
new TailTipWidgets(reader, desc, 1, TipType.COMPLETER)
149+
val keyMap = reader.getKeyMaps.get("main")
150+
151+
object ScalaShowType {
152+
val Name = "scala-show-type"
153+
private var lastInvokeLocation: Option[(String, Int)] = None
154+
def apply(): Boolean = {
155+
val nextInvokeLocation = Some((reader.getBuffer.toString, reader.getBuffer.cursor()))
156+
val cursor = reader.getBuffer.cursor()
157+
val text = reader.getBuffer.toString
158+
val result = completer.complete(text, cursor, filter = true)
159+
if (lastInvokeLocation == nextInvokeLocation) {
160+
showTree(result)
161+
lastInvokeLocation = None
162+
} else {
163+
showType(result)
164+
lastInvokeLocation = nextInvokeLocation
165+
}
166+
true
167+
}
168+
def showType(result: shell.CompletionResult): Unit = {
169+
reader.getTerminal.writer.println()
170+
reader.getTerminal.writer.println(result.typeAtCursor)
171+
reader.callWidget(LineReader.REDRAW_LINE)
172+
reader.callWidget(LineReader.REDISPLAY)
173+
reader.getTerminal.flush()
174+
}
175+
def showTree(result: shell.CompletionResult): Unit = {
176+
reader.getTerminal.writer.println()
177+
reader.getTerminal.writer.println(Naming.unmangle(result.typedTree))
178+
reader.callWidget(LineReader.REDRAW_LINE)
179+
reader.callWidget(LineReader.REDISPLAY)
180+
reader.getTerminal.flush()
181+
}
182+
}
183+
reader.getWidgets().put(ScalaShowType.Name, () => ScalaShowType())
184+
131185
locally {
132186
import LineReader._
133187
// VIINS, VICMD, EMACS
134188
val keymap = if (config.viMode) VIINS else EMACS
135189
reader.getKeyMaps.put(MAIN, reader.getKeyMaps.get(keymap));
190+
keyMap.bind(new Reference(ScalaShowType.Name), KeyMap.ctrl('T'))
136191
}
137192
def secure(p: java.nio.file.Path): Unit = {
138193
try scala.reflect.internal.util.OwnerOnlyChmod.chmodFileOrCreateEmpty(p)
@@ -201,6 +256,12 @@ object Reader {
201256
val (wordCursor, wordIndex) = current match {
202257
case Some(t) if t.isIdentifier =>
203258
(cursor - t.start, tokens.indexOf(t))
259+
case Some(t) =>
260+
val isIdentifierStartKeyword = (t.start until t.end).forall(i => Chars.isIdentifierPart(line.charAt(i)))
261+
if (isIdentifierStartKeyword)
262+
(cursor - t.start, tokens.indexOf(t))
263+
else
264+
(0, -1)
204265
case _ =>
205266
(0, -1)
206267
}
@@ -257,15 +318,16 @@ object Reader {
257318
* It delegates both interfaces to an underlying `Completion`.
258319
*/
259320
class Completion(delegate: shell.Completion) extends shell.Completion with Completer {
321+
var lastPrefix: String = ""
260322
require(delegate != null)
261323
// REPL Completion
262-
def complete(buffer: String, cursor: Int): shell.CompletionResult = delegate.complete(buffer, cursor)
324+
def complete(buffer: String, cursor: Int, filter: Boolean): shell.CompletionResult = delegate.complete(buffer, cursor, filter)
263325

264326
// JLine Completer
265327
def complete(lineReader: LineReader, parsedLine: ParsedLine, newCandidates: JList[Candidate]): Unit = {
266-
def candidateForResult(line: String, cc: CompletionCandidate): Candidate = {
267-
val value = if (line.startsWith(":")) ":" + cc.defString else cc.defString
268-
val displayed = cc.defString + (cc.arity match {
328+
def candidateForResult(cc: CompletionCandidate): Candidate = {
329+
val value = cc.name
330+
val displayed = cc.name + (cc.arity match {
269331
case CompletionCandidate.Nullary => ""
270332
case CompletionCandidate.Nilary => "()"
271333
case _ => "("
@@ -280,24 +342,20 @@ class Completion(delegate: shell.Completion) extends shell.Completion with Compl
280342
val complete = false // more to complete?
281343
new Candidate(value, displayed, group, descr, suffix, key, complete)
282344
}
283-
val result = complete(parsedLine.line, parsedLine.cursor)
284-
result.candidates.map(_.defString) match {
285-
// the presence of the empty string here is a signal that the symbol
286-
// is already complete and so instead of completing, we want to show
287-
// the user the method signature. there are various JLine 3 features
288-
// one might use to do this instead; sticking to basics for now
289-
case "" :: defStrings if defStrings.nonEmpty =>
290-
// specifics here are cargo-culted from Ammonite
345+
val result = complete(parsedLine.line, parsedLine.cursor, filter = false)
346+
for (cc <- result.candidates)
347+
newCandidates.add(candidateForResult(cc))
348+
349+
val parsedLineWord = parsedLine.word()
350+
result.candidates.filter(_.name == parsedLineWord) match {
351+
case Nil =>
352+
case exacts =>
291353
lineReader.getTerminal.writer.println()
292-
for (cc <- result.candidates.tail)
293-
lineReader.getTerminal.writer.println(cc.defString)
354+
for (cc <- exacts)
355+
lineReader.getTerminal.writer.println(cc.declString())
294356
lineReader.callWidget(LineReader.REDRAW_LINE)
295357
lineReader.callWidget(LineReader.REDISPLAY)
296358
lineReader.getTerminal.flush()
297-
// normal completion
298-
case _ =>
299-
for (cc <- result.candidates)
300-
newCandidates.add(candidateForResult(result.line, cc))
301359
}
302360
}
303361
}

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,23 @@ package scala.tools.nsc.interpreter
1414
package shell
1515

1616
trait Completion {
17-
def complete(buffer: String, cursor: Int): CompletionResult
17+
final def complete(buffer: String, cursor: Int): CompletionResult = complete(buffer, cursor, filter = true)
18+
def complete(buffer: String, cursor: Int, filter: Boolean): CompletionResult
1819
}
1920
object NoCompletion extends Completion {
20-
def complete(buffer: String, cursor: Int) = NoCompletions
21+
def complete(buffer: String, cursor: Int, filter: Boolean) = NoCompletions
2122
}
2223

23-
case class CompletionResult(line: String, cursor: Int, candidates: List[CompletionCandidate]) {
24+
case class CompletionResult(line: String, cursor: Int, candidates: List[CompletionCandidate], typeAtCursor: String = "", typedTree: String = "") {
2425
final def orElse(other: => CompletionResult): CompletionResult =
2526
if (candidates.nonEmpty) this else other
2627
}
2728
object CompletionResult {
2829
val empty: CompletionResult = NoCompletions
2930
}
30-
object NoCompletions extends CompletionResult("", -1, Nil)
31+
object NoCompletions extends CompletionResult("", -1, Nil, "", "")
3132

3233
case class MultiCompletion(underlying: Completion*) extends Completion {
33-
override def complete(buffer: String, cursor: Int) =
34-
underlying.foldLeft(CompletionResult.empty)((r, c) => r.orElse(c.complete(buffer, cursor)))
34+
override def complete(buffer: String, cursor: Int, filter: Boolean) =
35+
underlying.foldLeft(CompletionResult.empty)((r,c) => r.orElse(c.complete(buffer, cursor, filter)))
3536
}

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null,
228228
.map(d => CompletionResult(buffer, i, d.toDirectory.list.map(x => CompletionCandidate(x.name)).toList))
229229
.getOrElse(NoCompletions)
230230
def listedIn(dir: Directory, name: String) = dir.list.filter(_.name.startsWith(name)).map(_.name).toList
231-
def complete(buffer: String, cursor: Int): CompletionResult =
231+
def complete(buffer: String, cursor: Int, filter: Boolean): CompletionResult =
232232
buffer.substring(0, cursor) match {
233233
case emptyWord(s) => listed(buffer, cursor, Directory.Current)
234234
case directorily(s) => listed(buffer, cursor, Option(Path(s)))
@@ -247,13 +247,13 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null,
247247
// complete settings name
248248
val settingsCompletion: Completion = new Completion {
249249
val trailingWord = """(\S+)$""".r.unanchored
250-
def complete(buffer: String, cursor: Int): CompletionResult = {
250+
def complete(buffer: String, cursor: Int, filter: Boolean): CompletionResult = {
251251
buffer.substring(0, cursor) match {
252252
case trailingWord(s) =>
253-
val maybes = intp.visibleSettings.filter(_.name.startsWith(s)).map(_.name)
253+
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(buffer, cursor - s.length, maybes.map(CompletionCandidate(_)))
256+
else CompletionResult(buffer, cursor - s.length, maybes.map(CompletionCandidate(_)), "", "")
257257
case _ => NoCompletions
258258
}
259259
}
@@ -541,8 +541,8 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null,
541541
MultiCompletion(shellCompletion, rc)
542542
}
543543
val shellCompletion = new Completion {
544-
override def complete(buffer: String, cursor: Int) =
545-
if (buffer.startsWith(":")) colonCompletion(buffer, cursor).complete(buffer, cursor)
544+
override def complete(buffer: String, cursor: Int, filter: Boolean) =
545+
if (buffer.startsWith(":")) colonCompletion(buffer, cursor).complete(buffer, cursor, filter)
546546
else NoCompletions
547547
}
548548

@@ -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: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ package scala.tools.nsc.interpreter
1414
package shell
1515

1616
import java.io.{PrintWriter => JPrintWriter}
17-
1817
import scala.language.implicitConversions
1918
import scala.collection.mutable.ListBuffer
2019
import scala.tools.nsc.interpreter.ReplStrings.words
@@ -60,6 +59,7 @@ trait LoopCommands {
6059

6160
// subclasses may provide completions
6261
def completion: Completion = NoCompletion
62+
override def toString(): String = name
6363
}
6464
object LoopCommand {
6565
def nullary(name: String, help: String, f: () => Result): LoopCommand =
@@ -135,15 +135,15 @@ trait LoopCommands {
135135
case cmd :: Nil if !cursorAtName => cmd.completion
136136
case cmd :: Nil if cmd.name == name => NoCompletion
137137
case cmd :: Nil =>
138-
val completion = if (cmd.isInstanceOf[NullaryCmd] || cursor < line.length) cmd.name else cmd.name + " "
138+
val completion = ":" + cmd.name
139139
new Completion {
140-
def complete(buffer: String, cursor: Int) =
141-
CompletionResult(buffer, cursor = 1, List(CompletionCandidate(completion)))
140+
def complete(buffer: String, cursor: Int, filter: Boolean) =
141+
CompletionResult(buffer, cursor = 1, List(CompletionCandidate(completion)), "", "")
142142
}
143143
case cmd :: rest =>
144144
new Completion {
145-
def complete(buffer: String, cursor: Int) =
146-
CompletionResult(buffer, cursor = 1, cmds.map(cmd => CompletionCandidate(cmd.name)))
145+
def complete(buffer: String, cursor: Int, filter: Boolean) =
146+
CompletionResult(buffer, cursor = 1, cmds.map(cmd => CompletionCandidate(":" + cmd.name)), "", "")
147147
}
148148
}
149149
case _ => NoCompletion

0 commit comments

Comments
 (0)