diff --git a/build.gradle b/build.gradle index 18974af8f..3e8c6814c 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() @@ -23,6 +23,7 @@ allprojects { jcenter() mavenLocal() mavenCentral() + maven { url '/home/ileasile/maven_repo/repo_1/maven' } // only when using Kotlin EAP releases ... maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' } maven { url 'https://dl.bintray.com/kotlin/kotlin-dev' } @@ -50,6 +51,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/src/main/kotlin/org/jetbrains/kotlin/jupyter/protocol.kt b/src/main/kotlin/org/jetbrains/kotlin/jupyter/protocol.kt index 9f30dbdf4..2f3e7f602 100644 --- a/src/main/kotlin/org/jetbrains/kotlin/jupyter/protocol.kt +++ b/src/main/kotlin/org/jetbrains/kotlin/jupyter/protocol.kt @@ -46,7 +46,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 +122,26 @@ fun JupyterConnection.Socket.shellMessagesHandler(msg: Message, repl: ReplForJup connection.iopub.send(makeReplyMessage(msg, "status", content = jsonObject("execution_state" to "idle"))) connection.contextMessage = null } + "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..f4ac99b65 100644 --- a/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl.kt +++ b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl.kt @@ -6,6 +6,12 @@ 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.building.ReplBuilding +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,6 +65,8 @@ class ReplForJupyter(val classpath: List = emptyList(), val config: Resolv } } + private val receiver = KotlinReceiver() + private val compilerConfiguration by lazy { ScriptCompilationConfiguration { hostConfiguration.update { it.withDefaultsFrom(defaultJvmScriptingHostConfiguration) } @@ -71,10 +79,18 @@ class ReplForJupyter(val classpath: List = emptyList(), val config: Resolv refineConfiguration { onAnnotations(DependsOn::class, Repository::class, handler = { configureMavenDepsOnAnnotations(it) }) } + + val kt = KotlinType(receiver.javaClass.canonicalName) + implicitReceivers.invoke(listOf(kt)) + + val receiverClassPath = receiver.javaClass.protectionDomain.codeSource.location.path + compilerOptions.invoke(listOf("-classpath", receiverClassPath)) } } - 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/building/KotlinReplProperties.kt b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/building/KotlinReplProperties.kt new file mode 100644 index 000000000..1e037fe67 --- /dev/null +++ b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/building/KotlinReplProperties.kt @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.jupyter.repl.building + +import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration +import java.io.File +import java.util.ArrayList +import java.util.Collections +import java.util.HashSet +import org.jetbrains.kotlin.jupyter.repl.context.KotlinReceiver + +/** + * Class that holds properties for Kotlin REPL creation, + * namely implicit receiver, classpath, preloaded code, directory for class bytecode output, + * max result limit and shortening types flag. + * + * Set its parameters by chaining corresponding methods, e.g. + * properties.outputDir(dir).shortenTypes(false) + * + * Get its parameters via getters. + */ +class KotlinReplProperties { + + val hostConf = defaultJvmScriptingHostConfiguration + + var receiver: KotlinReceiver? = null + private set + private val classpath: MutableSet + private val codeOnLoad: MutableList + var outputDir: String? = null + private set + var maxResult = 1000 + private set + var shortenTypes = true + private set + + init { + this.receiver = KotlinReceiver() + + this.classpath = HashSet() + val javaClasspath = System.getProperty("java.class.path").split(File.pathSeparator.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + Collections.addAll(classpath, *javaClasspath) + + this.codeOnLoad = ArrayList() + } + + fun receiver(receiver: KotlinReceiver): KotlinReplProperties { + this.receiver = receiver + return this + } + + fun classPath(path: String): KotlinReplProperties { + this.classpath.add(path) + return this + } + + fun classPath(paths: Collection): KotlinReplProperties { + this.classpath.addAll(paths) + return this + } + + fun codeOnLoad(code: String): KotlinReplProperties { + this.codeOnLoad.add(code) + return this + } + + fun codeOnLoad(code: Collection): KotlinReplProperties { + this.codeOnLoad.addAll(code) + return this + } + + fun outputDir(outputDir: String): KotlinReplProperties { + this.outputDir = outputDir + return this + } + + fun maxResult(maxResult: Int): KotlinReplProperties { + this.maxResult = maxResult + return this + } + + fun shortenTypes(shortenTypes: Boolean): KotlinReplProperties { + this.shortenTypes = shortenTypes + return this + } + + fun getClasspath(): Set { + return classpath + } + + fun getCodeOnLoad(): List { + return codeOnLoad + } +} diff --git a/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/building/ReplBuilding.kt b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/building/ReplBuilding.kt new file mode 100644 index 000000000..aa2dd22e6 --- /dev/null +++ b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/building/ReplBuilding.kt @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.jupyter.repl.building + +import org.jetbrains.kotlin.scripting.compiler.plugin.impl.KJvmReplCompilerImpl +import java.io.File +import java.util.StringJoiner +import kotlin.script.experimental.api.* +import kotlin.script.experimental.jvm.BasicJvmScriptEvaluator +import kotlin.script.experimental.jvm.dependenciesFromCurrentContext +import kotlin.script.experimental.jvm.jvm +import kotlin.script.experimental.host.withDefaultsFrom +import kotlin.script.experimental.jvmhost.repl.JvmReplCompiler +import kotlin.script.experimental.jvmhost.repl.JvmReplEvaluator + +/** + * Util class for building REPL components. + */ +object ReplBuilding { + fun buildCompiler(properties: KotlinReplProperties): JvmReplCompiler { + val receiverClassPath = properties.receiver!!.javaClass + .protectionDomain.codeSource.location.path + properties.classPath(receiverClassPath) + + val compilerImpl = KJvmReplCompilerImpl(properties.hostConf) + + return JvmReplCompiler( + buildCompilationConfiguration(properties), + properties.hostConf, + compilerImpl) + } + + fun buildEvaluator(properties: KotlinReplProperties): JvmReplEvaluator { + return JvmReplEvaluator( + buildEvaluationConfiguration(properties), + BasicJvmScriptEvaluator()) + } + + private fun buildClassPath(p: KotlinReplProperties): String { + val joiner = StringJoiner(File.pathSeparator) + for (path in p.getClasspath()) { + if (path != "") { + joiner.add(path) + } + } + return joiner.toString() + } + + private fun buildCompilationConfiguration( + p: KotlinReplProperties): ScriptCompilationConfiguration { + return ScriptCompilationConfiguration { + hostConfiguration.invoke(p.hostConf) + + val jvmBuilder = jvm + jvmBuilder.dependenciesFromCurrentContext(wholeClasspath = true, unpackJarCollections = false) + + val compilerOptions = listOf("-classpath", buildClassPath(p)) + + this.compilerOptions.invoke(compilerOptions) + + val kt = KotlinType(p.receiver!!.javaClass.canonicalName) + val receivers = listOf(kt) + implicitReceivers.invoke(receivers) + + Unit + } + } + + private fun buildEvaluationConfiguration( + p: KotlinReplProperties): ScriptEvaluationConfiguration { + return ScriptEvaluationConfiguration { + hostConfiguration.invoke(p.hostConf) + + val receivers = listOf(p.receiver) + implicitReceivers.invoke(receivers) + + Unit + } + } +} 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..92256a065 --- /dev/null +++ b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/completion/KotlinCompleter.kt @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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..d094d66f7 --- /dev/null +++ b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/completion/KotlinKeywords.kt @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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..b40b71bd6 --- /dev/null +++ b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/reflect/ContextUpdater.kt @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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..127e4ebde --- /dev/null +++ b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/reflect/KotlinFunctionInfo.kt @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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..15b8e918c --- /dev/null +++ b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/reflect/KotlinReflectUtil.kt @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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..d3f9e55b5 --- /dev/null +++ b/src/main/kotlin/org/jetbrains/kotlin/jupyter/repl/reflect/KotlinVariableInfo.kt @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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