Skip to content

Commit 78d6078

Browse files
committed
Provide completion and errors analysis for commands
Fixes #90
1 parent dcdf4bd commit 78d6078

File tree

5 files changed

+102
-15
lines changed

5 files changed

+102
-15
lines changed

src/main/kotlin/org/jetbrains/kotlin/jupyter/commands.kt

+41-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
package org.jetbrains.kotlin.jupyter
22

33
import jupyter.kotlin.textResult
4+
import org.jetbrains.kotlin.jupyter.repl.completion.CompletionResult
5+
import org.jetbrains.kotlin.jupyter.repl.completion.KotlinCompleter
6+
import org.jetbrains.kotlin.jupyter.repl.completion.ListErrorsResult
7+
import org.jetbrains.kotlin.jupyter.repl.completion.SourceCodeImpl
8+
import kotlin.script.experimental.api.ScriptDiagnostic
9+
import kotlin.script.experimental.api.SourceCode
10+
import kotlin.script.experimental.api.SourceCodeCompletionVariant
11+
import kotlin.script.experimental.jvm.util.toSourceCodePosition
412

513
enum class ReplCommands(val desc: String) {
614
help("display help"),
@@ -9,17 +17,43 @@ enum class ReplCommands(val desc: String) {
917

1018
fun isCommand(code: String): Boolean = code.startsWith(":")
1119

20+
fun getCommand(string: String): ReplCommands? {
21+
return try {
22+
ReplCommands.valueOf(string)
23+
} catch (e: IllegalArgumentException) {
24+
null
25+
}
26+
}
27+
1228
fun <T> Iterable<T>.joinToStringIndented(transform: ((T) -> CharSequence)? = null) = joinToString("\n ", prefix = " ", transform = transform)
1329

30+
fun reportCommandErrors(code: String): ListErrorsResult {
31+
val commandString = code.trim().substring(1)
32+
val command = getCommand(commandString)
33+
if (command != null) return ListErrorsResult(code)
34+
35+
val sourceCode = SourceCodeImpl(0, code)
36+
val location = SourceCode.Location(
37+
0.toSourceCodePosition(sourceCode),
38+
(commandString.length + 1).toSourceCodePosition(sourceCode)
39+
)
40+
return ListErrorsResult(code, sequenceOf(
41+
ScriptDiagnostic(ScriptDiagnostic.unspecifiedError, "Unknown command", location = location)
42+
))
43+
}
44+
45+
fun doCommandCompletion(code: String, cursor: Int): CompletionResult {
46+
val prefix = code.substring(1, cursor)
47+
val suitableCommands = ReplCommands.values().filter { it.name.startsWith(prefix) }
48+
val completions = suitableCommands.map {
49+
SourceCodeCompletionVariant(it.name, it.name, "command", "command")
50+
}
51+
return KotlinCompleter.getResult(code, cursor, completions)
52+
}
53+
1454
fun runCommand(code: String, repl: ReplForJupyter?): Response {
1555
val args = code.trim().substring(1).split(" ")
16-
val cmd =
17-
try {
18-
ReplCommands.valueOf(args[0])
19-
}
20-
catch (e: IllegalArgumentException) {
21-
return AbortResponseWithMessage(textResult("Failed!"), "unknown command: $code\nto see available commands, enter :help")
22-
}
56+
val cmd = getCommand(args[0]) ?: return AbortResponseWithMessage(textResult("Failed!"), "unknown command: $code\nto see available commands, enter :help")
2357
return when (cmd) {
2458
ReplCommands.classpath -> {
2559
val cp = repl!!.currentClasspath

src/main/kotlin/org/jetbrains/kotlin/jupyter/repl.kt

+16-6
Original file line numberDiff line numberDiff line change
@@ -444,17 +444,27 @@ class ReplForJupyterImpl(private val scriptClasspath: List<File> = emptyList(),
444444
}
445445

446446
private val completionQueue = LockQueue<CompletionResult, CompletionArgs>()
447-
override suspend fun complete(code: String, cursor: Int, callback: (CompletionResult) -> Unit) = doWithLock(CompletionArgs(code, cursor, callback), completionQueue, CompletionResult.Empty(code, cursor)) {
448-
val preprocessed = magics.processMagics(code, true).code
449-
completer.complete(compiler, compilerConfiguration, code, preprocessed, executionCounter++, cursor)
447+
override suspend fun complete(code: String, cursor: Int, callback: (CompletionResult) -> Unit) =
448+
doWithLock(CompletionArgs(code, cursor, callback), completionQueue, CompletionResult.Empty(code, cursor), ::doComplete)
449+
450+
private fun doComplete(args: CompletionArgs): CompletionResult {
451+
if (isCommand(args.code)) return doCommandCompletion(args.code, args.cursor)
452+
453+
val preprocessed = magics.processMagics(args.code, true).code
454+
return completer.complete(compiler, compilerConfiguration, args.code, preprocessed, executionCounter++, args.cursor)
450455
}
451456

452457
private val listErrorsQueue = LockQueue<ListErrorsResult, ListErrorsArgs>()
453-
override suspend fun listErrors(code: String, callback: (ListErrorsResult) -> Unit) = doWithLock(ListErrorsArgs(code, callback), listErrorsQueue, ListErrorsResult(code)) {
454-
val preprocessed = magics.processMagics(code, true).code
458+
override suspend fun listErrors(code: String, callback: (ListErrorsResult) -> Unit) =
459+
doWithLock(ListErrorsArgs(code, callback), listErrorsQueue, ListErrorsResult(code), ::doListErrors)
460+
461+
private fun doListErrors(args: ListErrorsArgs): ListErrorsResult {
462+
if (isCommand(args.code)) return reportCommandErrors(args.code)
463+
464+
val preprocessed = magics.processMagics(args.code, true).code
455465
val codeLine = SourceCodeImpl(executionCounter++, preprocessed)
456466
val errorsList = runBlocking { compiler.analyze(codeLine, 0.toSourceCodePosition(codeLine), compilerConfiguration) }
457-
ListErrorsResult(code, errorsList.valueOrThrow()[ReplAnalyzerResult.analysisDiagnostics]!!)
467+
return ListErrorsResult(args.code, errorsList.valueOrThrow()[ReplAnalyzerResult.analysisDiagnostics]!!)
458468
}
459469

460470
private fun <T, Args: LockQueueArgs<T>> doWithLock(args: Args, queue: LockQueue<T, Args>, default: T, action: (Args) -> T) {

src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/completion/KotlinCompleter.kt

+6-2
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,7 @@ class KotlinCompleter {
129129
val completionResult = codePos?.let { runBlocking { compiler.complete(preprocessedCodeLine, codePos, configuration) } }
130130

131131
completionResult?.valueOrNull()?.toList()?.let { completionList ->
132-
val bounds = getTokenBounds(code, cursor)
133-
CompletionResult.Success(completionList.map { it.text }, bounds, completionList, code, cursor)
132+
getResult(code, cursor, completionList)
134133
} ?: CompletionResult.Empty(code, cursor)
135134

136135
} catch (e: Exception) {
@@ -141,6 +140,11 @@ class KotlinCompleter {
141140
}
142141

143142
companion object {
143+
fun getResult(code: String, cursor: Int, completions: List<SourceCodeCompletionVariant>): CompletionResult.Success {
144+
val bounds = getTokenBounds(code, cursor)
145+
return CompletionResult.Success(completions.map { it.text }, bounds, completions, code, cursor)
146+
}
147+
144148
private fun getTokenBounds(buf: String, cursor: Int): CompletionTokenBounds {
145149
require(cursor <= buf.length) { "Position $cursor does not exist in code snippet <$buf>" }
146150

src/main/kotlin/org/jetbrains/kotlin/jupyter/util.kt

+12
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import kotlinx.coroutines.GlobalScope
55
import kotlinx.coroutines.async
66
import kotlinx.coroutines.runBlocking
77
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocationWithRange
8+
import org.jetbrains.kotlin.jupyter.repl.completion.SourceCodeImpl
89
import org.slf4j.Logger
910
import java.io.File
1011
import kotlin.script.experimental.api.ResultWithDiagnostics
@@ -54,6 +55,17 @@ fun generateDiagnostic(fromLine: Int, fromCol: Int, toLine: Int, toCol: Int, mes
5455
SourceCode.Location(SourceCode.Position(fromLine, fromCol), SourceCode.Position(toLine, toCol))
5556
)
5657

58+
fun generateDiagnosticFromAbsolute(code: String, from: Int, to: Int, message: String, severity: String): ScriptDiagnostic {
59+
val snippet = SourceCodeImpl(0, code)
60+
return ScriptDiagnostic(
61+
ScriptDiagnostic.unspecifiedError,
62+
message,
63+
ScriptDiagnostic.Severity.valueOf(severity),
64+
null,
65+
SourceCode.Location(from.toSourceCodePosition(snippet), to.toSourceCodePosition(snippet))
66+
)
67+
}
68+
5769
fun withPath(path: String?, diagnostics: List<ScriptDiagnostic>): List<ScriptDiagnostic> =
5870
diagnostics.map { it.copy(sourcePath = path) }
5971

src/test/kotlin/org/jetbrains/kotlin/jupyter/test/replTests.kt

+27
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import org.jetbrains.kotlin.jupyter.ReplForJupyterImpl
1212
import org.jetbrains.kotlin.jupyter.ResolverConfig
1313
import org.jetbrains.kotlin.jupyter.defaultRepositories
1414
import org.jetbrains.kotlin.jupyter.generateDiagnostic
15+
import org.jetbrains.kotlin.jupyter.generateDiagnosticFromAbsolute
1516
import org.jetbrains.kotlin.jupyter.repl.completion.CompletionResult
1617
import org.jetbrains.kotlin.jupyter.repl.completion.ListErrorsResult
1718
import org.jetbrains.kotlin.jupyter.runtimeProperties
@@ -270,6 +271,32 @@ class ReplTest : AbstractReplTest() {
270271
}
271272
}
272273

274+
@Test
275+
fun testCommands() {
276+
val code1 = ":help"
277+
val code2 = ":hex "
278+
279+
runBlocking {
280+
repl.listErrors(code1) { result ->
281+
assertEquals(code1, result.code)
282+
assertEquals(0, result.errors.toList().size)
283+
}
284+
repl.listErrors(code2) { result ->
285+
assertEquals(code2, result.code)
286+
val expectedList = listOf(generateDiagnosticFromAbsolute(code2, 0, 4, "Unknown command", "ERROR"))
287+
val actualList = result.errors.toList()
288+
assertEquals(expectedList, actualList)
289+
}
290+
repl.complete(code2, 3) { result ->
291+
if (result is CompletionResult.Success) {
292+
assertEquals(listOf("help"), result.sortedMatches())
293+
} else {
294+
fail("Result should be success")
295+
}
296+
}
297+
}
298+
}
299+
273300
@Test
274301
fun testEmptyErrorsListJson() {
275302
val res = ListErrorsResult("someCode")

0 commit comments

Comments
 (0)