diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 43e51914d..7afdab91c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,10 @@ name: Build -on: [ push, pull_request ] + +on: + push: + branches: + - main + pull_request: jobs: build: diff --git a/.vscode/launch.json b/.vscode/launch.json index 0e7ab461c..523d944fc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -30,6 +30,15 @@ "request": "attach", "hostName": "localhost", "port": 5005 + }, + { + "type": "extensionHost", + "name": "Run Grammar Dev Extension", + "request": "launch", + "args": [ + "--disable-extension=fwcd.kotlin", + "--extensionDevelopmentPath=${workspaceFolder}/grammars/vscode-grammar-dev" + ] } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index e1160aa16..636f21b25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to the language server will be documented in this file. Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. +## [1.3.1] +- Add support for run/debug code lenses +- Add definition lookup support for JDT symbols +- Add quick fix for implementing abstract functions +- Add experimental JDT.LS integration + ## [1.3.0] - Bump to Kotlin 1.6 - Support JDK 17 diff --git a/README.md b/README.md index 4d7840357..93fa64a2c 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,10 @@ Any editor conforming to LSP is supported, including [VSCode](https://github.com * See [Kotlin Debug Adapter](https://github.com/fwcd/kotlin-debug-adapter) for debugging support on JVM * See [tree-sitter-kotlin](https://github.com/fwcd/tree-sitter-kotlin) for an experimental [Tree-Sitter](https://tree-sitter.github.io/tree-sitter/) grammar +## Packaging + +[![Packaging status](https://repology.org/badge/vertical-allrepos/kotlin-language-server.svg)](https://repology.org/project/kotlin-language-server/versions) + ## This repository needs your help! [The original author](https://github.com/georgewfraser) created this project while he was considering using Kotlin in his work. He ended up deciding not to and is not really using Kotlin these days though this is a pretty fully-functional language server that just needs someone to use it every day for a while and iron out the last few pesky bugs. diff --git a/gradle.properties b/gradle.properties index e8355f897..1c44847c2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -projectVersion=1.4.0 +projectVersion=1.3.2 kotlinVersion=1.6.10 exposedVersion=0.37.3 -lsp4jVersion=0.12.0 +lsp4jVersion=0.15.0 javaVersion=11 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2..249e5832f 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 84d1f85fd..8049c684f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c78733..a69d9cb6c 100755 --- a/gradlew +++ b/gradlew @@ -205,6 +205,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index ac1b06f93..53a6b238d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/grammars/Kotlin.tmLanguage.json b/grammars/Kotlin.tmLanguage.json index 2689ca4e3..23c3a4876 100644 --- a/grammars/Kotlin.tmLanguage.json +++ b/grammars/Kotlin.tmLanguage.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", "name": "Kotlin", "scopeName": "source.kotlin", "patterns": [ @@ -475,10 +476,14 @@ "end": "(\\})", "name": "meta.template.expression.kotlin", "beginCaptures": { - "1": "punctuation.definition.template-expression.begin" + "1": { + "name": "punctuation.definition.template-expression.begin" + } }, "endCaptures": { - "1": "punctuation.definition.template-expression.begin" + "1": { + "name": "punctuation.definition.template-expression.end" + } }, "patterns": [ { diff --git a/grammars/vscode-grammar-dev/.vscodeignore b/grammars/vscode-grammar-dev/.vscodeignore new file mode 100644 index 000000000..f369b5e55 --- /dev/null +++ b/grammars/vscode-grammar-dev/.vscodeignore @@ -0,0 +1,4 @@ +.vscode/** +.vscode-test/** +.gitignore +vsc-extension-quickstart.md diff --git a/grammars/vscode-grammar-dev/README.md b/grammars/vscode-grammar-dev/README.md new file mode 100644 index 000000000..f9ba45b0e --- /dev/null +++ b/grammars/vscode-grammar-dev/README.md @@ -0,0 +1,11 @@ +# Grammar Development Extension for VSCode + +A small VSCode extension for development of the Kotlin grammar. + +> **Note**: The actual Kotlin support for VSCode, including the language client, is located in the [`vscode-kotlin` repository](https://github.com/fwcd/vscode-kotlin). + +## Usage + +The most convenient way to run the extension is to simply use the debug configuration `Run Grammar Dev` in this repo. + +> **Note**: VSCode might show a warning that grammar paths are located outside of the extension folder. This message can be ignored, since the extension is only indended for development anyway. diff --git a/grammars/vscode-grammar-dev/package.json b/grammars/vscode-grammar-dev/package.json new file mode 100644 index 000000000..a0fa617f5 --- /dev/null +++ b/grammars/vscode-grammar-dev/package.json @@ -0,0 +1,25 @@ +{ + "name": "kotlin-grammar-dev", + "displayName": "Kotlin Grammar Development", + "description": "Grammar development extension for Kotlin", + "version": "0.0.1", + "engines": { + "vscode": "^1.67.0" + }, + "categories": [ + "Programming Languages" + ], + "contributes": { + "languages": [{ + "id": "kotlin", + "aliases": ["Kotlin", "kotlin"], + "extensions": [".kt",".kts"], + "configuration": "../kotlin.configuration.json" + }], + "grammars": [{ + "language": "kotlin", + "scopeName": "source.kotlin", + "path": "../Kotlin.tmLanguage.json" + }] + } +} diff --git a/server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt b/server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt index 23fe1356e..0f1659098 100644 --- a/server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt +++ b/server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt @@ -15,7 +15,8 @@ import java.nio.file.Path * and the compiler. Note that Kotlin sources are stored in SourcePath. */ class CompilerClassPath(private val config: CompilerConfiguration) : Closeable { - private val workspaceRoots = mutableSetOf() + val workspaceRoots = mutableSetOf() + private val javaSourcePath = mutableSetOf() private val buildScriptClassPath = mutableSetOf() val classPath = mutableSetOf() diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt b/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt index 7f2017359..96eb61565 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt @@ -6,6 +6,7 @@ import org.eclipse.lsp4j.jsonrpc.services.JsonDelegate import org.eclipse.lsp4j.services.LanguageClient import org.eclipse.lsp4j.services.LanguageClientAware import org.eclipse.lsp4j.services.LanguageServer +import org.eclipse.lsp4j.services.NotebookDocumentService import org.javacs.kt.command.ALL_COMMANDS import org.javacs.kt.externalsources.* import org.javacs.kt.util.AsyncExecutor @@ -30,7 +31,7 @@ class KotlinLanguageServer : LanguageServer, LanguageClientAware, Closeable { private val textDocuments = KotlinTextDocumentService(sourceFiles, sourcePath, config, tempDirectory, uriContentProvider, classPath) private val workspaces = KotlinWorkspaceService(sourceFiles, sourcePath, classPath, textDocuments, config) - private val protocolExtensions = KotlinProtocolExtensionService(uriContentProvider, classPath) + private val protocolExtensions = KotlinProtocolExtensionService(uriContentProvider, classPath, sourcePath) private lateinit var client: LanguageClient @@ -162,4 +163,10 @@ class KotlinLanguageServer : LanguageServer, LanguageClientAware, Closeable { } override fun exit() {} + + // Fixed in https://github.com/eclipse/lsp4j/commit/04b0c6112f0a94140e22b8b15bb5a90d5a0ed851 + // Causes issue in lsp 0.15 + override fun getNotebookDocumentService(): NotebookDocumentService? { + return null; + } } diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt b/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt index cba9d5d6d..76428a510 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt @@ -3,11 +3,16 @@ package org.javacs.kt import org.eclipse.lsp4j.* import org.javacs.kt.util.AsyncExecutor import org.javacs.kt.util.parseURI +import org.javacs.kt.resolve.resolveMain +import org.javacs.kt.position.offset +import org.javacs.kt.overridemembers.listOverridableMembers import java.util.concurrent.CompletableFuture +import java.nio.file.Paths class KotlinProtocolExtensionService( private val uriContentProvider: URIContentProvider, - private val cp: CompilerClassPath + private val cp: CompilerClassPath, + private val sp: SourcePath ) : KotlinProtocolExtensions { private val async = AsyncExecutor() @@ -18,4 +23,30 @@ class KotlinProtocolExtensionService( override fun buildOutputLocation(): CompletableFuture = async.compute { cp.outputDirectory.absolutePath } + + override fun mainClass(textDocument: TextDocumentIdentifier): CompletableFuture> = async.compute { + val fileUri = parseURI(textDocument.uri) + val filePath = Paths.get(fileUri) + + // we find the longest one in case both the root and submodule are included + val workspacePath = cp.workspaceRoots.filter { + filePath.startsWith(it) + }.map { + it.toString() + }.maxByOrNull(String::length) ?: "" + + val compiledFile = sp.currentVersion(fileUri) + + resolveMain(compiledFile) + mapOf( + "projectRoot" to workspacePath + ) + } + + override fun overrideMember(position: TextDocumentPositionParams): CompletableFuture> = async.compute { + val fileUri = parseURI(position.textDocument.uri) + val compiledFile = sp.currentVersion(fileUri) + val cursorOffset = offset(compiledFile.content, position.position) + + listOverridableMembers(compiledFile, cursorOffset) + } } diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt b/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt index b6338eaf5..b29c4b247 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt @@ -12,4 +12,10 @@ interface KotlinProtocolExtensions { @JsonRequest fun buildOutputLocation(): CompletableFuture + + @JsonRequest + fun mainClass(textDocument: TextDocumentIdentifier): CompletableFuture> + + @JsonRequest + fun overrideMember(position: TextDocumentPositionParams): CompletableFuture> } diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt b/server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt index d74d84540..1b483632a 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt @@ -156,6 +156,7 @@ class KotlinTextDocumentService( TODO("not implemented") } + @Suppress("DEPRECATION") override fun documentSymbol(params: DocumentSymbolParams): CompletableFuture>> = async.compute { LOG.info("Find symbols in {}", describeURI(params.textDocument.uri)) diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt b/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt index 85af3b1ad..766f76a5a 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt @@ -1,6 +1,5 @@ package org.javacs.kt -import com.intellij.openapi.project.Project import org.eclipse.lsp4j.* import org.eclipse.lsp4j.services.WorkspaceService import org.eclipse.lsp4j.services.LanguageClient @@ -9,11 +8,9 @@ import org.eclipse.lsp4j.jsonrpc.messages.Either import org.javacs.kt.symbols.workspaceSymbols import org.javacs.kt.command.JAVA_TO_KOTLIN_COMMAND import org.javacs.kt.j2k.convertJavaToKotlin -import org.javacs.kt.KotlinTextDocumentService import org.javacs.kt.position.extractRange import org.javacs.kt.util.filePath import org.javacs.kt.util.parseURI -import java.net.URI import java.nio.file.Paths import java.util.concurrent.CompletableFuture import com.google.gson.JsonElement @@ -142,10 +139,11 @@ class KotlinWorkspaceService( LOG.info("Updated configuration: {}", settings) } - override fun symbol(params: WorkspaceSymbolParams): CompletableFuture> { + @Suppress("DEPRECATION") + override fun symbol(params: WorkspaceSymbolParams): CompletableFuture, List>> { val result = workspaceSymbols(params.query, sp) - return CompletableFuture.completedFuture(result) + return CompletableFuture.completedFuture(Either.forRight(result)) } override fun didChangeWorkspaceFolders(params: DidChangeWorkspaceFoldersParams) { diff --git a/server/src/main/kotlin/org/javacs/kt/codeaction/CodeAction.kt b/server/src/main/kotlin/org/javacs/kt/codeaction/CodeAction.kt index 1adc4f991..7f9bdbdcc 100644 --- a/server/src/main/kotlin/org/javacs/kt/codeaction/CodeAction.kt +++ b/server/src/main/kotlin/org/javacs/kt/codeaction/CodeAction.kt @@ -3,14 +3,14 @@ package org.javacs.kt.codeaction import org.eclipse.lsp4j.* import org.eclipse.lsp4j.jsonrpc.messages.Either import org.javacs.kt.CompiledFile -import org.javacs.kt.codeaction.quickfix.ImplementAbstractFunctionsQuickFix +import org.javacs.kt.codeaction.quickfix.ImplementAbstractMembersQuickFix import org.javacs.kt.codeaction.quickfix.AddMissingImportsQuickFix import org.javacs.kt.command.JAVA_TO_KOTLIN_COMMAND import org.javacs.kt.util.toPath import org.javacs.kt.index.SymbolIndex val QUICK_FIXES = listOf( - ImplementAbstractFunctionsQuickFix(), + ImplementAbstractMembersQuickFix(), AddMissingImportsQuickFix() ) diff --git a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractFunctionsQuickFix.kt b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractFunctionsQuickFix.kt deleted file mode 100644 index cb063951f..000000000 --- a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractFunctionsQuickFix.kt +++ /dev/null @@ -1,160 +0,0 @@ -package org.javacs.kt.codeaction.quickfix - -import org.eclipse.lsp4j.* -import org.eclipse.lsp4j.jsonrpc.messages.Either -import org.javacs.kt.CompiledFile -import org.javacs.kt.index.SymbolIndex -import org.javacs.kt.position.offset -import org.javacs.kt.position.position -import org.javacs.kt.util.toPath -import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi -import org.jetbrains.kotlin.lexer.KtTokens -import org.jetbrains.kotlin.psi.KtClass -import org.jetbrains.kotlin.psi.KtDeclaration -import org.jetbrains.kotlin.psi.KtNamedFunction -import org.jetbrains.kotlin.psi.psiUtil.containingClass -import org.jetbrains.kotlin.psi.psiUtil.endOffset -import org.jetbrains.kotlin.psi.psiUtil.isAbstract -import org.jetbrains.kotlin.psi.psiUtil.startOffset -import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics - -private const val DEFAULT_TAB_SIZE = 4 - -class ImplementAbstractFunctionsQuickFix : QuickFix { - override fun compute(file: CompiledFile, index: SymbolIndex, range: Range, diagnostics: List): List> { - val diagnostic = findDiagnosticMatch(diagnostics, range) - - val startCursor = offset(file.content, range.start) - val endCursor = offset(file.content, range.end) - val kotlinDiagnostics = file.compile.diagnostics - - // If the client side and the server side diagnostics contain a valid diagnostic for this range. - if (diagnostic != null && anyDiagnosticMatch(kotlinDiagnostics, startCursor, endCursor)) { - // Get the class with the missing functions - val kotlinClass = file.parseAtPoint(startCursor) - if (kotlinClass is KtClass) { - // Get the functions that need to be implemented - val functionsToImplement = getAbstractFunctionStubs(file, kotlinClass) - - val uri = file.parse.toPath().toUri().toString() - // Get the padding to be introduced before the function declarations - val padding = getDeclarationPadding(file, kotlinClass) - // Get the location where the new code will be placed - val newFunctionStartPosition = getNewFunctionStartPosition(file, kotlinClass) - - val textEdits = functionsToImplement.map { - // We leave two new lines before the function is inserted - val newText = System.lineSeparator() + System.lineSeparator() + padding + it - TextEdit(Range(newFunctionStartPosition, newFunctionStartPosition), newText) - } - - val codeAction = CodeAction() - codeAction.edit = WorkspaceEdit(mapOf(uri to textEdits)) - codeAction.kind = CodeActionKind.QuickFix - codeAction.title = "Implement abstract functions" - codeAction.diagnostics = listOf(diagnostic) - return listOf(Either.forRight(codeAction)) - } - } - return listOf() - } -} - -fun findDiagnosticMatch(diagnostics: List, range: Range) = - diagnostics.find { diagnosticMatch(it, range, hashSetOf("ABSTRACT_MEMBER_NOT_IMPLEMENTED", "ABSTRACT_CLASS_MEMBER_NOT_IMPLEMENTED")) } - -private fun anyDiagnosticMatch(diagnostics: Diagnostics, startCursor: Int, endCursor: Int) = - diagnostics.any { diagnosticMatch(it, startCursor, endCursor, hashSetOf("ABSTRACT_MEMBER_NOT_IMPLEMENTED", "ABSTRACT_CLASS_MEMBER_NOT_IMPLEMENTED")) } - -private fun getAbstractFunctionStubs(file: CompiledFile, kotlinClass: KtClass) = - // For each of the super types used by this class - kotlinClass.superTypeListEntries.mapNotNull { - // Find the definition of this super type - val descriptor = file.referenceAtPoint(it.startOffset)?.second - val superClass = descriptor?.findPsi() - // If the super class is abstract or an interface - if (superClass is KtClass && (superClass.isAbstract() || superClass.isInterface())) { - // Get the abstract functions of this super type that are currently not implemented by this class - val abstractFunctions = superClass.declarations.filter { - declaration -> isAbstractFunction(declaration) && !overridesDeclaration(kotlinClass, declaration) - } - // Get stubs for each function - abstractFunctions.map { function -> getFunctionStub(function as KtNamedFunction) } - } else { - null - } - }.flatten() - -private fun isAbstractFunction(declaration: KtDeclaration): Boolean = - declaration is KtNamedFunction && !declaration.hasBody() - && (declaration.containingClass()?.isInterface() ?: false || declaration.hasModifier(KtTokens.ABSTRACT_KEYWORD)) - -// Checks if the class overrides the given declaration -private fun overridesDeclaration(kotlinClass: KtClass, declaration: KtDeclaration): Boolean = - kotlinClass.declarations.any { - if (it.name == declaration.name && it.hasModifier(KtTokens.OVERRIDE_KEYWORD)) { - if (it is KtNamedFunction && declaration is KtNamedFunction) { - parametersMatch(it, declaration) - } else { - true - } - } else { - false - } - } - -// Checks if two functions have matching parameters -private fun parametersMatch(function: KtNamedFunction, functionDeclaration: KtNamedFunction): Boolean { - if (function.valueParameters.size == functionDeclaration.valueParameters.size) { - for (index in 0 until function.valueParameters.size) { - if (function.valueParameters[index].name != functionDeclaration.valueParameters[index].name) { - return false - } else if (function.valueParameters[index].typeReference?.name != functionDeclaration.valueParameters[index].typeReference?.name) { - return false - } - } - - if (function.typeParameters.size == functionDeclaration.typeParameters.size) { - for (index in 0 until function.typeParameters.size) { - if (function.typeParameters[index].variance != functionDeclaration.typeParameters[index].variance) { - return false - } - } - } - - return true - } - - return false -} - -private fun getFunctionStub(function: KtNamedFunction): String = - "override fun" + function.text.substringAfter("fun") + " { }" - -private fun getDeclarationPadding(file: CompiledFile, kotlinClass: KtClass): String { - // If the class is not empty, the amount of padding is the same as the one in the last declaration of the class - val paddingSize = if (kotlinClass.declarations.isNotEmpty()) { - val lastFunctionStartOffset = kotlinClass.declarations.last().startOffset - position(file.content, lastFunctionStartOffset).character - } else { - // Otherwise, we just use a default tab size in addition to any existing padding - // on the class itself (note that the class could be inside another class, for example) - position(file.content, kotlinClass.startOffset).character + DEFAULT_TAB_SIZE - } - - return " ".repeat(paddingSize) -} - -private fun getNewFunctionStartPosition(file: CompiledFile, kotlinClass: KtClass): Position? = - // If the class is not empty, the new function will be put right after the last declaration - if (kotlinClass.declarations.isNotEmpty()) { - val lastFunctionEndOffset = kotlinClass.declarations.last().endOffset - position(file.content, lastFunctionEndOffset) - } else { // Otherwise, the function is put at the beginning of the class - val body = kotlinClass.body - if (body != null) { - position(file.content, body.startOffset + 1) - } else { - null - } - } diff --git a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt new file mode 100644 index 000000000..e4b0a6b11 --- /dev/null +++ b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractMembersQuickFix.kt @@ -0,0 +1,116 @@ +package org.javacs.kt.codeaction.quickfix + +import org.eclipse.lsp4j.* +import org.eclipse.lsp4j.jsonrpc.messages.Either +import org.javacs.kt.CompiledFile +import org.javacs.kt.index.SymbolIndex +import org.javacs.kt.position.offset +import org.javacs.kt.position.position +import org.javacs.kt.util.toPath +import org.javacs.kt.overridemembers.createFunctionStub +import org.javacs.kt.overridemembers.createVariableStub +import org.javacs.kt.overridemembers.getClassDescriptor +import org.javacs.kt.overridemembers.getDeclarationPadding +import org.javacs.kt.overridemembers.getNewMembersStartPosition +import org.javacs.kt.overridemembers.getSuperClassTypeProjections +import org.javacs.kt.overridemembers.hasNoBody +import org.javacs.kt.overridemembers.overridesDeclaration +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.ClassConstructorDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.PropertyDescriptor +import org.jetbrains.kotlin.descriptors.isInterface +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtSimpleNameExpression +import org.jetbrains.kotlin.psi.KtSuperTypeListEntry +import org.jetbrains.kotlin.psi.KtTypeArgumentList +import org.jetbrains.kotlin.psi.KtTypeReference +import org.jetbrains.kotlin.psi.psiUtil.containingClass +import org.jetbrains.kotlin.psi.psiUtil.endOffset +import org.jetbrains.kotlin.psi.psiUtil.isAbstract +import org.jetbrains.kotlin.psi.psiUtil.startOffset +import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.TypeProjection +import org.jetbrains.kotlin.types.typeUtil.asTypeProjection + +class ImplementAbstractMembersQuickFix : QuickFix { + override fun compute(file: CompiledFile, index: SymbolIndex, range: Range, diagnostics: List): List> { + val diagnostic = findDiagnosticMatch(diagnostics, range) + + val startCursor = offset(file.content, range.start) + val endCursor = offset(file.content, range.end) + val kotlinDiagnostics = file.compile.diagnostics + + // If the client side and the server side diagnostics contain a valid diagnostic for this range. + if (diagnostic != null && anyDiagnosticMatch(kotlinDiagnostics, startCursor, endCursor)) { + // Get the class with the missing members + val kotlinClass = file.parseAtPoint(startCursor) + if (kotlinClass is KtClass) { + // Get the functions that need to be implemented + val membersToImplement = getAbstractMembersStubs(file, kotlinClass) + + val uri = file.parse.toPath().toUri().toString() + // Get the padding to be introduced before the member declarations + val padding = getDeclarationPadding(file, kotlinClass) + + // Get the location where the new code will be placed + val newMembersStartPosition = getNewMembersStartPosition(file, kotlinClass) + val bodyAppendBeginning = listOf(TextEdit(Range(newMembersStartPosition, newMembersStartPosition), "{")).takeIf { kotlinClass.hasNoBody() } ?: emptyList() + val bodyAppendEnd = listOf(TextEdit(Range(newMembersStartPosition, newMembersStartPosition), System.lineSeparator() + "}")).takeIf { kotlinClass.hasNoBody() } ?: emptyList() + + val textEdits = bodyAppendBeginning + membersToImplement.map { + // We leave two new lines before the member is inserted + val newText = System.lineSeparator() + System.lineSeparator() + padding + it + TextEdit(Range(newMembersStartPosition, newMembersStartPosition), newText) + } + bodyAppendEnd + + val codeAction = CodeAction() + codeAction.edit = WorkspaceEdit(mapOf(uri to textEdits)) + codeAction.kind = CodeActionKind.QuickFix + codeAction.title = "Implement abstract members" + codeAction.diagnostics = listOf(diagnostic) + return listOf(Either.forRight(codeAction)) + } + } + return listOf() + } +} + +fun findDiagnosticMatch(diagnostics: List, range: Range) = + diagnostics.find { diagnosticMatch(it, range, hashSetOf("ABSTRACT_MEMBER_NOT_IMPLEMENTED", "ABSTRACT_CLASS_MEMBER_NOT_IMPLEMENTED")) } + +private fun anyDiagnosticMatch(diagnostics: Diagnostics, startCursor: Int, endCursor: Int) = + diagnostics.any { diagnosticMatch(it, startCursor, endCursor, hashSetOf("ABSTRACT_MEMBER_NOT_IMPLEMENTED", "ABSTRACT_CLASS_MEMBER_NOT_IMPLEMENTED")) } + +private fun getAbstractMembersStubs(file: CompiledFile, kotlinClass: KtClass) = + // For each of the super types used by this class + kotlinClass.superTypeListEntries.mapNotNull { + // Find the definition of this super type + val referenceAtPoint = file.referenceExpressionAtPoint(it.startOffset) + val descriptor = referenceAtPoint?.second + + val classDescriptor = getClassDescriptor(descriptor) + + // If the super class is abstract or an interface + if (null != classDescriptor && (classDescriptor.kind.isInterface || classDescriptor.modality == Modality.ABSTRACT)) { + val superClassTypeArguments = getSuperClassTypeProjections(file, it) + classDescriptor.getMemberScope(superClassTypeArguments).getContributedDescriptors().filter { classMember -> + (classMember is FunctionDescriptor && classMember.modality == Modality.ABSTRACT && !overridesDeclaration(kotlinClass, classMember)) || (classMember is PropertyDescriptor && classMember.modality == Modality.ABSTRACT && !overridesDeclaration(kotlinClass, classMember)) + }.mapNotNull { member -> + when (member) { + is FunctionDescriptor -> createFunctionStub(member) + is PropertyDescriptor -> createVariableStub(member) + else -> null + } + } + } else { + null + } + }.flatten() diff --git a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/QuickFix.kt b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/QuickFix.kt index a7048ddf4..89526bb0a 100644 --- a/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/QuickFix.kt +++ b/server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/QuickFix.kt @@ -7,6 +7,7 @@ import org.eclipse.lsp4j.Range import org.eclipse.lsp4j.jsonrpc.messages.Either import org.javacs.kt.CompiledFile import org.javacs.kt.index.SymbolIndex +import org.javacs.kt.util.isSubrangeOf import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics import org.jetbrains.kotlin.diagnostics.Diagnostic as KotlinDiagnostic @@ -16,10 +17,10 @@ interface QuickFix { } fun diagnosticMatch(diagnostic: Diagnostic, range: Range, diagnosticTypes: Set): Boolean = - diagnostic.range.equals(range) && diagnosticTypes.contains(diagnostic.code.left) + range.isSubrangeOf(diagnostic.range) && diagnosticTypes.contains(diagnostic.code.left) fun diagnosticMatch(diagnostic: KotlinDiagnostic, startCursor: Int, endCursor: Int, diagnosticTypes: Set): Boolean = - diagnostic.textRanges.any { it.startOffset == startCursor && it.endOffset == endCursor } && diagnosticTypes.contains(diagnostic.factory.name) + diagnostic.textRanges.any { it.startOffset <= startCursor && it.endOffset >= endCursor } && diagnosticTypes.contains(diagnostic.factory.name) fun findDiagnosticMatch(diagnostics: List, range: Range, diagnosticTypes: Set) = diagnostics.find { diagnosticMatch(it, range, diagnosticTypes) } diff --git a/server/src/main/kotlin/org/javacs/kt/command/Commands.kt b/server/src/main/kotlin/org/javacs/kt/command/Commands.kt index 0b47ad7e5..6d7feb7e0 100644 --- a/server/src/main/kotlin/org/javacs/kt/command/Commands.kt +++ b/server/src/main/kotlin/org/javacs/kt/command/Commands.kt @@ -1,6 +1,7 @@ package org.javacs.kt.command const val JAVA_TO_KOTLIN_COMMAND = "convertJavaToKotlin" + val ALL_COMMANDS = listOf( - JAVA_TO_KOTLIN_COMMAND + JAVA_TO_KOTLIN_COMMAND, ) diff --git a/server/src/main/kotlin/org/javacs/kt/compiler/Compiler.kt b/server/src/main/kotlin/org/javacs/kt/compiler/Compiler.kt index 207ae70e0..7704ab4cb 100644 --- a/server/src/main/kotlin/org/javacs/kt/compiler/Compiler.kt +++ b/server/src/main/kotlin/org/javacs/kt/compiler/Compiler.kt @@ -382,25 +382,10 @@ private class CompilationEnvironment( } fun updateConfiguration(config: CompilerConfiguration) { - jvmTargetFrom(config.jvm.target) + JvmTarget.fromString(config.jvm.target) ?.let { environment.configuration.put(JVMConfigurationKeys.JVM_TARGET, it) } } - private fun jvmTargetFrom(target: String): JvmTarget? = when (target) { - // See https://github.com/JetBrains/kotlin/blob/master/compiler/config.jvm/src/org/jetbrains/kotlin/config/JvmTarget.kt - "default" -> JvmTarget.DEFAULT - "1.6" -> JvmTarget.JVM_1_6 - "1.8" -> JvmTarget.JVM_1_8 - "9" -> JvmTarget.JVM_9 - "10" -> JvmTarget.JVM_10 - "11" -> JvmTarget.JVM_11 - "12" -> JvmTarget.JVM_12 - "13" -> JvmTarget.JVM_13 - "14" -> JvmTarget.JVM_14 - "15" -> JvmTarget.JVM_15 - else -> null - } - fun createContainer(sourcePath: Collection): Pair { val trace = CliBindingTrace() val container = TopDownAnalyzerFacadeForJVM.createContainer( diff --git a/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt b/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt new file mode 100644 index 000000000..e404d94a9 --- /dev/null +++ b/server/src/main/kotlin/org/javacs/kt/overridemembers/OverrideMembers.kt @@ -0,0 +1,272 @@ +package org.javacs.kt.overridemembers + +import org.eclipse.lsp4j.CodeAction +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.Range +import org.eclipse.lsp4j.TextEdit +import org.eclipse.lsp4j.WorkspaceEdit +import org.javacs.kt.CompiledFile +import org.javacs.kt.util.toPath +import org.javacs.kt.position.position +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtTypeArgumentList +import org.jetbrains.kotlin.psi.KtSuperTypeListEntry +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtTypeReference +import org.jetbrains.kotlin.psi.KtSimpleNameExpression +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.DescriptorVisibilities +import org.jetbrains.kotlin.descriptors.ClassConstructorDescriptor +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.isInterface +import org.jetbrains.kotlin.descriptors.PropertyDescriptor +import org.jetbrains.kotlin.descriptors.MemberDescriptor +import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi +import org.jetbrains.kotlin.psi.psiUtil.endOffset +import org.jetbrains.kotlin.psi.psiUtil.isAbstract +import org.jetbrains.kotlin.psi.psiUtil.startOffset +import org.jetbrains.kotlin.psi.psiUtil.unwrapNullability +import org.jetbrains.kotlin.types.TypeProjection +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.types.typeUtil.asTypeProjection + +// TODO: see where this should ideally be placed +private const val DEFAULT_TAB_SIZE = 4 + +fun listOverridableMembers(file: CompiledFile, cursor: Int): List { + val kotlinClass = file.parseAtPoint(cursor) + + if (kotlinClass is KtClass) { + return createOverrideAlternatives(file, kotlinClass) + } + + return emptyList() +} + +private fun createOverrideAlternatives(file: CompiledFile, kotlinClass: KtClass): List { + // Get the functions that need to be implemented + val membersToImplement = getUnimplementedMembersStubs(file, kotlinClass) + + val uri = file.parse.toPath().toUri().toString() + + // Get the padding to be introduced before the member declarations + val padding = getDeclarationPadding(file, kotlinClass) + + // Get the location where the new code will be placed + val newMembersStartPosition = getNewMembersStartPosition(file, kotlinClass) + + // loop through the memberstoimplement and create code actions + return membersToImplement.map { member -> + val newText = System.lineSeparator() + System.lineSeparator() + padding + member + val textEdit = TextEdit(Range(newMembersStartPosition, newMembersStartPosition), newText) + + val codeAction = CodeAction() + codeAction.edit = WorkspaceEdit(mapOf(uri to listOf(textEdit))) + codeAction.title = member + + codeAction + } +} + +// TODO: any way can repeat less code between this and the getAbstractMembersStubs in the ImplementAbstractMembersQuickfix? +private fun getUnimplementedMembersStubs(file: CompiledFile, kotlinClass: KtClass): List = + // For each of the super types used by this class + // TODO: does not seem to handle the implicit Any and Object super types that well. Need to find out if that is easily solvable. Finds the methods from them if any super class or interface is present + kotlinClass + .superTypeListEntries + .mapNotNull { + // Find the definition of this super type + val referenceAtPoint = file.referenceExpressionAtPoint(it.startOffset) + val descriptor = referenceAtPoint?.second + val classDescriptor = getClassDescriptor(descriptor) + + // If the super class is abstract, interface or just plain open + if (null != classDescriptor && classDescriptor.canBeExtended()) { + val superClassTypeArguments = getSuperClassTypeProjections(file, it) + classDescriptor + .getMemberScope(superClassTypeArguments) + .getContributedDescriptors() + .filter { classMember -> + classMember is MemberDescriptor && + classMember.canBeOverriden() && + !overridesDeclaration(kotlinClass, classMember) + } + .mapNotNull { member -> + when (member) { + is FunctionDescriptor -> createFunctionStub(member) + is PropertyDescriptor -> createVariableStub(member) + else -> null + } + } + } else { + null + } + } + .flatten() + +private fun ClassDescriptor.canBeExtended() = this.kind.isInterface || + this.modality == Modality.ABSTRACT || + this.modality == Modality.OPEN + +private fun MemberDescriptor.canBeOverriden() = (Modality.ABSTRACT == this.modality || Modality.OPEN == this.modality) && Modality.FINAL != this.modality && this.visibility != DescriptorVisibilities.PRIVATE && this.visibility != DescriptorVisibilities.PROTECTED + +// interfaces are ClassDescriptors by default. When calling AbstractClass super methods, we get a ClassConstructorDescriptor +fun getClassDescriptor(descriptor: DeclarationDescriptor?): ClassDescriptor? = + if (descriptor is ClassDescriptor) { + descriptor + } else if (descriptor is ClassConstructorDescriptor) { + descriptor.containingDeclaration + } else { + null + } + +fun getSuperClassTypeProjections( + file: CompiledFile, + superType: KtSuperTypeListEntry +): List = + superType + .typeReference + ?.typeElement + ?.children + ?.filter { it is KtTypeArgumentList } + ?.flatMap { (it as KtTypeArgumentList).arguments } + ?.mapNotNull { + (file.referenceExpressionAtPoint(it?.startOffset ?: 0)?.second as? + ClassDescriptor) + ?.defaultType?.asTypeProjection() + } + ?: emptyList() + +// Checks if the class overrides the given declaration +fun overridesDeclaration(kotlinClass: KtClass, descriptor: MemberDescriptor): Boolean = + when (descriptor) { + is FunctionDescriptor -> kotlinClass.declarations.any { + it.name == descriptor.name.asString() + && it.hasModifier(KtTokens.OVERRIDE_KEYWORD) + && ((it as? KtNamedFunction)?.let { parametersMatch(it, descriptor) } ?: true) + } + is PropertyDescriptor -> kotlinClass.declarations.any { + it.name == descriptor.name.asString() && it.hasModifier(KtTokens.OVERRIDE_KEYWORD) + } + else -> false + } + +// Checks if two functions have matching parameters +private fun parametersMatch( + function: KtNamedFunction, + functionDescriptor: FunctionDescriptor +): Boolean { + if (function.valueParameters.size == functionDescriptor.valueParameters.size) { + for (index in 0 until function.valueParameters.size) { + if (function.valueParameters[index].name != + functionDescriptor.valueParameters[index].name.asString() + ) { + return false + } else if (function.valueParameters[index].typeReference?.typeName() != + functionDescriptor.valueParameters[index] + .type + .unwrappedType() + .toString() && function.valueParameters[index].typeReference?.typeName() != null + ) { + // Any and Any? seems to be null for Kt* psi objects for some reason? At least for equals + // TODO: look further into this + + // Note: Since we treat Java overrides as non nullable by default, the above test + // will fail when the user has made the type nullable. + // TODO: look into this + return false + } + } + + if (function.typeParameters.size == functionDescriptor.typeParameters.size) { + for (index in 0 until function.typeParameters.size) { + if (function.typeParameters[index].variance != + functionDescriptor.typeParameters[index].variance + ) { + return false + } + } + } + + return true + } + + return false +} + +private fun KtTypeReference.typeName(): String? = + this.name + ?: this.typeElement + ?.children + ?.filter { it is KtSimpleNameExpression } + ?.map { (it as KtSimpleNameExpression).getReferencedName() } + ?.firstOrNull() + +fun createFunctionStub(function: FunctionDescriptor): String { + val name = function.name + val arguments = + function.valueParameters + .map { argument -> + val argumentName = argument.name + val argumentType = argument.type.unwrappedType() + + "$argumentName: $argumentType" + } + .joinToString(", ") + val returnType = function.returnType?.unwrappedType()?.toString()?.takeIf { "Unit" != it } + + return "override fun $name($arguments)${returnType?.let { ": $it" } ?: ""} { }" +} + +fun createVariableStub(variable: PropertyDescriptor): String { + val variableType = variable.returnType?.unwrappedType()?.toString()?.takeIf { "Unit" != it } + return "override val ${variable.name}${variableType?.let { ": $it" } ?: ""} = TODO(\"SET VALUE\")" +} + +// about types: regular Kotlin types are marked T or T?, but types from Java are (T..T?) because +// nullability cannot be decided. +// Therefore we have to unpack in case we have the Java type. Fortunately, the Java types are not +// marked nullable, so we default to non nullable types. Let the user decide if they want nullable +// types instead. With this implementation Kotlin types also keeps their nullability +private fun KotlinType.unwrappedType(): KotlinType = + this.unwrap().makeNullableAsSpecified(this.isMarkedNullable) + +fun getDeclarationPadding(file: CompiledFile, kotlinClass: KtClass): String { + // If the class is not empty, the amount of padding is the same as the one in the last + // declaration of the class + val paddingSize = + if (kotlinClass.declarations.isNotEmpty()) { + val lastFunctionStartOffset = kotlinClass.declarations.last().startOffset + position(file.content, lastFunctionStartOffset).character + } else { + // Otherwise, we just use a default tab size in addition to any existing padding + // on the class itself (note that the class could be inside another class, for + // example) + position(file.content, kotlinClass.startOffset).character + DEFAULT_TAB_SIZE + } + + return " ".repeat(paddingSize) +} + +fun getNewMembersStartPosition(file: CompiledFile, kotlinClass: KtClass): Position? = + // If the class is not empty, the new member will be put right after the last declaration + if (kotlinClass.declarations.isNotEmpty()) { + val lastFunctionEndOffset = kotlinClass.declarations.last().endOffset + position(file.content, lastFunctionEndOffset) + } else { // Otherwise, the member is put at the beginning of the class + val body = kotlinClass.body + if (body != null) { + position(file.content, body.startOffset + 1) + } else { + // function has no body. We have to create one. New position is right after entire + // kotlin class text (with space) + val newPosCorrectLine = position(file.content, kotlinClass.startOffset + 1) + newPosCorrectLine.character = (kotlinClass.text.length + 2) + newPosCorrectLine + } + } + +fun KtClass.hasNoBody() = null == this.body diff --git a/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt b/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt new file mode 100644 index 000000000..4a52fa50f --- /dev/null +++ b/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt @@ -0,0 +1,65 @@ +package org.javacs.kt.resolve + +import org.jetbrains.kotlin.fileClasses.JvmFileClassUtil +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtObjectDeclaration +import org.javacs.kt.CompiledFile +import org.javacs.kt.position.range +import org.javacs.kt.util.partitionAroundLast +import com.intellij.openapi.util.TextRange + +fun resolveMain(file: CompiledFile): Map { + val parsedFile = file.parse.copy() as KtFile + + findTopLevelMainFunction(parsedFile)?.let { mainFunction -> + // the KtFiles name is weird. Full path. This causes the class to have full path in name as well. Correcting to top level only + parsedFile.name = parsedFile.name.partitionAroundLast("/").second.substring(1) + + return mapOf( + "mainClass" to JvmFileClassUtil.getFileClassInfoNoResolve(parsedFile).facadeClassFqName.asString(), + "range" to range(file.content, mainFunction.second) + ) + } + + findCompanionObjectMain(parsedFile)?.let { companionMain -> + return mapOf( + "mainClass" to (companionMain.first ?: ""), + "range" to range(file.content, companionMain.second) + ) + } + + return emptyMap() +} + +// only one main method allowed top level in a file (so invalid syntax files will not show any main methods) +private fun findTopLevelMainFunction(file: KtFile): Pair? = file.declarations.find { + it is KtNamedFunction && "main" == it.name +}?.let { + Pair(it.name, it.textRangeInParent) +} + +// finds a top level class that contains a companion object with a main function inside +private fun findCompanionObjectMain(file: KtFile): Pair? = file.declarations + .flatMap { topLevelDeclaration -> + if (topLevelDeclaration is KtClass) { + topLevelDeclaration.companionObjects + } else { + emptyList() + } + } + .flatMap { companionObject -> + companionObject.body?.children?.toList() ?: emptyList() + } + .mapNotNull { companionObjectInternal -> + companionObjectInternal.takeIf { + companionObjectInternal is KtNamedFunction + && "main" == companionObjectInternal.name + && companionObjectInternal.text.startsWith("@JvmStatic") + } + } + .firstOrNull()?.let { + // a little ugly, but because of success of the above, we know that "it" has 4 layers of parent objects (child of companion object body, companion object body, companion object, outer class) + Pair((it.parent.parent.parent.parent as KtClass).fqName?.toString(), it.textRange) + } diff --git a/server/src/main/kotlin/org/javacs/kt/symbols/Symbols.kt b/server/src/main/kotlin/org/javacs/kt/symbols/Symbols.kt index 8255992aa..cee4dbe42 100644 --- a/server/src/main/kotlin/org/javacs/kt/symbols/Symbols.kt +++ b/server/src/main/kotlin/org/javacs/kt/symbols/Symbols.kt @@ -1,10 +1,13 @@ +@file:Suppress("DEPRECATION") + package org.javacs.kt.symbols import com.intellij.psi.PsiElement -import org.eclipse.lsp4j.Location import org.eclipse.lsp4j.SymbolInformation import org.eclipse.lsp4j.SymbolKind import org.eclipse.lsp4j.DocumentSymbol +import org.eclipse.lsp4j.WorkspaceSymbol +import org.eclipse.lsp4j.WorkspaceSymbolLocation import org.eclipse.lsp4j.jsonrpc.messages.Either import org.javacs.kt.SourcePath import org.javacs.kt.position.range @@ -15,7 +18,7 @@ import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.parents fun documentSymbols(file: KtFile): List> = - doDocumentSymbols(file).map { Either.forRight(it) } + doDocumentSymbols(file).map { Either.forRight(it) } private fun doDocumentSymbols(element: PsiElement): List { val children = element.children.flatMap(::doDocumentSymbols) @@ -30,10 +33,10 @@ private fun doDocumentSymbols(element: PsiElement): List { } ?: children } -fun workspaceSymbols(query: String, sp: SourcePath): List = +fun workspaceSymbols(query: String, sp: SourcePath): List = doWorkspaceSymbols(sp) .filter { containsCharactersInOrder(it.name!!, query, false) } - .mapNotNull(::symbolInformation) + .mapNotNull(::workspaceSymbol) .toList() private fun doWorkspaceSymbols(sp: SourcePath): Sequence = @@ -53,10 +56,10 @@ private fun pickImportantElements(node: PsiElement, includeLocals: Boolean): KtN else -> null } -private fun symbolInformation(d: KtNamedDeclaration): SymbolInformation? { +private fun workspaceSymbol(d: KtNamedDeclaration): WorkspaceSymbol? { val name = d.name ?: return null - return SymbolInformation(name, symbolKind(d), symbolLocation(d), symbolContainer(d)) + return WorkspaceSymbol(name, symbolKind(d), Either.forRight(workspaceLocation(d)), symbolContainer(d)) } private fun symbolKind(d: KtNamedDeclaration): SymbolKind = @@ -70,12 +73,11 @@ private fun symbolKind(d: KtNamedDeclaration): SymbolKind = else -> throw IllegalArgumentException("Unexpected symbol $d") } -private fun symbolLocation(d: KtNamedDeclaration): Location { +private fun workspaceLocation(d: KtNamedDeclaration): WorkspaceSymbolLocation { val file = d.containingFile val uri = file.toPath().toUri().toString() - val range = range(file.text, d.textRange) - return Location(uri, range) + return WorkspaceSymbolLocation(uri) } private fun symbolContainer(d: KtNamedDeclaration): String? = diff --git a/server/src/main/kotlin/org/javacs/kt/util/RangeUtils.kt b/server/src/main/kotlin/org/javacs/kt/util/RangeUtils.kt new file mode 100644 index 000000000..01820c80d --- /dev/null +++ b/server/src/main/kotlin/org/javacs/kt/util/RangeUtils.kt @@ -0,0 +1,8 @@ +package org.javacs.kt.util + +import org.eclipse.lsp4j.Range + +// checks if the current range is within the other range (same lines, within the character bounds) +fun Range.isSubrangeOf(otherRange: Range): Boolean = + otherRange.start.line == this.start.line && otherRange.end.line == this.end.line && + otherRange.start.character <= this.start.character && otherRange.end.character >= this.end.character diff --git a/server/src/test/kotlin/org/javacs/kt/ClassPathTest.kt b/server/src/test/kotlin/org/javacs/kt/ClassPathTest.kt index a118efdb5..386db010a 100644 --- a/server/src/test/kotlin/org/javacs/kt/ClassPathTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/ClassPathTest.kt @@ -28,6 +28,19 @@ class ClassPathTest { assertThat(classPath, hasItem(containsString("junit"))) } + @Test fun `find maven classpath`() { + val workspaceRoot = testResourcesRoot().resolve("mavenWorkspace") + val buildFile = workspaceRoot.resolve("pom.xml") + + assertTrue(Files.exists(buildFile)) + + val resolvers = defaultClassPathResolver(listOf(workspaceRoot)) + print(resolvers) + val classPath = resolvers.classpathOrEmpty.map { it.toString() } + + assertThat(classPath, hasItem(containsString("junit"))) + } + @Test fun `find kotlin stdlib`() { assertThat(findKotlinStdlib(), notNullValue()) } diff --git a/server/src/test/kotlin/org/javacs/kt/OverrideMemberTest.kt b/server/src/test/kotlin/org/javacs/kt/OverrideMemberTest.kt new file mode 100644 index 000000000..e2f6c1f18 --- /dev/null +++ b/server/src/test/kotlin/org/javacs/kt/OverrideMemberTest.kt @@ -0,0 +1,146 @@ +package org.javacs.kt + +import com.google.gson.Gson +import org.eclipse.lsp4j.ExecuteCommandParams +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.Range +import org.eclipse.lsp4j.TextDocumentIdentifier +import org.eclipse.lsp4j.TextDocumentPositionParams +import org.junit.Test +import org.hamcrest.core.Every.everyItem +import org.hamcrest.Matchers.containsInAnyOrder +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.hasSize +import org.junit.Assert.assertThat + +class OverrideMemberTest : SingleFileTestFixture("overridemember", "OverrideMembers.kt") { + + val root = testResourcesRoot().resolve(workspaceRoot) + val fileUri = root.resolve(file).toUri().toString() + + @Test + fun `should show all overrides for class`() { + val result = languageServer.getProtocolExtensionService().overrideMember(TextDocumentPositionParams(TextDocumentIdentifier(fileUri), position(9, 8))).get() + + val titles = result.map { it.title } + val edits = result.flatMap { it.edit.changes[fileUri]!! } + val newTexts = edits.map { it.newText } + val ranges = edits.map { it.range } + + assertThat(titles, containsInAnyOrder("override val text: String = TODO(\"SET VALUE\")", + "override fun print() { }", + "override fun equals(other: Any?): Boolean { }", + "override fun hashCode(): Int { }", + "override fun toString(): String { }")) + + val padding = System.lineSeparator() + System.lineSeparator() + " " + assertThat(newTexts, containsInAnyOrder(padding + "override val text: String = TODO(\"SET VALUE\")", + padding + "override fun print() { }", + padding + "override fun equals(other: Any?): Boolean { }", + padding + "override fun hashCode(): Int { }", + padding + "override fun toString(): String { }")) + + + assertThat(ranges, everyItem(equalTo(range(9, 31, 9, 31)))) + } + + @Test + fun `should show one less override for class where one member is already implemented`() { + val result = languageServer.getProtocolExtensionService().overrideMember(TextDocumentPositionParams(TextDocumentIdentifier(fileUri), position(11, 8))).get() + + val titles = result.map { it.title } + val edits = result.flatMap { it.edit.changes[fileUri]!! } + val newTexts = edits.map { it.newText } + val ranges = edits.map { it.range } + + assertThat(titles, containsInAnyOrder("override fun print() { }", + "override fun equals(other: Any?): Boolean { }", + "override fun hashCode(): Int { }", + "override fun toString(): String { }")) + + val padding = System.lineSeparator() + System.lineSeparator() + " " + assertThat(newTexts, containsInAnyOrder(padding + "override fun print() { }", + padding + "override fun equals(other: Any?): Boolean { }", + padding + "override fun hashCode(): Int { }", + padding + "override fun toString(): String { }")) + + assertThat(ranges, everyItem(equalTo(range(12, 56, 12, 56)))) + } + + @Test + fun `should show NO overrides for class where all other alternatives are already implemented`() { + val result = languageServer.getProtocolExtensionService().overrideMember(TextDocumentPositionParams(TextDocumentIdentifier(fileUri), position(15, 8))).get() + + assertThat(result, hasSize(0)) + } + + @Test + fun `should find method in open class`() { + val result = languageServer.getProtocolExtensionService().overrideMember(TextDocumentPositionParams(TextDocumentIdentifier(fileUri), position(37, 8))).get() + + val titles = result.map { it.title } + val edits = result.flatMap { it.edit.changes[fileUri]!! } + val newTexts = edits.map { it.newText } + val ranges = edits.map { it.range } + + assertThat(titles, containsInAnyOrder("override fun numOpenDoorsWithName(input: String): Int { }", + "override fun equals(other: Any?): Boolean { }", + "override fun hashCode(): Int { }", + "override fun toString(): String { }")) + + val padding = System.lineSeparator() + System.lineSeparator() + " " + assertThat(newTexts, containsInAnyOrder(padding + "override fun numOpenDoorsWithName(input: String): Int { }", + padding + "override fun equals(other: Any?): Boolean { }", + padding + "override fun hashCode(): Int { }", + padding + "override fun toString(): String { }")) + + assertThat(ranges, everyItem(equalTo(range(37, 25, 37, 25)))) + } + + @Test + fun `should find members in jdk object`() { + val result = languageServer.getProtocolExtensionService().overrideMember(TextDocumentPositionParams(TextDocumentIdentifier(fileUri), position(39, 9))).get() + + val titles = result.map { it.title } + val edits = result.flatMap { it.edit.changes[fileUri]!! } + val newTexts = edits.map { it.newText } + val ranges = edits.map { it.range } + + assertThat(titles, containsInAnyOrder("override fun equals(other: Any?): Boolean { }", + "override fun hashCode(): Int { }", + "override fun toString(): String { }", + "override fun run() { }", + "override fun clone(): Any { }", + "override fun start() { }", + "override fun interrupt() { }", + "override fun isInterrupted(): Boolean { }", + "override fun countStackFrames(): Int { }", + "override fun getContextClassLoader(): ClassLoader { }", + "override fun setContextClassLoader(cl: ClassLoader) { }", + "override fun getStackTrace(): (Array<(StackTraceElement..StackTraceElement?)>..Array) { }", + "override fun getId(): Long { }", + "override fun getState(): State { }", + "override fun getUncaughtExceptionHandler(): UncaughtExceptionHandler { }", + "override fun setUncaughtExceptionHandler(eh: UncaughtExceptionHandler) { }")) + + val padding = System.lineSeparator() + System.lineSeparator() + " " + assertThat(newTexts, containsInAnyOrder(padding + "override fun equals(other: Any?): Boolean { }", + padding + "override fun hashCode(): Int { }", + padding + "override fun toString(): String { }", + padding + "override fun run() { }", + padding + "override fun clone(): Any { }", + padding + "override fun start() { }", + padding + "override fun interrupt() { }", + padding + "override fun isInterrupted(): Boolean { }", + padding + "override fun countStackFrames(): Int { }", + padding + "override fun getContextClassLoader(): ClassLoader { }", + padding + "override fun setContextClassLoader(cl: ClassLoader) { }", + padding + "override fun getStackTrace(): (Array<(StackTraceElement..StackTraceElement?)>..Array) { }", + padding + "override fun getId(): Long { }", + padding + "override fun getState(): State { }", + padding + "override fun getUncaughtExceptionHandler(): UncaughtExceptionHandler { }", + padding + "override fun setUncaughtExceptionHandler(eh: UncaughtExceptionHandler) { }")) + + assertThat(ranges, everyItem(equalTo(range(39, 25, 39, 25)))) + } +} diff --git a/server/src/test/kotlin/org/javacs/kt/QuickFixesTest.kt b/server/src/test/kotlin/org/javacs/kt/QuickFixesTest.kt index 229a01859..4a73190a3 100644 --- a/server/src/test/kotlin/org/javacs/kt/QuickFixesTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/QuickFixesTest.kt @@ -3,11 +3,12 @@ package org.javacs.kt import org.eclipse.lsp4j.CodeActionKind import org.eclipse.lsp4j.Diagnostic import org.eclipse.lsp4j.jsonrpc.messages.Either -import org.hamcrest.Matchers -import org.junit.Assert +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.hasSize +import org.junit.Assert.assertThat import org.junit.Test -class ImplementAbstractFunctionsQuickFixTest : SingleFileTestFixture("quickfixes", "SomeSubclass.kt") { +class ImplementAbstractMembersQuickFixTest : SingleFileTestFixture("quickfixes", "SomeSubclass.kt") { @Test fun `gets workspace edit for all abstract methods when none are implemented`() { val diagnostic = Diagnostic(range(3, 1, 3, 19), "") @@ -16,27 +17,27 @@ class ImplementAbstractFunctionsQuickFixTest : SingleFileTestFixture("quickfixes val codeActions = languageServer.textDocumentService.codeAction(codeActionParams).get() - Assert.assertThat(codeActions.size, Matchers.equalTo(1)) - Assert.assertThat(codeActions[0].right.kind, Matchers.equalTo(CodeActionKind.QuickFix)) - Assert.assertThat(codeActions[0].right.diagnostics.size, Matchers.equalTo(1)) - Assert.assertThat(codeActions[0].right.diagnostics[0], Matchers.equalTo(diagnostic)) - Assert.assertThat(codeActions[0].right.edit.changes.size, Matchers.equalTo(1)) - Assert.assertThat(codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.size, Matchers.equalTo(2)) - Assert.assertThat( + assertThat(codeActions.size, equalTo(1)) + assertThat(codeActions[0].right.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeActions[0].right.diagnostics.size, equalTo(1)) + assertThat(codeActions[0].right.diagnostics[0], equalTo(diagnostic)) + assertThat(codeActions[0].right.edit.changes.size, equalTo(1)) + assertThat(codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.size, equalTo(2)) + assertThat( codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.get(0)?.range, - Matchers.equalTo(range(3, 55, 3, 55)) + equalTo(range(3, 55, 3, 55)) ) - Assert.assertThat( + assertThat( codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.get(0)?.newText, - Matchers.equalTo(System.lineSeparator() + System.lineSeparator() + " override fun someSuperMethod(someParameter: String): Int { }") + equalTo(System.lineSeparator() + System.lineSeparator() + " override fun someSuperMethod(someParameter: String): Int { }") ) - Assert.assertThat( + assertThat( codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.get(1)?.range, - Matchers.equalTo(range(3, 55, 3, 55)) + equalTo(range(3, 55, 3, 55)) ) - Assert.assertThat( + assertThat( codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.get(1)?.newText, - Matchers.equalTo(System.lineSeparator() + System.lineSeparator() + " override fun someInterfaceMethod() { }") + equalTo(System.lineSeparator() + System.lineSeparator() + " override fun someInterfaceMethod() { }") ) } @@ -48,19 +49,19 @@ class ImplementAbstractFunctionsQuickFixTest : SingleFileTestFixture("quickfixes val codeActions = languageServer.textDocumentService.codeAction(codeActionParams).get() - Assert.assertThat(codeActions.size, Matchers.equalTo(1)) - Assert.assertThat(codeActions[0].right.kind, Matchers.equalTo(CodeActionKind.QuickFix)) - Assert.assertThat(codeActions[0].right.diagnostics.size, Matchers.equalTo(1)) - Assert.assertThat(codeActions[0].right.diagnostics[0], Matchers.equalTo(diagnostic)) - Assert.assertThat(codeActions[0].right.edit.changes.size, Matchers.equalTo(1)) - Assert.assertThat(codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.size, Matchers.equalTo(1)) - Assert.assertThat( + assertThat(codeActions.size, equalTo(1)) + assertThat(codeActions[0].right.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeActions[0].right.diagnostics.size, equalTo(1)) + assertThat(codeActions[0].right.diagnostics[0], equalTo(diagnostic)) + assertThat(codeActions[0].right.edit.changes.size, equalTo(1)) + assertThat(codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.size, equalTo(1)) + assertThat( codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.get(0)?.range, - Matchers.equalTo(range(7, 74, 7, 74)) + equalTo(range(7, 74, 7, 74)) ) - Assert.assertThat( + assertThat( codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.get(0)?.newText, - Matchers.equalTo(System.lineSeparator() + System.lineSeparator() + " override fun someInterfaceMethod() { }") + equalTo(System.lineSeparator() + System.lineSeparator() + " override fun someInterfaceMethod() { }") ) } @@ -72,19 +73,274 @@ class ImplementAbstractFunctionsQuickFixTest : SingleFileTestFixture("quickfixes val codeActions = languageServer.textDocumentService.codeAction(codeActionParams).get() - Assert.assertThat(codeActions.size, Matchers.equalTo(1)) - Assert.assertThat(codeActions[0].right.kind, Matchers.equalTo(CodeActionKind.QuickFix)) - Assert.assertThat(codeActions[0].right.diagnostics.size, Matchers.equalTo(1)) - Assert.assertThat(codeActions[0].right.diagnostics[0], Matchers.equalTo(diagnostic)) - Assert.assertThat(codeActions[0].right.edit.changes.size, Matchers.equalTo(1)) - Assert.assertThat(codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.size, Matchers.equalTo(1)) - Assert.assertThat( + assertThat(codeActions.size, equalTo(1)) + assertThat(codeActions[0].right.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeActions[0].right.diagnostics.size, equalTo(1)) + assertThat(codeActions[0].right.diagnostics[0], equalTo(diagnostic)) + assertThat(codeActions[0].right.edit.changes.size, equalTo(1)) + assertThat(codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.size, equalTo(1)) + assertThat( codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.get(0)?.range, - Matchers.equalTo(range(11, 43, 11, 43)) + equalTo(range(11, 43, 11, 43)) ) - Assert.assertThat( + assertThat( codeActions[0].right.edit.changes[codeActionParams.textDocument.uri]?.get(0)?.newText, - Matchers.equalTo(System.lineSeparator() + System.lineSeparator() + " override fun someSuperMethod(someParameter: String): Int { }") + equalTo(System.lineSeparator() + System.lineSeparator() + " override fun someSuperMethod(someParameter: String): Int { }") ) } } + +class ImplementAbstractMembersQuickFixSameFileTest : SingleFileTestFixture("quickfixes", "samefile.kt") { + @Test + fun `should find no code actions`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 3, 1, 3, 22, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(0)) + } + + @Test + fun `should find one abstract method to implement`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 7, 1, 7, 14, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract members")) + assertThat(codeAction.diagnostics, equalTo(listOf(diagnostics[0]))) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(1)) + + val functionToImplementEdit = textEdit[key]?.get(0) + assertThat(functionToImplementEdit?.range, equalTo(range(7, 30, 7, 30))) + assertThat(functionToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun test(input: String, otherInput: Int) { }")) + } + + @Test + fun `should find several abstract methods to implement`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 15, 1, 15, 21, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract members")) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(2)) + + val firstFunctionToImplementEdit = textEdit[key]?.get(0) + assertThat(firstFunctionToImplementEdit?.range, equalTo(range(15, 49, 15, 49))) + assertThat(firstFunctionToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun print() { }")) + + val secondFunctionToImplementEdit = textEdit[key]?.get(1) + assertThat(secondFunctionToImplementEdit?.range, equalTo(range(15, 49, 15, 49))) + assertThat(secondFunctionToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun test(input: String, otherInput: Int) { }")) + } + + @Test + fun `should find only one abstract method when the other one is already implemented`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 17, 1, 17, 26, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract members")) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(1)) + + val functionToImplementEdit = textEdit[key]?.get(0) + assertThat(functionToImplementEdit?.range, equalTo(range(18, 57, 18, 57))) + assertThat(functionToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun print() { }")) + } + + @Test + fun `should respect nullability of parameter and return value in abstract method`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 25, 1, 25, 16, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract members")) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(1)) + + val functionToImplementEdit = textEdit[key]?.get(0) + assertThat(functionToImplementEdit?.range, equalTo(range(25, 48, 25, 48))) + assertThat(functionToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun myMethod(myStr: String?): String? { }")) + } + + @Test + fun `should find abstract variable and function`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 35, 1, 35, 18, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract members")) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(2)) + + val firstMemberToImplementEdit = textEdit[key]?.get(0) + assertThat(firstMemberToImplementEdit?.range, equalTo(range(35, 35, 35, 35))) + assertThat(firstMemberToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override val name: String = TODO(\"SET VALUE\")")) + + val secondMemberToImplementEdit = textEdit[key]?.get(1) + assertThat(secondMemberToImplementEdit?.range, equalTo(range(35, 35, 35, 35))) + assertThat(secondMemberToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun myFun() { }")) + } + + @Test + fun `should find abstract function when variable is already implemented`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 37, 1, 37, 17, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract members")) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(1)) + + val memberToImplementEdit = textEdit[key]?.get(0) + assertThat(memberToImplementEdit?.range, equalTo(range(38, 31, 38, 31))) + assertThat(memberToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun myFun() { }")) + } + + @Test + fun `should find abstract members when class has no body (square brackets)`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 47, 1, 47, 12, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract members")) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(3)) + + val firstMemberToImplementEdit = textEdit[key]?.get(0) + assertThat(firstMemberToImplementEdit?.range, equalTo(range(47, 23, 47, 23))) + assertThat(firstMemberToImplementEdit?.newText, equalTo("{")) + + val secondMemberToImplementEdit = textEdit[key]?.get(1) + assertThat(secondMemberToImplementEdit?.range, equalTo(range(47, 23, 47, 23))) + assertThat(secondMemberToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun behaviour() { }")) + + val thirdMemberToImplementEdit = textEdit[key]?.get(2) + assertThat(thirdMemberToImplementEdit?.range, equalTo(range(47, 23, 47, 23))) + assertThat(thirdMemberToImplementEdit?.newText, equalTo(System.lineSeparator() + "}")) + } +} + +class ImplementAbstractMembersQuickFixExternalLibraryTest : SingleFileTestFixture("quickfixes", "standardlib.kt") { + @Test + fun `should find one abstract method from Runnable to implement`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 5, 1, 5, 15, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract members")) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(1)) + + val functionToImplementEdit = textEdit[key]?.get(0) + assertThat(functionToImplementEdit?.range, equalTo(range(5, 28, 5, 28))) + assertThat(functionToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun run() { }")) + } + + @Test + fun `should find one abstract method from Comparable to implement`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 7, 1, 7, 19, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract members")) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(1)) + + val functionToImplementEdit = textEdit[key]?.get(0) + assertThat(functionToImplementEdit?.range, equalTo(range(7, 42, 7, 42))) + assertThat(functionToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun compare(p0: String, p1: String): Int { }")) + } + + @Test + fun `should find abstract members for AbstractList`() { + val only = listOf(CodeActionKind.QuickFix) + val codeActionParams = codeActionParams(file, 9, 1, 9, 13, diagnostics, only) + + val codeActionResult = languageServer.textDocumentService.codeAction(codeActionParams).get() + + assertThat(codeActionResult, hasSize(1)) + val codeAction = codeActionResult[0].right + assertThat(codeAction.kind, equalTo(CodeActionKind.QuickFix)) + assertThat(codeAction.title, equalTo("Implement abstract members")) + + val textEdit = codeAction.edit.changes + val key = workspaceRoot.resolve(file).toUri().toString() + assertThat(textEdit.containsKey(key), equalTo(true)) + assertThat(textEdit[key], hasSize(2)) + + val firstMemberToImplementEdit = textEdit[key]?.get(0) + assertThat(firstMemberToImplementEdit?.range, equalTo(range(9, 40, 9, 40))) + assertThat(firstMemberToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override val size: Int = TODO(\"SET VALUE\")")) + + val secondMemberToImplementEdit = textEdit[key]?.get(1) + assertThat(secondMemberToImplementEdit?.range, equalTo(range(9, 40, 9, 40))) + assertThat(secondMemberToImplementEdit?.newText, equalTo(System.lineSeparator() + System.lineSeparator() + " override fun get(index: Int): String { }")) + } +} diff --git a/server/src/test/kotlin/org/javacs/kt/ResolveMainTest.kt b/server/src/test/kotlin/org/javacs/kt/ResolveMainTest.kt new file mode 100644 index 000000000..af67a8f77 --- /dev/null +++ b/server/src/test/kotlin/org/javacs/kt/ResolveMainTest.kt @@ -0,0 +1,79 @@ +package org.javacs.kt + +import com.google.gson.Gson +import org.eclipse.lsp4j.ExecuteCommandParams +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.Range +import org.eclipse.lsp4j.TextDocumentIdentifier +import org.junit.Test +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull + +class NoMainResolve : SingleFileTestFixture("resolvemain", "NoMain.kt") { + @Test + fun `Should not find any main class info`() { + val root = testResourcesRoot().resolve(workspaceRoot) + val fileUri = root.resolve(file).toUri().toString() + + val result = languageServer.getProtocolExtensionService().mainClass(TextDocumentIdentifier(fileUri)).get() + + @Suppress("UNCHECKED_CAST") + val mainInfo = result as Map + assertNull(mainInfo["mainClass"]) + assertEquals(root.toString(), mainInfo["projectRoot"]) + } +} + + +class SimpleMainResolve : SingleFileTestFixture("resolvemain", "Simple.kt") { + @Test + fun `Should resolve correct main class of simple file`() { + val root = testResourcesRoot().resolve(workspaceRoot) + val fileUri = root.resolve(file).toUri().toString() + + val result = languageServer.getProtocolExtensionService().mainClass(TextDocumentIdentifier(fileUri)).get() + + assertNotNull(result) + @Suppress("UNCHECKED_CAST") + val mainInfo = result as Map + assertEquals("test.SimpleKt", mainInfo["mainClass"]) + assertEquals(Range(Position(2, 0), Position(4, 1)), mainInfo["range"]) + assertEquals(root.toString(), mainInfo["projectRoot"]) + } +} + + +class JvmNameAnnotationMainResolve : SingleFileTestFixture("resolvemain", "JvmNameAnnotation.kt") { + @Test + fun `Should resolve correct main class of file annotated with JvmName`() { + val root = testResourcesRoot().resolve(workspaceRoot) + val fileUri = root.resolve(file).toUri().toString() + + val result = languageServer.getProtocolExtensionService().mainClass(TextDocumentIdentifier(fileUri)).get() + + assertNotNull(result) + @Suppress("UNCHECKED_CAST") + val mainInfo = result as Map + assertEquals("com.mypackage.name.Potato", mainInfo["mainClass"]) + assertEquals(Range(Position(5, 0), Position(7, 1)), mainInfo["range"]) + assertEquals(root.toString(), mainInfo["projectRoot"]) + } +} + +class CompanionObjectMainResolve : SingleFileTestFixture("resolvemain", "CompanionObject.kt") { + @Test + fun `Should resolve correct main class of main function inside companion object`() { + val root = testResourcesRoot().resolve(workspaceRoot) + val fileUri = root.resolve(file).toUri().toString() + + val result = languageServer.getProtocolExtensionService().mainClass(TextDocumentIdentifier(fileUri)).get() + + assertNotNull(result) + @Suppress("UNCHECKED_CAST") + val mainInfo = result as Map + assertEquals("test.my.companion.SweetPotato", mainInfo["mainClass"]) + assertEquals(Range(Position(8, 8), Position(11, 9)), mainInfo["range"]) + assertEquals(root.toString(), mainInfo["projectRoot"]) + } +} diff --git a/server/src/test/kotlin/org/javacs/kt/WorkspaceSymbolsTest.kt b/server/src/test/kotlin/org/javacs/kt/WorkspaceSymbolsTest.kt index 80e70bc1d..5c4ebf2fa 100644 --- a/server/src/test/kotlin/org/javacs/kt/WorkspaceSymbolsTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/WorkspaceSymbolsTest.kt @@ -5,12 +5,11 @@ import org.eclipse.lsp4j.WorkspaceSymbolParams import org.hamcrest.Matchers.hasItem import org.hamcrest.Matchers.not import org.junit.Assert.assertThat -import org.junit.Before import org.junit.Test class WorkspaceSymbolsTest : SingleFileTestFixture("symbols", "DocumentSymbols.kt") { @Test fun `find symbols in OtherFileSymbols`() { - val found = languageServer.workspaceService.symbol(WorkspaceSymbolParams("")).get() + val found = languageServer.workspaceService.symbol(WorkspaceSymbolParams("")).get().right val byKind = found.groupBy({ it.kind }, { it.name }) val all = found.map { it.name }.toList() diff --git a/server/src/test/resources/mavenWorkspace/pom.xml b/server/src/test/resources/mavenWorkspace/pom.xml new file mode 100644 index 000000000..8e47afbee --- /dev/null +++ b/server/src/test/resources/mavenWorkspace/pom.xml @@ -0,0 +1,18 @@ + + 4.0.0 + com.example + test-project + jar + 1.0-SNAPSHOT + test-project + http://maven.apache.org + + + junit + junit + 3.8.1 + test + + + diff --git a/server/src/test/resources/mavenWorkspace/src/main/java/com/example/App.java b/server/src/test/resources/mavenWorkspace/src/main/java/com/example/App.java new file mode 100644 index 000000000..daa6d0859 --- /dev/null +++ b/server/src/test/resources/mavenWorkspace/src/main/java/com/example/App.java @@ -0,0 +1,10 @@ +package com.example; + +/** + * Hello world! + */ +public class App { + public static void main(String[] args) { + System.out.println("Hello World!"); + } +} diff --git a/server/src/test/resources/mavenWorkspace/src/test/java/com/example/AppTest.java b/server/src/test/resources/mavenWorkspace/src/test/java/com/example/AppTest.java new file mode 100644 index 000000000..0ac8f7895 --- /dev/null +++ b/server/src/test/resources/mavenWorkspace/src/test/java/com/example/AppTest.java @@ -0,0 +1,33 @@ +package com.example; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Unit test for simple App. + */ +public class AppTest extends TestCase { + /** + * Create the test case + * + * @param testName name of the test case + */ + public AppTest(String testName) { + super(testName); + } + + /** + * @return the suite of tests being tested + */ + public static Test suite() { + return new TestSuite(AppTest.class); + } + + /** + * Rigorous Test :-) + */ + public void testApp() { + assertTrue(true); + } +} diff --git a/server/src/test/resources/overridemember/OverrideMembers.kt b/server/src/test/resources/overridemember/OverrideMembers.kt new file mode 100644 index 000000000..4e6b14465 --- /dev/null +++ b/server/src/test/resources/overridemember/OverrideMembers.kt @@ -0,0 +1,39 @@ +interface Printable { + val text: String + + fun print() { + println("not implemented yet yo") + } +} + +class MyPrintable: Printable {} + +class OtherPrintable: Printable { + override val text: String = "you had me at lasagna" +} + +class CompletePrintable: Printable { + override val text: String = "something something something darkside" + + override fun equals(other: Any?): Boolean { return true } + + override fun hashCode(): Int { return 1 } + + override fun toString(): String { + return "something something complete" + } + + override fun print() { + println("not implemented yet yo") + } +} + +open class MyOpen { + open fun numOpenDoorsWithName(input: String): Int { + return 2 + } +} + +class Closed: MyOpen() {} + +class MyThread: Thread {} diff --git a/server/src/test/resources/quickfixes/samefile.kt b/server/src/test/resources/quickfixes/samefile.kt new file mode 100644 index 000000000..3d17c623e --- /dev/null +++ b/server/src/test/resources/quickfixes/samefile.kt @@ -0,0 +1,47 @@ +package test.kotlin.lsp + +interface MyInterface { + fun test(input: String, otherInput: Int) +} + +class MyClass : MyInterface { +} + + +abstract class CanPrint { + abstract fun print() +} + +class PrintableClass : CanPrint(), MyInterface {} + +class OtherPrintableClass : CanPrint(), MyInterface { + override fun test(input: String, otherInput: Int) {} +} + +interface NullMethodAndReturn { + fun myMethod(myStr: T?): T? +} + +class NullClass : NullMethodAndReturn {} + +abstract class MyAbstract { + val otherValToTestAbstractOverride = 1 + + abstract val name: String + + abstract fun myFun() +} + +class MyImplClass : MyAbstract() {} + +class My2ndClass : MyAbstract() { + override val name = "Nils" +} + + +// defect GH-366, part of the solution +interface IThing { + fun behaviour +} + +class Thing : IThing diff --git a/server/src/test/resources/quickfixes/standardlib.kt b/server/src/test/resources/quickfixes/standardlib.kt new file mode 100644 index 000000000..bb5acb08d --- /dev/null +++ b/server/src/test/resources/quickfixes/standardlib.kt @@ -0,0 +1,9 @@ +package test.kotlin.lsp + +import java.util.Comparator + +class MyThread : Runnable {} + +class MyComperable : Comparator {} + +class MyList : AbstractList() {} diff --git a/server/src/test/resources/resolvemain/CompanionObject.kt b/server/src/test/resources/resolvemain/CompanionObject.kt new file mode 100644 index 000000000..b9a95def1 --- /dev/null +++ b/server/src/test/resources/resolvemain/CompanionObject.kt @@ -0,0 +1,14 @@ +package test.my.companion + +val SOME_GLOBAL_CONSTANT = 42 + +fun multiplyByOne(num: Int) = num*1 + +class SweetPotato { + companion object { + @JvmStatic + fun main() { + println("42 multiplied by 1: ${multiplyByOne(42)}") + } + } +} diff --git a/server/src/test/resources/resolvemain/JvmNameAnnotation.kt b/server/src/test/resources/resolvemain/JvmNameAnnotation.kt new file mode 100644 index 000000000..04c03c434 --- /dev/null +++ b/server/src/test/resources/resolvemain/JvmNameAnnotation.kt @@ -0,0 +1,8 @@ +@JvmName("Potato") +package com.mypackage.name + +val MY_CONSTANT = 1 + +fun main(args: Array) { + +} diff --git a/server/src/test/resources/resolvemain/NoMain.kt b/server/src/test/resources/resolvemain/NoMain.kt new file mode 100644 index 000000000..64eb222b1 --- /dev/null +++ b/server/src/test/resources/resolvemain/NoMain.kt @@ -0,0 +1,3 @@ +package no.main.found.hopefully + +fun multiplyByOne(num: Int) = num*1 diff --git a/server/src/test/resources/resolvemain/Simple.kt b/server/src/test/resources/resolvemain/Simple.kt new file mode 100644 index 000000000..de8be317a --- /dev/null +++ b/server/src/test/resources/resolvemain/Simple.kt @@ -0,0 +1,5 @@ +package test + +fun main() { + println("Hello!") +} diff --git a/shared/src/main/kotlin/org/javacs/kt/classpath/BackupClassPathResolver.kt b/shared/src/main/kotlin/org/javacs/kt/classpath/BackupClassPathResolver.kt index c14175225..b3b0eb0db 100644 --- a/shared/src/main/kotlin/org/javacs/kt/classpath/BackupClassPathResolver.kt +++ b/shared/src/main/kotlin/org/javacs/kt/classpath/BackupClassPathResolver.kt @@ -18,6 +18,7 @@ object BackupClassPathResolver : ClassPathResolver { fun findKotlinStdlib(): Path? = findLocalArtifact("org.jetbrains.kotlin", "kotlin-stdlib") ?: findKotlinCliCompilerLibrary("kotlin-stdlib") + ?: findAlternativeLibraryLocation("kotlin-stdlib") private fun findLocalArtifact(group: String, artifact: String) = tryResolving("$artifact using Maven") { tryFindingLocalArtifactUsing(group, artifact, findLocalArtifactDirUsingMaven(group, artifact)) } @@ -53,8 +54,16 @@ private fun findKotlinCliCompilerLibrary(name: String): Path? = findCommandOnPath("kotlinc") ?.toRealPath() ?.parent // bin - ?.parent // libexec - ?.resolve("lib") + ?.parent // libexec or "top-level" dir + ?.let { + // either in libexec or a top-level directory (that may contain libexec, or just a lib-directory directly) + val possibleLibDir = it.resolve("lib") + if (Files.exists(possibleLibDir)) { + possibleLibDir + } else { + it.resolve("libexec").resolve("lib") + } + } ?.takeIf { Files.exists(it) } ?.let(Files::list) ?.filter { it.fileName.toString() == "$name.jar" } @@ -62,6 +71,11 @@ private fun findKotlinCliCompilerLibrary(name: String): Path? = ?.orElse(null) +// alternative library locations like for snap +// (can probably just use elvis operator and multiple similar expressions for other install directories) +private fun findAlternativeLibraryLocation(name: String): Path? = + Paths.get("/snap/kotlin/current/lib/${name}.jar").existsOrNull() + private fun Path.existsOrNull() = if (Files.exists(this)) this else null diff --git a/shared/src/main/resources/projectClassPathFinder.gradle b/shared/src/main/resources/projectClassPathFinder.gradle index 7ef0a56b3..7fce53927 100644 --- a/shared/src/main/resources/projectClassPathFinder.gradle +++ b/shared/src/main/resources/projectClassPathFinder.gradle @@ -58,6 +58,21 @@ allprojects { project -> } } } + + + // handle kotlin multiplatform style dependencies if any + def kotlinExtension = project.extensions.findByName("kotlin") + if(kotlinExtension && kotlinExtension.hasProperty("targets")) { + def kotlinSourceSets = kotlinExtension.sourceSets + + // Print the list of all dependencies jar files. + kotlinExtension.targets.names.each { + def classpath = configurations["${it}CompileClasspath"] + classpath.files.each { + System.out.println "kotlin-lsp-gradle $it" + } + } + } } }