diff --git a/package.json b/package.json index 3a0e29e57..03c94d151 100644 --- a/package.json +++ b/package.json @@ -63,15 +63,32 @@ "default": true, "description": "Specifies whether the language server should be used. When enabled the extension will provide code completions and linting, otherwise just syntax highlighting. Might require a reload to apply." }, - "kotlin.debounceTime": { + "kotlin.compiler.jvm.target": { + "type": "string", + "default": "default", + "description": "Specifies the JVM target, e.g. \"1.6\" or \"1.8\"" + }, + "kotlin.linting.debounceTime": { "type": "integer", "default": 250, "description": "[DEBUG] Specifies the debounce time limit. Lower to increase responsiveness at the cost of possibile stability issues" }, - "kotlin.snippetsEnabled": { + "kotlin.completion.snippets.enabled": { "type": "boolean", "default": true, "description": "Specifies whether code completion should provide snippets (true) or plain-text items (false)" + }, + "kotlin.debounceTime": { + "type": "integer", + "default": 250, + "description": "[DEPRECATED] Specifies the debounce time limit. Lower to increase responsiveness at the cost of possibile stability issues", + "deprecationMessage": "Use 'kotlin.linting.debounceTime' instead" + }, + "kotlin.snippetsEnabled": { + "type": "boolean", + "default": true, + "description": "[DEPRECATED] Specifies whether code completion should provide snippets (true) or plain-text items (false)", + "deprecationMessage": "Use 'kotlin.completion.snippets.enabled'" } } } diff --git a/server/src/main/kotlin/org/javacs/kt/Compiler.kt b/server/src/main/kotlin/org/javacs/kt/Compiler.kt index 0f223df85..fd90e8bf6 100644 --- a/server/src/main/kotlin/org/javacs/kt/Compiler.kt +++ b/server/src/main/kotlin/org/javacs/kt/Compiler.kt @@ -4,6 +4,7 @@ import com.intellij.codeInsight.NullableNotNullManager import com.intellij.openapi.Disposable import com.intellij.openapi.vfs.StandardFileSystems import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.openapi.vfs.VirtualFileSystem import com.intellij.psi.PsiFileFactory import com.intellij.mock.MockProject import org.jetbrains.kotlin.cli.common.script.CliScriptDefinitionProvider @@ -14,8 +15,9 @@ import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots import org.jetbrains.kotlin.config.CommonConfigurationKeys -import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.config.CompilerConfiguration as KotlinCompilerConfiguration import org.jetbrains.kotlin.config.JVMConfigurationKeys +import org.jetbrains.kotlin.config.JvmTarget import org.jetbrains.kotlin.container.ComponentProvider import org.jetbrains.kotlin.container.get import org.jetbrains.kotlin.idea.KotlinLanguage @@ -46,32 +48,61 @@ import org.javacs.kt.util.LoggingMessageCollector * The basic strategy for compiling one file at-a-time is outlined in OneFilePerformance. */ class Compiler(classPath: Set) { - private val config = CompilerConfiguration().apply { - put(CommonConfigurationKeys.MODULE_NAME, JvmAbi.DEFAULT_MODULE_NAME) - put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, LoggingMessageCollector) - addJvmClasspathRoots(classPath.map { it.toFile() }) - } val environment: KotlinCoreEnvironment + private var parser: KtPsiFactory + private var scripts: CliScriptDefinitionProvider + private val localFileSystem: VirtualFileSystem + + companion object { + init { + System.setProperty("idea.io.use.fallback", "true") + } + } + init { - System.setProperty("idea.io.use.fallback", "true") environment = KotlinCoreEnvironment.createForProduction( - parentDisposable = Disposable { }, - configuration = config, + parentDisposable = Disposable {}, + // Not to be confused with the CompilerConfiguration in the language server Configuration + configuration = KotlinCompilerConfiguration().apply { + put(CommonConfigurationKeys.MODULE_NAME, JvmAbi.DEFAULT_MODULE_NAME) + put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, LoggingMessageCollector) + addJvmClasspathRoots(classPath.map { it.toFile() }) + }, configFiles = EnvironmentConfigFiles.JVM_CONFIG_FILES ) + val project = environment.project if (project is MockProject) { project.registerService(NullableNotNullManager::class.java, KotlinNullableNotNullManager(project)) } + + parser = KtPsiFactory(environment.project) + scripts = ScriptDefinitionProvider.getInstance(environment.project) as CliScriptDefinitionProvider + scripts.setScriptDefinitions(listOf(KotlinScriptDefinition(Any::class))) + localFileSystem = VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL) } - private val parser = KtPsiFactory(environment.project) - private val localFileSystem = VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL) - private val scripts = ScriptDefinitionProvider.getInstance(environment.project) as CliScriptDefinitionProvider + /** + * Updates the compiler environment using the given + * configuration (which is a class from this project). + */ + fun updateConfiguration(config: CompilerConfiguration) { + jvmTargetFrom(config.jvm.target) + ?.let { environment.configuration.put(JVMConfigurationKeys.JVM_TARGET, it) } + } - init { - scripts.setScriptDefinitions(listOf(KotlinScriptDefinition(Any::class))) + private fun jvmTargetFrom(target: String): JvmTarget? = when (target) { + // See https://github.com/JetBrains/kotlin/blob/master/compiler/frontend.java/src/org/jetbrains/kotlin/config/JvmTarget.kt + "default" -> JvmTarget.DEFAULT + "1.6" -> JvmTarget.JVM_1_6 + "1.8" -> JvmTarget.JVM_1_8 + // TODO: Add once Java 9+ is supported + // "9" -> JvmTarget.JVM_9 + // "10" -> JvmTarget.JVM_10 + // "11" -> JvmTarget.JVM_11 + // "12" -> JvmTarget.JVM_12 + else -> null } fun createFile(content: String, file: Path = Paths.get("dummy.kt")): KtFile { diff --git a/server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt b/server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt index 952c4182b..a5665b552 100644 --- a/server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt +++ b/server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt @@ -3,12 +3,16 @@ package org.javacs.kt import org.javacs.kt.classpath.findClassPath import java.nio.file.Path -class CompilerClassPath { +class CompilerClassPath(private val config: CompilerConfiguration) { private val workspaceRoots = mutableSetOf() private val classPath = mutableSetOf() var compiler = Compiler(classPath) private set + init { + compiler.updateConfiguration(config) + } + private fun refresh() { val newClassPath = findClassPath(workspaceRoots) @@ -22,9 +26,14 @@ class CompilerClassPath { classPath.removeAll(removed) classPath.addAll(added) compiler = Compiler(classPath) + updateCompilerConfiguration() } } + fun updateCompilerConfiguration() { + compiler.updateConfiguration(config) + } + fun addWorkspaceRoot(root: Path) { LOG.info("Searching for dependencies in workspace root {}", root) diff --git a/server/src/main/kotlin/org/javacs/kt/Configuration.kt b/server/src/main/kotlin/org/javacs/kt/Configuration.kt index b1cc38573..7b25dfb76 100644 --- a/server/src/main/kotlin/org/javacs/kt/Configuration.kt +++ b/server/src/main/kotlin/org/javacs/kt/Configuration.kt @@ -1,6 +1,27 @@ package org.javacs.kt -public class Configuration( - var debounceTime: Long = 250L, - var snippetsEnabled: Boolean = true +public data class SnippetsConfiguration( + var enabled: Boolean = true +) + +public data class CompletionConfiguration( + val snippets: SnippetsConfiguration = SnippetsConfiguration() +) + +public data class LintingConfiguration( + var debounceTime: Long = 250L +) + +public data class JVMConfiguration( + var target: String = "default" // See Compiler.jvmTargetFrom for possible values +) + +public data class CompilerConfiguration( + val jvm: JVMConfiguration = JVMConfiguration() +) + +public data class Configuration( + val compiler: CompilerConfiguration = CompilerConfiguration(), + val completion: CompletionConfiguration = CompletionConfiguration(), + val linting: LintingConfiguration = LintingConfiguration() ) diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt b/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt index f4dcc21c8..6e81d6936 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt @@ -12,10 +12,10 @@ import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture.completedFuture class KotlinLanguageServer : LanguageServer, LanguageClientAware { - val classPath = CompilerClassPath() + private val config = Configuration() + val classPath = CompilerClassPath(config.compiler) val sourcePath = SourcePath(classPath) val sourceFiles = SourceFiles(sourcePath) - private val config = Configuration() private val textDocuments = KotlinTextDocumentService(sourceFiles, sourcePath, config) private val workspaces = KotlinWorkspaceService(sourceFiles, sourcePath, classPath, textDocuments, config) @@ -57,7 +57,7 @@ class KotlinLanguageServer : LanguageServer, LanguageClientAware { serverCapabilities.executeCommandProvider = ExecuteCommandOptions(ALL_COMMANDS) val clientCapabilities = params.capabilities - config.snippetsEnabled = clientCapabilities?.textDocument?.completion?.completionItem?.snippetSupport ?: false + config.completion.snippets.enabled = clientCapabilities?.textDocument?.completion?.completionItem?.snippetSupport ?: false if (params.rootUri != null) { LOG.info("Adding workspace {} to source path", params.rootUri) diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt b/server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt index fcb99a1a2..f8f0134f8 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt @@ -32,7 +32,7 @@ class KotlinTextDocumentService( private val async = AsyncExecutor() private var linting = false - var debounceLint = Debouncer(Duration.ofMillis(config.debounceTime)) + var debounceLint = Debouncer(Duration.ofMillis(config.linting.debounceTime)) val lintTodo = mutableSetOf() var lintCount = 0 @@ -115,7 +115,7 @@ class KotlinTextDocumentService( LOG.info("Completing at {}", describePosition(position)) val (file, cursor) = recover(position, false) - val completions = completions(file, cursor, config.snippetsEnabled) + val completions = completions(file, cursor, config.completion) LOG.info("Found {} items", completions.items.size) @@ -190,7 +190,7 @@ class KotlinTextDocumentService( } public fun updateDebouncer() { - debounceLint = Debouncer(Duration.ofMillis(config.debounceTime)) + debounceLint = Debouncer(Duration.ofMillis(config.linting.debounceTime)) } private fun clearLint(): List { diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt b/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt index 3e6323450..3dd7a9c19 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt @@ -81,12 +81,45 @@ class KotlinWorkspaceService( override fun didChangeConfiguration(params: DidChangeConfigurationParams) { val settings = params.settings as? JsonObject settings?.get("kotlin")?.asJsonObject?.apply { - get("debounceTime")?.asLong?.let { config.debounceTime = it } - get("snippetsEnabled")?.asBoolean?.let { config.snippetsEnabled = it } + // Update deprecated configuration keys + get("debounceTime")?.asLong?.let { + config.linting.debounceTime = it + docService.updateDebouncer() + } + get("snippetsEnabled")?.asBoolean?.let { config.completion.snippets.enabled = it } + + // Update compiler options + get("compiler")?.asJsonObject?.apply { + val compiler = config.compiler + get("jvm")?.asJsonObject?.apply { + val jvm = compiler.jvm + get("target")?.asString?.let { + jvm.target = it + cp.updateCompilerConfiguration() + } + } + } + + // Update linter options + get("linting")?.asJsonObject?.apply { + val linting = config.linting + get("debounceTime")?.asLong?.let { + linting.debounceTime = it + docService.updateDebouncer() + } + } + + // Update code-completion options + get("completion")?.asJsonObject?.apply { + val completion = config.completion + get("snippets")?.asJsonObject?.apply { + val snippets = completion.snippets + get("enabled")?.asBoolean?.let { snippets.enabled = it } + } + } } - - docService.updateDebouncer() - LOG.info("configurations updated {}", settings) + + LOG.info("Updated configurations: {}", settings) } override fun symbol(params: WorkspaceSymbolParams): CompletableFuture> { diff --git a/server/src/main/kotlin/org/javacs/kt/completion/Completions.kt b/server/src/main/kotlin/org/javacs/kt/completion/Completions.kt index f7573ef8a..04c32529b 100644 --- a/server/src/main/kotlin/org/javacs/kt/completion/Completions.kt +++ b/server/src/main/kotlin/org/javacs/kt/completion/Completions.kt @@ -5,6 +5,7 @@ import org.eclipse.lsp4j.CompletionItem import org.eclipse.lsp4j.CompletionList import org.javacs.kt.CompiledFile import org.javacs.kt.LOG +import org.javacs.kt.CompletionConfiguration import org.javacs.kt.util.findParent import org.javacs.kt.util.noResult import org.javacs.kt.util.toPath @@ -34,7 +35,7 @@ import java.util.concurrent.TimeUnit private const val MAX_COMPLETION_ITEMS = 50 -fun completions(file: CompiledFile, cursor: Int, snippetsEnabled: Boolean = true): CompletionList { +fun completions(file: CompiledFile, cursor: Int, config: CompletionConfiguration): CompletionList { val surroundingElement = completableElement(file, cursor) ?: return CompletionList(true, emptyList()) val completions = doCompletions(file, cursor, surroundingElement) val partial = findPartialIdentifier(file, cursor) @@ -42,7 +43,7 @@ fun completions(file: CompiledFile, cursor: Int, snippetsEnabled: Boolean = true val nameFilter = matchesPartialIdentifier(partial) val matchesName = completions.filter(nameFilter) val visible = matchesName.filter(isVisible(file, cursor)) - val list = visible.map { completionItem(it, surroundingElement, file, snippetsEnabled) }.take(MAX_COMPLETION_ITEMS).toList() + val list = visible.map { completionItem(it, surroundingElement, file, config) }.take(MAX_COMPLETION_ITEMS).toList() val isIncomplete = (list.size == MAX_COMPLETION_ITEMS) return CompletionList(isIncomplete, list) } @@ -50,8 +51,8 @@ fun completions(file: CompiledFile, cursor: Int, snippetsEnabled: Boolean = true private val callPattern = Regex("(.*)\\((\\$\\d+)?\\)") private val methodSignature = Regex("""(?:fun|constructor) (?:<(?:[a-zA-Z\?\!\: ]+)(?:, [A-Z])*> )?([a-zA-Z]+\(.*\))""") -private fun completionItem(d: DeclarationDescriptor, surroundingElement: KtElement, file: CompiledFile, snippetsEnabled: Boolean): CompletionItem { - val result = d.accept(RenderCompletionItem(snippetsEnabled), null) +private fun completionItem(d: DeclarationDescriptor, surroundingElement: KtElement, file: CompiledFile, config: CompletionConfiguration): CompletionItem { + val result = d.accept(RenderCompletionItem(config.snippets.enabled), null) result.label = methodSignature.find(result.detail)?.groupValues?.get(1) ?: result.label diff --git a/server/src/test/kotlin/org/javacs/kt/CompiledFileTest.kt b/server/src/test/kotlin/org/javacs/kt/CompiledFileTest.kt index 98a417161..dc2541c7a 100644 --- a/server/src/test/kotlin/org/javacs/kt/CompiledFileTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/CompiledFileTest.kt @@ -13,7 +13,7 @@ class CompiledFileTest { val file = testResourcesRoot().resolve("compiledFile/CompiledFileExample.kt") val content = Files.readAllLines(file).joinToString("\n") val parse = compiler.createFile(content, file) - val classPath = CompilerClassPath() + val classPath = CompilerClassPath(CompilerConfiguration()) val sourcePath = listOf(parse) val (context, container) = compiler.compileFiles(sourcePath, sourcePath) return CompiledFile(content, parse, context, container, sourcePath, classPath) @@ -24,4 +24,4 @@ class CompiledFileTest { assertThat(type.toString(), equalTo("Int")) } -} \ No newline at end of file +} diff --git a/server/src/test/kotlin/org/javacs/kt/SimpleScriptTest.kt b/server/src/test/kotlin/org/javacs/kt/SimpleScriptTest.kt index 96a91678c..7a54cee96 100644 --- a/server/src/test/kotlin/org/javacs/kt/SimpleScriptTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/SimpleScriptTest.kt @@ -2,7 +2,8 @@ package org.javacs.kt import org.jetbrains.kotlin.cli.common.repl.* import org.jetbrains.kotlin.cli.jvm.repl.* -import org.jetbrains.kotlin.config.* +import org.jetbrains.kotlin.config.CommonConfigurationKeys +import org.jetbrains.kotlin.config.CompilerConfiguration as KotlinCompilerConfiguration import org.jetbrains.kotlin.script.* import com.intellij.openapi.util.* import org.jetbrains.kotlin.cli.common.messages.* @@ -19,7 +20,7 @@ class SimpleScriptTest { fun basicScript() { val scriptDef = KotlinScriptDefinition(Any::class) val repl = GenericReplEvaluator(listOf()) - val config = CompilerConfiguration() + val config = KotlinCompilerConfiguration() config.put(CommonConfigurationKeys.MODULE_NAME, JvmAbi.DEFAULT_MODULE_NAME) val compiler = GenericReplCompiler(scriptDef, config, MessageCollector.NONE) val compilerState = compiler.createState() @@ -38,7 +39,7 @@ class SimpleScriptTest { is ReplEvalResult.UnitResult -> println('_') } } - + private fun assertNotError(result: ReplCompileResult) { if (result is ReplCompileResult.Error) { fail(result.message) diff --git a/vscode-extension-src/extension.ts b/vscode-extension-src/extension.ts index 2e91413d2..e7367a818 100644 --- a/vscode-extension-src/extension.ts +++ b/vscode-extension-src/extension.ts @@ -27,10 +27,10 @@ export async function activate(context: vscode.ExtensionContext): Promise export function deactivate(): void {} async function activateLanguageServer(context: vscode.ExtensionContext) { - LOG.info('Activating Kotlin language server...'); + LOG.info('Activating Kotlin Language Server...'); let barItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); context.subscriptions.push(barItem); - barItem.text = '$(sync) Activating Kotlin language server...'; + barItem.text = '$(sync~spin) Activating Kotlin Language Server...'; barItem.show(); let javaExecutablePath = findJavaExecutable('java');