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