diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..9d10e2b20 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,28 @@ +language: java + +sudo: false + +before_cache: + - sudo chown -R travis:travis $HOME/.m2 + +cache: + directories: + - ${HOME}/.m2 + +matrix: + include: + # All tests + - os: linux + sudo: false + jdk: "openjdk8" + dist: xenial + # env: ENV=... + +script: + - ./gradlew test + +after_success: + - echo "Travis exited with ${TRAVIS_TEST_RESULT}" + +after_failure: + - echo "Travis exited with ${TRAVIS_TEST_RESULT}" diff --git a/build.gradle b/build.gradle index 18974af8f..1a68e0842 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext.shadowJarVersion = "5.2.0" //ext.kotlinVersion = '1.3.60-dev-2180-83' - ext.kotlinVersion = '1.3-SNAPSHOT' + ext.kotlinVersion = '1.3.70-dev-1590' repositories { jcenter() mavenLocal() @@ -50,6 +50,7 @@ configurations { dependencies { compile project(":jupyter-lib") + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" compile "org.jetbrains.kotlin:kotlin-scripting-jvm-host-embeddable:$kotlinVersion" compile "org.jetbrains.kotlin:kotlin-scripting-common:$kotlinVersion" compile "org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:$kotlinVersion" diff --git a/readme.md b/readme.md index 1472c0eee..dacd2291c 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,5 @@ +[![Build Status](https://travis-ci.com/ileasile/kotlin-jupyter.svg?branch=master)](https://travis-ci.com/ileasile/kotlin-jupyter)
+ # Kotlin kernel for IPython/Jupyter Basic kotlin (1.3.40) REPL kernel for jupyter (http://jupyter.org). diff --git a/src/main/kotlin/org/jetbrains/kotlin/jupyter/protocol.kt b/src/main/kotlin/org/jetbrains/kotlin/jupyter/protocol.kt index 9f30dbdf4..7225587ba 100644 --- a/src/main/kotlin/org/jetbrains/kotlin/jupyter/protocol.kt +++ b/src/main/kotlin/org/jetbrains/kotlin/jupyter/protocol.kt @@ -29,7 +29,11 @@ fun JupyterConnection.Socket.shellMessagesHandler(msg: Message, repl: ReplForJup "protocol_version" to protocolVersion, "language" to "Kotlin", "language_version" to KotlinCompilerVersion.VERSION, - "language_info" to jsonObject("name" to "kotlin", "file_extension" to "kt") + "language_info" to jsonObject( + "name" to "kotlin", + "codemirror_mode" to "text/x-kotlin", + "file_extension" to "kt" + ) ))) "history_request" -> send(makeReplyMessage(msg, "history_reply", @@ -46,7 +50,7 @@ fun JupyterConnection.Socket.shellMessagesHandler(msg: Message, repl: ReplForJup .map { Pair("${it.name}_port", connection.config.ports[it.ordinal]) }))) "execute_request" -> { connection.contextMessage = msg - var count = executionCount.getAndIncrement() + val count = executionCount.getAndIncrement() val startedTime = ISO8601DateNow connection.iopub.send(makeReplyMessage(msg, "status", content = jsonObject("execution_state" to "busy"))) @@ -122,18 +126,29 @@ fun JupyterConnection.Socket.shellMessagesHandler(msg: Message, repl: ReplForJup connection.iopub.send(makeReplyMessage(msg, "status", content = jsonObject("execution_state" to "idle"))) connection.contextMessage = null } + "comm_info_request" -> { + send(makeReplyMessage(msg, "comm_info_reply", content = jsonObject("comms" to jsonObject()))) + } + "complete_request" -> { + val code = msg.content["code"].toString() + val cursor = msg.content["cursor_pos"] as Int + val result = repl?.complete(code, cursor)?.toJson() + if (result == null) { + System.err.println("Repl is not yet initialized on complete request") + return + } + send(makeReplyMessage(msg, "complete_reply", content = result)) + } "is_complete_request" -> { val code = msg.content["code"].toString() val resStr = if (isCommand(code)) "complete" else { val result = try { val check = repl?.checkComplete(executionCount.get(), code) - if (check == null) { - "error: no repl" - } else if (check.isComplete) { - "complete" - } else { - "incomplete" - } + when { + check == null -> "error: no repl" + check.isComplete -> "complete" + else -> "incomplete" + } } catch (ex: ReplCompilerException) { "invalid" } diff --git a/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl.kt b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl.kt index 5e70c76ac..5e940be33 100644 --- a/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl.kt +++ b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl.kt @@ -6,6 +6,11 @@ import jupyter.kotlin.ScriptTemplateWithDisplayHelpers import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation import org.jetbrains.kotlin.cli.common.repl.* import org.jetbrains.kotlin.config.KotlinCompilerVersion +import org.jetbrains.kotlin.jupyter.repl.completion.CompletionResult +import org.jetbrains.kotlin.jupyter.repl.completion.KotlinCompleter +import org.jetbrains.kotlin.jupyter.repl.context.KotlinContext +import org.jetbrains.kotlin.jupyter.repl.context.KotlinReceiver +import org.jetbrains.kotlin.jupyter.repl.reflect.ContextUpdater import java.io.File import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.script.dependencies.ScriptContents @@ -59,22 +64,33 @@ class ReplForJupyter(val classpath: List = emptyList(), val config: Resolv } } + private val receiver = KotlinReceiver() + private val compilerConfiguration by lazy { ScriptCompilationConfiguration { hostConfiguration.update { it.withDefaultsFrom(defaultJvmScriptingHostConfiguration) } baseClass.put(KotlinType(ScriptTemplateWithDisplayHelpers::class)) fileExtension.put("jupyter.kts") - defaultImports(DependsOn::class, Repository::class) + defaultImports(DependsOn::class, Repository::class, ScriptTemplateWithDisplayHelpers::class) jvm { updateClasspath(classpath) } refineConfiguration { onAnnotations(DependsOn::class, Repository::class, handler = { configureMavenDepsOnAnnotations(it) }) } + + val kt = KotlinType(receiver.javaClass.canonicalName) + implicitReceivers.invoke(listOf(kt)) + + val classes = listOf(receiver.javaClass, ScriptTemplateWithDisplayHelpers::class.java) + val classPath = classes.asSequence().map { it.protectionDomain.codeSource.location.path }.joinToString(":") + compilerOptions.invoke(listOf("-classpath", classPath)) } } - private val evaluatorConfiguration = ScriptEvaluationConfiguration { } + private val evaluatorConfiguration = ScriptEvaluationConfiguration { + implicitReceivers.invoke(receiver) + } private var executionCounter = 0 @@ -88,14 +104,19 @@ class ReplForJupyter(val classpath: List = emptyList(), val config: Resolv private val stateLock = ReentrantReadWriteLock() - private val state = compiler.createState(stateLock) - + private val compilerState = compiler.createState(stateLock) private val evaluatorState = evaluator.createState(stateLock) + private val state = AggregatedReplStageState(compilerState, evaluatorState, stateLock) + + private val ctx = KotlinContext() + private val contextUpdater = ContextUpdater(state, ctx.vars, ctx.functions) + + private val completer = KotlinCompleter(ctx) + fun checkComplete(executionNumber: Long, code: String): CheckResult { val codeLine = ReplCodeLine(executionNumber.toInt(), 0, code) - var result = compiler.check(state, codeLine) - return when(result) { + return when(val result = compiler.check(compilerState, codeLine)) { is ReplCheckResult.Error -> throw ReplCompilerException(result) is ReplCheckResult.Ok -> CheckResult(LineId(codeLine), true) is ReplCheckResult.Incomplete -> CheckResult(LineId(codeLine), false) @@ -130,31 +151,34 @@ class ReplForJupyter(val classpath: List = emptyList(), val config: Resolv return EvalResult(result, displays) } + + fun complete(code: String, cursor: Int): CompletionResult = completer.complete(code, cursor) + private fun doEval(code: String): Any? { - synchronized(this) { - val codeLine = ReplCodeLine(executionCounter++, 0, code) - val compileResult = compiler.compile(state, codeLine) - when (compileResult) { - is ReplCompileResult.CompiledClasses -> { - var result = evaluator.eval(evaluatorState, compileResult) - return when (result) { - is ReplEvalResult.Error.CompileTime -> throw ReplCompilerException(result) - is ReplEvalResult.Error.Runtime -> throw ReplEvalRuntimeException(result) - is ReplEvalResult.Incomplete -> throw ReplCompilerException(result) - is ReplEvalResult.HistoryMismatch -> throw ReplCompilerException(result) - is ReplEvalResult.UnitResult -> { - Unit - } - is ReplEvalResult.ValueResult -> { - result.value - } - else -> throw IllegalStateException("Unknown eval result type ${this}") + synchronized(this) { + val codeLine = ReplCodeLine(executionCounter++, 0, code) + when (val compileResult = compiler.compile(compilerState, codeLine)) { + is ReplCompileResult.CompiledClasses -> { + val result = evaluator.eval(evaluatorState, compileResult) + contextUpdater.update() + return when (result) { + is ReplEvalResult.Error.CompileTime -> throw ReplCompilerException(result) + is ReplEvalResult.Error.Runtime -> throw ReplEvalRuntimeException(result) + is ReplEvalResult.Incomplete -> throw ReplCompilerException(result) + is ReplEvalResult.HistoryMismatch -> throw ReplCompilerException(result) + is ReplEvalResult.UnitResult -> { + Unit + } + is ReplEvalResult.ValueResult -> { + result.value } + else -> throw IllegalStateException("Unknown eval result type ${this}") } - is ReplCompileResult.Error -> throw ReplCompilerException(compileResult) - is ReplCompileResult.Incomplete -> throw ReplCompilerException(compileResult) } + is ReplCompileResult.Error -> throw ReplCompilerException(compileResult) + is ReplCompileResult.Incomplete -> throw ReplCompilerException(compileResult) } + } } init { diff --git a/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/completion/KotlinCompleter.kt b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/completion/KotlinCompleter.kt new file mode 100644 index 000000000..0fb938081 --- /dev/null +++ b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/completion/KotlinCompleter.kt @@ -0,0 +1,109 @@ +package org.jetbrains.kotlin.jupyter.repl.completion + +import com.beust.klaxon.JsonObject +import org.jetbrains.kotlin.jupyter.jsonObject +import org.jetbrains.kotlin.jupyter.repl.context.KotlinContext +import org.jetbrains.kotlin.jupyter.repl.reflect.KotlinReflectUtil.shorten +import java.util.TreeMap +import java.io.PrintWriter +import java.io.StringWriter + +enum class CompletionStatus(private val value: String) { + OK("ok"), + ERROR("error"); + + override fun toString(): String { + return value + } +} + +abstract class CompletionResult( + val status: CompletionStatus +) { + open fun toJson(): JsonObject { + return jsonObject("status" to status.toString()) + } +} + +data class CompletionTokenBounds(val start: Int, val end: Int) + +class CompletionResultSuccess( + val matches: List, + val bounds: CompletionTokenBounds, + val metadata: Map +): CompletionResult(CompletionStatus.OK) { + override fun toJson(): JsonObject { + val res = super.toJson() + res["matches"] = matches + res["cursor_start"] = bounds.start + res["cursor_end"] = bounds.end + res["metadata"] = metadata + return res + } +} + +class CompletionResultError( + val errorName: String, + val errorValue: String, + val traceBack: String +): CompletionResult(CompletionStatus.ERROR) { + override fun toJson(): JsonObject { + val res = super.toJson() + res["ename"] = errorName + res["evalue"] = errorValue + res["traceback"] = traceBack + return res + } +} + + +class KotlinCompleter(private val ctx: KotlinContext) { + fun complete(buf: String, cursor: Int): CompletionResult { + try { + val bounds = getTokenBounds(buf, cursor) + val token = buf.substring(bounds.start, bounds.end) + + val tokens = TreeMap() + val tokensFilter = { t: String -> t.startsWith(token) } + tokens.putAll(keywords.filter { entry -> tokensFilter(entry.key) }) + + tokens.putAll(ctx.getVarsList().asSequence() + .filter { tokensFilter(it.name) } + .map { it.name to shorten(it.type) }) + + tokens.putAll(ctx.getFunctionsList().asSequence() + .filter { tokensFilter(it.name) } + .map { it.name to it.toString(true) }) + + return CompletionResultSuccess(tokens.keys.toList(), bounds, tokens) + } catch (e: Exception) { + val sw = StringWriter() + e.printStackTrace(PrintWriter(sw)) + return CompletionResultError(e.javaClass.simpleName, e.message ?: "", sw.toString()) + } + } + + companion object { + private val keywords = KotlinKeywords.KEYWORDS.asSequence().map { it to "keyword" }.toMap() + + fun getTokenBounds(buf: String, cursor: Int): CompletionTokenBounds { + require(cursor <= buf.length) { "Position $cursor does not exist in code snippet <$buf>" } + + val startSubstring = buf.substring(0, cursor) + val endSubstring = buf.substring(cursor) + + val filter = {c: Char -> !c.isLetterOrDigit()} + + val start = startSubstring.indexOfLast(filter) + 1 + var end = endSubstring.indexOfFirst(filter) + end = if (end == -1) { + buf.length + } else { + end + startSubstring.length + } + + return CompletionTokenBounds(start, end) + + } + } +} diff --git a/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/completion/KotlinKeywords.kt b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/completion/KotlinKeywords.kt new file mode 100644 index 000000000..7a4954f60 --- /dev/null +++ b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/completion/KotlinKeywords.kt @@ -0,0 +1,84 @@ +package org.jetbrains.kotlin.jupyter.repl.completion + +object KotlinKeywords { + /** + * List of Kotlin keywords for completion. + */ + val KEYWORDS: List = listOf( + "as", + "as?", + "break", + "class", + "continue", + "do", + "else", + "false", + "for", + "fun", + "if", + "in", + "interface", + "is", + "null", + "object", + "package", + "return", + "super", + "this", + "throw", + "true", + "try", + "typealias", + "typeof", + "val", + "var", + "when", + "while", + "by", + "catch", + "constructor", + "delegate", + "dynamic", + "field", + "file", + "finally", + "get", + "import", + "init", + "param", + "property", + "receiver", + "set", + "setparam", + "where", + "actual", + "abstract", + "annotation", + "companion", + "const", + "crossinline", + "data", + "enum", + "expect", + "external", + "final", + "infix", + "inline", + "inner", + "internal", + "lateinit", + "noinline", + "open", + "operator", + "out", + "override", + "private", + "protected", + "public", + "reified", + "sealed", + "suspend", + "tailrec", + "vararg" + ) +} diff --git a/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/context/KotlinContext.kt b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/context/KotlinContext.kt new file mode 100644 index 000000000..498069acf --- /dev/null +++ b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/context/KotlinContext.kt @@ -0,0 +1,41 @@ +package org.jetbrains.kotlin.jupyter.repl.context + +import org.jetbrains.kotlin.jupyter.repl.reflect.KotlinFunctionInfo +import org.jetbrains.kotlin.jupyter.repl.reflect.KotlinVariableInfo + +import java.util.* + +/** + * Kotlin REPL has built-in context for getting user-declared functions and variables + * and setting invokeWrapper for additional side effects in evaluation. + * It can be accessed inside REPL by name `kc`, e.g. kc.showVars() + */ +class KotlinContext(val vars: HashMap = HashMap(), + val functions: MutableSet = TreeSet()) { + + fun getVarsList(): List { + return ArrayList(vars.values) + } + + fun getFunctionsList(): List { + return ArrayList(functions) + } +} + + + +/** + * The implicit receiver for lines in Kotlin REPL. + * It is passed to the script as an implicit receiver, identical to: + * with (context) { + * ... + * } + * + * KotlinReceiver can be inherited from and passed to REPL building properties, + * so other variables and functions can be accessed inside REPL. + * By default, it only has KotlinContext. + * Inherited KotlinReceivers should be in separate java file, they can't be inner or nested. + */ +class KotlinReceiver { + var kc: KotlinContext? = null +} diff --git a/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/reflect/ContextUpdater.kt b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/reflect/ContextUpdater.kt new file mode 100644 index 000000000..127ec67f3 --- /dev/null +++ b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/reflect/ContextUpdater.kt @@ -0,0 +1,123 @@ +package org.jetbrains.kotlin.jupyter.repl.reflect + +import org.jetbrains.kotlin.cli.common.repl.AggregatedReplStageState +import org.jetbrains.kotlin.cli.common.repl.ReplHistoryRecord +import org.slf4j.LoggerFactory + +import java.lang.reflect.Field +import java.util.* +import java.util.stream.Collectors +import kotlin.reflect.jvm.kotlinFunction +import kotlin.reflect.jvm.kotlinProperty + +/** + * ContextUpdater updates current user-defined functions and variables + * to use in completion and KotlinContext. + */ +class ContextUpdater(private val state: AggregatedReplStageState<*, *>, + private val vars: MutableMap, + private val functions: MutableSet) { + + private val lines: List + get() { + val lines = state.history.stream() + .map{ this.getLineFromRecord(it) } + .collect(Collectors.toList()) + + lines.reverse() + return lines.toList() + } + + fun update() { + try { + val lines = lines + refreshVariables(lines) + refreshMethods(lines) + } catch (e: ReflectiveOperationException) { + logger.error("Exception updating current variables", e) + } catch (e: NullPointerException) { + logger.error("Exception updating current variables", e) + } + + } + + private fun refreshMethods(lines: List) { + functions.clear() + for (line in lines) { + val methods = line.javaClass.methods + for (method in methods) { + if (objectMethods.contains(method) || method.name == "main") { + continue + } + val function = method.kotlinFunction ?: continue + functions.add(KotlinFunctionInfo(function)) + } + } + } + + private fun getLineFromRecord(record: ReplHistoryRecord>): Any { + val statePair = record.item.second + return (statePair as Pair<*, *>).second!! + } + + @Throws(ReflectiveOperationException::class) + private fun getImplicitReceiver(script: Any): Any { + val receiverField = script.javaClass.getDeclaredField("\$\$implicitReceiver0") + return receiverField.get(script) + } + + @Throws(ReflectiveOperationException::class) + private fun refreshVariables(lines: List) { + vars.clear() + if (lines.isNotEmpty()) { + val receiver = getImplicitReceiver(lines[0]) + findReceiverVariables(receiver) + } + for (line in lines) { + findLineVariables(line) + } + } + + // For lines, we only want fields from top level class + @Throws(IllegalAccessException::class) + private fun findLineVariables(line: Any) { + val fields = line.javaClass.declaredFields + findVariables(fields, line) + } + + // For implicit receiver, we want to also get fields in parent classes + @Throws(IllegalAccessException::class) + private fun findReceiverVariables(receiver: Any) { + val fieldsList = ArrayList() + var cl: Class<*>? = receiver.javaClass + while (cl != null) { + fieldsList.addAll(listOf(*cl.declaredFields)) + cl = cl.superclass + } + findVariables(fieldsList.toTypedArray(), receiver) + } + + @Throws(IllegalAccessException::class) + private fun findVariables(fields: Array, o: Any) { + for (field in fields) { + val fieldName = field.name + if (fieldName.contains("$\$implicitReceiver")) { + continue + } + + field.isAccessible = true + val value = field.get(o) + if (!fieldName.contains("script$")) { + val descriptor = field.kotlinProperty + if (descriptor != null) { + vars.putIfAbsent(fieldName, KotlinVariableInfo(value, descriptor)) + } + } + } + } + + companion object { + private val logger = LoggerFactory.getLogger(ContextUpdater::class.java) + private val objectMethods = HashSet(listOf(*Any::class.java.methods)) + } +} diff --git a/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/reflect/KotlinFunctionInfo.kt b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/reflect/KotlinFunctionInfo.kt new file mode 100644 index 000000000..9d5b148d2 --- /dev/null +++ b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/reflect/KotlinFunctionInfo.kt @@ -0,0 +1,37 @@ +package org.jetbrains.kotlin.jupyter.repl.reflect + +import kotlin.reflect.KFunction + +import org.jetbrains.kotlin.jupyter.repl.reflect.KotlinReflectUtil.functionSignature +import org.jetbrains.kotlin.jupyter.repl.reflect.KotlinReflectUtil.shorten + + +class KotlinFunctionInfo(private val function: KFunction<*>) : Comparable { + + val name: String + get() = function.name + + fun toString(shortenTypes: Boolean): String { + return if (shortenTypes) { + shorten(toString()) + } else toString() + } + + override fun toString(): String { + return functionSignature(function) + } + + override fun compareTo(other: KotlinFunctionInfo): Int { + return this.toString().compareTo(other.toString()) + } + + override fun hashCode(): Int { + return this.toString().hashCode() + } + + override fun equals(other: Any?): Boolean { + return if (other is KotlinFunctionInfo) { + this.toString() == other.toString() + } else false + } +} diff --git a/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/reflect/KotlinReflectUtil.kt b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/reflect/KotlinReflectUtil.kt new file mode 100644 index 000000000..9e76b90d0 --- /dev/null +++ b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/reflect/KotlinReflectUtil.kt @@ -0,0 +1,17 @@ +package org.jetbrains.kotlin.jupyter.repl.reflect + +import kotlin.reflect.KFunction + +/** + * Util class for pretty-printing Kotlin variables and functions. + */ +object KotlinReflectUtil { + fun functionSignature(function: KFunction<*>): String { + return function.toString().replace("Line_\\d+\\.".toRegex(), "") + } + + fun shorten(name: String): String { + return name.replace("(\\b[_a-zA-Z$][_a-zA-Z0-9$]*\\b\\.)+".toRegex(), "") + // kotlin.collections.List -> List + } +} diff --git a/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/reflect/KotlinVariableInfo.kt b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/reflect/KotlinVariableInfo.kt new file mode 100644 index 000000000..d14c64090 --- /dev/null +++ b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/reflect/KotlinVariableInfo.kt @@ -0,0 +1,26 @@ +package org.jetbrains.kotlin.jupyter.repl.reflect + +import kotlin.reflect.KProperty + +import org.jetbrains.kotlin.jupyter.repl.reflect.KotlinReflectUtil.shorten + +class KotlinVariableInfo(private val value: Any?, private val descriptor: KProperty<*>) { + + val name: String + get() = descriptor.name + + val type: String + get() = descriptor.returnType.toString() + + fun toString(shortenTypes: Boolean): String { + var type: String = type + if (shortenTypes) { + type = shorten(type) + } + return "$name: $type = $value" + } + + override fun toString(): String { + return toString(false) + } +} diff --git a/src/test/kotlin/org/jetbrains/kotlin/jupyter/test/replTests.kt b/src/test/kotlin/org/jetbrains/kotlin/jupyter/test/replTests.kt index 9cd8e5c39..131562efd 100644 --- a/src/test/kotlin/org/jetbrains/kotlin/jupyter/test/replTests.kt +++ b/src/test/kotlin/org/jetbrains/kotlin/jupyter/test/replTests.kt @@ -1,6 +1,7 @@ package org.jetbrains.kotlin.jupyter.test import org.jetbrains.kotlin.jupyter.ReplForJupyter +import org.jetbrains.kotlin.jupyter.repl.completion.CompletionResultSuccess import org.junit.Assert import org.junit.Before import org.junit.Test @@ -40,4 +41,17 @@ class ReplTest{ repl.eval("@file:DependsOn(\"klaxon\")") repl.eval("val k = Klaxon()") } + + @Test + fun TestCompletion() { + repl.eval("val foobar = 42") + repl.eval("var foobaz = 43") + val result = repl.complete("val t = foo", 11) + + if (result is CompletionResultSuccess) { + Assert.assertEquals(arrayListOf("foobar", "foobaz"), result.matches.sorted()) + } else { + Assert.fail("Result should be success") + } + } } \ No newline at end of file