diff --git a/gradle.properties b/gradle.properties index e3f294a92..d4c48f012 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,4 @@ projectVersion=0.9.1 -kotlinVersion=1.4.20-release-327 +kotlinVersion=1.4.30-RC-232 +exposedVersion=0.29.1 javaVersion=11 diff --git a/server/build.gradle b/server/build.gradle index f6ae4c622..4ea81b611 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -27,6 +27,9 @@ startScripts { repositories { maven { url uri("$projectDir/lib") } maven { url 'https://jitpack.io' } + // TODO: Update once https://github.com/JetBrains/Exposed/issues/1160 is resolved + // since Bintray will be shutting down soon + maven { url 'https://cache-redirector.jetbrains.com/dl.bintray.com/kotlin/exposed' } } dependencies { @@ -38,9 +41,13 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-scripting-compiler-impl:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-scripting-jvm-host-unshaded:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" - implementation "org.jetbrains.kotlin:ide-common-ij201:$kotlinVersion" + implementation "org.jetbrains.kotlin:ide-common-ij202:$kotlinVersion" // implementation("org.jetbrains.kotlin:kotlin-plugin-ij201:$kotlinVersion") { transitive = false } implementation 'org.jetbrains:fernflower:1.0' + implementation "org.jetbrains.exposed:exposed-core:$exposedVersion" + implementation "org.jetbrains.exposed:exposed-dao:$exposedVersion" + implementation "org.jetbrains.exposed:exposed-jdbc:$exposedVersion" + implementation 'com.h2database:h2:1.4.200' implementation 'com.github.fwcd:ktfmt:22bd538a1c' implementation 'com.beust:jcommander:1.78' diff --git a/server/src/main/dist/licenseReport.html b/server/src/main/dist/licenseReport.html index 0c4b714af..493de7cc1 100644 --- a/server/src/main/dist/licenseReport.html +++ b/server/src/main/dist/licenseReport.html @@ -16,6 +16,9 @@

Notice for packages:

GNU LESSER GENERAL PUBLIC LICENSE 2.1
 https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
+
  • + Joda-Time +
  • Kotlinx-coroutines-core
  • @@ -428,10 +431,18 @@

    Notice for packages:

    See the License for the specific language governing permissions and limitations under the License. +
  • + Exposed +
  • Ktfmt
  • No license found
    +
  • + H2 Database Engine +
  • +
    MPL 2.0 or EPL 1.0
    +https://h2database.com/html/license.html
  • Checker Qual
  • @@ -545,7 +556,7 @@

    Notice for packages:

    Kotlin Util Klib
  • - Org.jetbrains.kotlin:ide-common-ij201 + Org.jetbrains.kotlin:ide-common-ij202
  •                                  Apache License
    @@ -758,7 +769,10 @@ 

    Notice for packages:

  • Animal Sniffer Annotations
  • -
    MIT license
    +      
  • + SLF4J API Module +
  • +
    MIT License
     http://www.opensource.org/licenses/mit-license.php
  • LSP4J diff --git a/server/src/main/kotlin/org/javacs/kt/Configuration.kt b/server/src/main/kotlin/org/javacs/kt/Configuration.kt index ee694c0b9..f05a68cdb 100644 --- a/server/src/main/kotlin/org/javacs/kt/Configuration.kt +++ b/server/src/main/kotlin/org/javacs/kt/Configuration.kt @@ -23,6 +23,11 @@ public data class CompilerConfiguration( val jvm: JVMConfiguration = JVMConfiguration() ) +public data class IndexingConfiguration( + /** Whether an index of global symbols should be built in the background. */ + var enabled: Boolean = true +) + public data class ExternalSourcesConfiguration( /** Whether kls-URIs should be sent to the client to describe classes in JARs. */ var useKlsScheme: Boolean = false, @@ -34,5 +39,6 @@ public data class Configuration( val compiler: CompilerConfiguration = CompilerConfiguration(), val completion: CompletionConfiguration = CompletionConfiguration(), val linting: LintingConfiguration = LintingConfiguration(), + var indexing: IndexingConfiguration = IndexingConfiguration(), val externalSources: ExternalSourcesConfiguration = ExternalSourcesConfiguration() ) diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt b/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt index a13da41fe..614189965 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt @@ -11,6 +11,8 @@ import org.javacs.kt.externalsources.JarClassContentProvider import org.javacs.kt.util.AsyncExecutor import org.javacs.kt.util.TemporaryDirectory import org.javacs.kt.util.parseURI +import org.javacs.kt.progress.Progress +import org.javacs.kt.progress.LanguageClientProgress import java.net.URI import java.io.Closeable import java.nio.file.Paths @@ -18,12 +20,12 @@ import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture.completedFuture class KotlinLanguageServer : LanguageServer, LanguageClientAware, Closeable { - private val config = Configuration() + val config = Configuration() val classPath = CompilerClassPath(config.compiler) private val tempDirectory = TemporaryDirectory() private val uriContentProvider = URIContentProvider(JarClassContentProvider(config.externalSources, classPath, tempDirectory)) - val sourcePath = SourcePath(classPath, uriContentProvider) + val sourcePath = SourcePath(classPath, uriContentProvider, config.indexing) val sourceFiles = SourceFiles(sourcePath, uriContentProvider) private val textDocuments = KotlinTextDocumentService(sourceFiles, sourcePath, config, tempDirectory, uriContentProvider) @@ -31,7 +33,13 @@ class KotlinLanguageServer : LanguageServer, LanguageClientAware, Closeable { private val protocolExtensions = KotlinProtocolExtensionService(uriContentProvider) private lateinit var client: LanguageClient + private val async = AsyncExecutor() + private var progressFactory: Progress.Factory = Progress.Factory.None + set(factory: Progress.Factory) { + field = factory + sourcePath.progressFactory = factory + } override fun connect(client: LanguageClient) { this.client = client @@ -72,44 +80,26 @@ class KotlinLanguageServer : LanguageServer, LanguageClientAware, Closeable { val clientCapabilities = params.capabilities config.completion.snippets.enabled = clientCapabilities?.textDocument?.completion?.completionItem?.snippetSupport ?: false - val folders = params.workspaceFolders - - fun reportProgress(notification: WorkDoneProgressNotification) { - params.workDoneToken?.let { - client.notifyProgress(ProgressParams(it, notification)) - } + if (clientCapabilities?.window?.workDoneProgress ?: false) { + progressFactory = LanguageClientProgress.Factory(client) } - reportProgress(WorkDoneProgressBegin().apply { - title = "Adding Kotlin workspace folders" - percentage = 0 - }) + val folders = params.workspaceFolders + val progress = params.workDoneToken?.let { LanguageClientProgress("Workspace folders", it, client) } folders.forEachIndexed { i, folder -> LOG.info("Adding workspace folder {}", folder.name) val progressPrefix = "[${i + 1}/${folders.size}] ${folder.name}" val progressPercent = (100 * i) / folders.size - reportProgress(WorkDoneProgressReport().apply { - message = "$progressPrefix: Updating source path" - percentage = progressPercent - }) - + progress?.update("$progressPrefix: Updating source path", progressPercent) val root = Paths.get(parseURI(folder.uri)) sourceFiles.addWorkspaceRoot(root) - reportProgress(WorkDoneProgressReport().apply { - message = "$progressPrefix: Updating class path" - percentage = progressPercent - }) - + progress?.update("$progressPrefix: Updating class path", progressPercent) val refreshed = classPath.addWorkspaceRoot(root) if (refreshed) { - reportProgress(WorkDoneProgressReport().apply { - message = "$progressPrefix: Refreshing source path" - percentage = progressPercent - }) - + progress?.update("$progressPrefix: Refreshing source path", progressPercent) sourcePath.refresh() } } diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt b/server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt index 9eae0cc85..21de9bebf 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt @@ -156,7 +156,7 @@ class KotlinTextDocumentService( LOG.info("Completing at {}", describePosition(position)) val (file, cursor) = recover(position, Recompile.NEVER) // TODO: Investigate when to recompile - val completions = completions(file, cursor, config.completion) + val completions = completions(file, cursor, sp.index, config.completion) LOG.info("Found {} items", completions.items.size) Either.forRight, CompletionList>(completions) diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt b/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt index 77019348e..f67a19f5b 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt @@ -123,6 +123,14 @@ class KotlinWorkspaceService( } } + // Update indexing options + get("indexing")?.asJsonObject?.apply { + val indexing = config.indexing + get("enabled")?.asBoolean?.let { + indexing.enabled = it + } + } + // Update options about external sources e.g. JAR files, decompilers, etc get("externalSources")?.asJsonObject?.apply { val externalSources = config.externalSources diff --git a/server/src/main/kotlin/org/javacs/kt/SourcePath.kt b/server/src/main/kotlin/org/javacs/kt/SourcePath.kt index 44bc7f3fd..02ae0e4cb 100644 --- a/server/src/main/kotlin/org/javacs/kt/SourcePath.kt +++ b/server/src/main/kotlin/org/javacs/kt/SourcePath.kt @@ -1,12 +1,20 @@ package org.javacs.kt import org.javacs.kt.compiler.CompilationKind +import org.javacs.kt.util.AsyncExecutor import org.javacs.kt.util.fileExtension import org.javacs.kt.util.filePath import org.javacs.kt.util.describeURI +import org.javacs.kt.index.SymbolIndex +import org.javacs.kt.progress.Progress +import org.javacs.kt.IndexingConfiguration import com.intellij.lang.Language import com.intellij.psi.PsiFile +import com.intellij.openapi.fileTypes.FileType +import com.intellij.openapi.fileTypes.LanguageFileType import org.jetbrains.kotlin.container.ComponentProvider +import org.jetbrains.kotlin.container.getService +import org.jetbrains.kotlin.descriptors.ModuleDescriptor import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.CompositeBindingContext @@ -18,13 +26,25 @@ import java.util.concurrent.locks.ReentrantLock class SourcePath( private val cp: CompilerClassPath, - private val contentProvider: URIContentProvider + private val contentProvider: URIContentProvider, + private val indexingConfig: IndexingConfiguration ) { private val files = mutableMapOf() private val parseDataWriteLock = ReentrantLock() + private val indexAsync = AsyncExecutor() + private var indexInitialized: Boolean = false + var indexEnabled: Boolean by indexingConfig::enabled + val index = SymbolIndex() + var beforeCompileCallback: () -> Unit = {} + var progressFactory: Progress.Factory = Progress.Factory.None + set(factory: Progress.Factory) { + field = factory + index.progressFactory = factory + } + private inner class SourceFile( val uri: URI, var content: String, @@ -36,7 +56,7 @@ class SourcePath( val language: Language? = null, val isTemporary: Boolean = false // A temporary source file will not be returned by .all() ) { - val extension: String? = uri.fileExtension ?: language?.associatedFileType?.defaultExtension + val extension: String? = uri.fileExtension ?: "kt" // TODO: Use language?.associatedFileType?.defaultExtension again val isScript: Boolean = extension == "kts" val kind: CompilationKind = if (path?.fileName?.toString()?.endsWith(".gradle.kts") ?: false) CompilationKind.BUILD_SCRIPT @@ -85,6 +105,8 @@ class SourcePath( compiledContainer = container compiledFile = parsed } + + initializeIndexAsyncIfNeeded(container) } private fun doCompileIfChanged() { @@ -190,6 +212,11 @@ class SourcePath( } } + // Only index normal files, not build files + if (kind == CompilationKind.DEFAULT) { + initializeIndexAsyncIfNeeded(container) + } + return context } @@ -203,6 +230,18 @@ class SourcePath( return CompositeBindingContext.create(combined) } + /** + * Initialized the symbol index asynchronously, if not + * already done. + */ + private fun initializeIndexAsyncIfNeeded(container: ComponentProvider) = indexAsync.execute { + if (indexEnabled && !indexInitialized) { + indexInitialized = true + val module = container.getService(ModuleDescriptor::class.java) + index.refresh(module) + } + } + /** * Recompiles all source files that are initialized. */ 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 7cd1439d3..4a267adb3 100644 --- a/server/src/main/kotlin/org/javacs/kt/completion/Completions.kt +++ b/server/src/main/kotlin/org/javacs/kt/completion/Completions.kt @@ -5,15 +5,21 @@ import org.eclipse.lsp4j.CompletionItem import org.eclipse.lsp4j.CompletionItemKind import org.eclipse.lsp4j.CompletionItemTag import org.eclipse.lsp4j.CompletionList +import org.eclipse.lsp4j.TextEdit +import org.eclipse.lsp4j.Range +import org.eclipse.lsp4j.Position import org.javacs.kt.CompiledFile import org.javacs.kt.LOG import org.javacs.kt.CompletionConfiguration +import org.javacs.kt.index.Symbol +import org.javacs.kt.index.SymbolIndex import org.javacs.kt.util.containsCharactersInOrder import org.javacs.kt.util.findParent import org.javacs.kt.util.noResult import org.javacs.kt.util.stringDistance import org.javacs.kt.util.toPath import org.javacs.kt.util.onEachIndexed +import org.javacs.kt.position.location import org.jetbrains.kotlin.builtins.KotlinBuiltIns import org.jetbrains.kotlin.container.get import org.jetbrains.kotlin.descriptors.* @@ -46,28 +52,101 @@ import org.jetbrains.kotlin.types.typeUtil.replaceArgumentsWithStarProjections import org.jetbrains.kotlin.types.checker.KotlinTypeChecker import java.util.concurrent.TimeUnit -// The maxmimum number of completion items +// The maximum number of completion items private const val MAX_COMPLETION_ITEMS = 75 // The minimum length after which completion lists are sorted private const val MIN_SORT_LENGTH = 3 /** Finds completions at the specified position. */ -fun completions(file: CompiledFile, cursor: Int, config: CompletionConfiguration): CompletionList { +fun completions(file: CompiledFile, cursor: Int, index: SymbolIndex, config: CompletionConfiguration): CompletionList { val partial = findPartialIdentifier(file, cursor) LOG.debug("Looking for completions that match '{}'", partial) - var isIncomplete = false - val items = elementCompletionItems(file, cursor, config, partial).ifEmpty { keywordCompletionItems(partial).also { isIncomplete = true } } + val (elementItems, isExhaustive, receiver) = elementCompletionItems(file, cursor, config, partial) + val elementItemList = elementItems.toList() + val elementItemLabels = elementItemList.mapNotNull { it.label }.toSet() + val items = ( + elementItemList.asSequence() + + (if (!isExhaustive) indexCompletionItems(file, cursor, receiver, index, partial).filter { it.label !in elementItemLabels } else emptySequence()) + + (if (elementItemList.isEmpty()) keywordCompletionItems(partial) else emptySequence()) + ) val itemList = items .take(MAX_COMPLETION_ITEMS) .toList() .onEachIndexed { i, item -> item.sortText = i.toString().padStart(2, '0') } - isIncomplete = isIncomplete || (itemList.size == MAX_COMPLETION_ITEMS) + val isIncomplete = itemList.size >= MAX_COMPLETION_ITEMS || elementItemList.isEmpty() return CompletionList(isIncomplete, itemList) } +/** Finds completions in the global symbol index, for potentially unimported symbols. */ +private fun indexCompletionItems(file: CompiledFile, cursor: Int, receiver: KtExpression?, index: SymbolIndex, partial: String): Sequence { + val parsedFile = file.parse + val imports = parsedFile.importDirectives + // TODO: Deal with alias imports + val wildcardPackages = imports + .mapNotNull { it.importPath } + .filter { it.isAllUnder } + .map { it.fqName } + .toSet() + val importedNames = imports + .mapNotNull { it.importedFqName?.shortName() } + .toSet() + val receiverType = receiver?.let { expr -> file.scopeAtPoint(cursor)?.let { file.typeOfExpression(expr, it) } } + val receiverTypeFqName = receiverType?.constructor?.declarationDescriptor?.fqNameSafe + + return index + .query(partial, receiverTypeFqName, limit = MAX_COMPLETION_ITEMS) + .asSequence() + .filter { it.kind != Symbol.Kind.MODULE } // Ignore global module/package name completions for now, since they cannot be 'imported' + .filter { it.fqName.shortName() !in importedNames && it.fqName.parent() !in wildcardPackages } + .filter { + // TODO: Visibility checker should be less liberal + it.visibility == Symbol.Visibility.PUBLIC + || it.visibility == Symbol.Visibility.PROTECTED + || it.visibility == Symbol.Visibility.INTERNAL + } + .map { CompletionItem().apply { + label = it.fqName.shortName().toString() + kind = when (it.kind) { + Symbol.Kind.CLASS -> CompletionItemKind.Class + Symbol.Kind.INTERFACE -> CompletionItemKind.Interface + Symbol.Kind.FUNCTION -> CompletionItemKind.Function + Symbol.Kind.VARIABLE -> CompletionItemKind.Variable + Symbol.Kind.MODULE -> CompletionItemKind.Module + Symbol.Kind.ENUM -> CompletionItemKind.Enum + Symbol.Kind.ENUM_MEMBER -> CompletionItemKind.EnumMember + Symbol.Kind.CONSTRUCTOR -> CompletionItemKind.Constructor + Symbol.Kind.FIELD -> CompletionItemKind.Field + Symbol.Kind.UNKNOWN -> CompletionItemKind.Text + } + detail = "(import from ${it.fqName.parent()})" + val pos = findImportInsertionPosition(parsedFile, it.fqName) + val prefix = if (importedNames.isEmpty()) "\n\n" else "\n" + additionalTextEdits = listOf(TextEdit(Range(pos, pos), "${prefix}import ${it.fqName}")) // TODO: CRLF? + } } +} + +/** Finds a good insertion position for a new import of the given fully-qualified name. */ +private fun findImportInsertionPosition(parsedFile: KtFile, fqName: FqName): Position = + (closestImport(parsedFile.importDirectives, fqName) as? KtElement ?: parsedFile.packageDirective as? KtElement) + ?.let(::location) + ?.range + ?.end + ?: Position(0, 0) + +// TODO: Lexicographic insertion +private fun closestImport(imports: List, fqName: FqName): KtImportDirective? = + imports + .asReversed() + .maxByOrNull { it.importedFqName?.let { matchingPrefixLength(it, fqName) } ?: 0 } + +private fun matchingPrefixLength(left: FqName, right: FqName): Int = + left.pathSegments().asSequence().zip(right.pathSegments().asSequence()) + .takeWhile { it.first == it.second } + .count() + /** Finds keyword completions starting with the given partial identifier. */ private fun keywordCompletionItems(partial: String): Sequence = (KtTokens.SOFT_KEYWORDS.getTypes() + KtTokens.KEYWORDS.getTypes()).asSequence() @@ -78,9 +157,11 @@ private fun keywordCompletionItems(partial: String): Sequence = kind = CompletionItemKind.Keyword } } +data class ElementCompletionItems(val items: Sequence, val isExhaustive: Boolean, val receiver: KtExpression? = null) + /** Finds completions based on the element around the user's cursor. */ -private fun elementCompletionItems(file: CompiledFile, cursor: Int, config: CompletionConfiguration, partial: String): Sequence { - val surroundingElement = completableElement(file, cursor) ?: return emptySequence() +private fun elementCompletionItems(file: CompiledFile, cursor: Int, config: CompletionConfiguration, partial: String): ElementCompletionItems { + val surroundingElement = completableElement(file, cursor) ?: return ElementCompletionItems(emptySequence(), isExhaustive = true) val completions = elementCompletions(file, cursor, surroundingElement) val matchesName = completions.filter { containsCharactersInOrder(name(it), partial, caseSensitive = false) } @@ -88,7 +169,12 @@ private fun elementCompletionItems(file: CompiledFile, cursor: Int, config: Comp ?: matchesName.sortedBy { if (name(it).startsWith(partial)) 0 else 1 } val visible = sorted.filter(isVisible(file, cursor)) - return visible.map { completionItem(it, surroundingElement, file, config) } + val isExhaustive = surroundingElement !is KtNameReferenceExpression + && surroundingElement !is KtTypeElement + && surroundingElement !is KtQualifiedExpression + val receiver = (surroundingElement as? KtQualifiedExpression)?.receiverExpression + + return ElementCompletionItems(visible.map { completionItem(it, surroundingElement, file, config) }, isExhaustive, receiver) } private val callPattern = Regex("(.*)\\((?:\\$\\d+)?\\)(?:\\$0)?") diff --git a/server/src/main/kotlin/org/javacs/kt/index/ExtractSymbolExtensionReceiverType.kt b/server/src/main/kotlin/org/javacs/kt/index/ExtractSymbolExtensionReceiverType.kt new file mode 100644 index 000000000..3fc2953ae --- /dev/null +++ b/server/src/main/kotlin/org/javacs/kt/index/ExtractSymbolExtensionReceiverType.kt @@ -0,0 +1,14 @@ +package org.javacs.kt.index + +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.descriptors.impl.DeclarationDescriptorVisitorEmptyBodies +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe + +object ExtractSymbolExtensionReceiverType : DeclarationDescriptorVisitorEmptyBodies() { + private fun convert(desc: ReceiverParameterDescriptor): FqName? = desc.value.type.constructor.declarationDescriptor?.fqNameSafe + + override fun visitFunctionDescriptor(desc: FunctionDescriptor, nothing: Unit?) = desc.extensionReceiverParameter?.let(this::convert) + + override fun visitVariableDescriptor(desc: VariableDescriptor, nothing: Unit?) = desc.extensionReceiverParameter?.let(this::convert) +} diff --git a/server/src/main/kotlin/org/javacs/kt/index/ExtractSymbolKind.kt b/server/src/main/kotlin/org/javacs/kt/index/ExtractSymbolKind.kt new file mode 100644 index 000000000..2d6910cc9 --- /dev/null +++ b/server/src/main/kotlin/org/javacs/kt/index/ExtractSymbolKind.kt @@ -0,0 +1,40 @@ +package org.javacs.kt.index + +import org.jetbrains.kotlin.descriptors.* + +object ExtractSymbolKind : DeclarationDescriptorVisitor { + override fun visitPropertySetterDescriptor(desc: PropertySetterDescriptor, nothing: Unit?) = Symbol.Kind.FIELD + + override fun visitConstructorDescriptor(desc: ConstructorDescriptor, nothing: Unit?) = Symbol.Kind.CONSTRUCTOR + + override fun visitReceiverParameterDescriptor(desc: ReceiverParameterDescriptor, nothing: Unit?) = Symbol.Kind.VARIABLE + + override fun visitPackageViewDescriptor(desc: PackageViewDescriptor, nothing: Unit?) = Symbol.Kind.MODULE + + override fun visitFunctionDescriptor(desc: FunctionDescriptor, nothing: Unit?) = Symbol.Kind.FUNCTION + + override fun visitModuleDeclaration(desc: ModuleDescriptor, nothing: Unit?) = Symbol.Kind.MODULE + + override fun visitClassDescriptor(desc: ClassDescriptor, nothing: Unit?): Symbol.Kind = when (desc.kind) { + ClassKind.INTERFACE -> Symbol.Kind.INTERFACE + ClassKind.ENUM_CLASS -> Symbol.Kind.ENUM + ClassKind.ENUM_ENTRY -> Symbol.Kind.ENUM_MEMBER + else -> Symbol.Kind.CLASS + } + + override fun visitPackageFragmentDescriptor(desc: PackageFragmentDescriptor, nothing: Unit?) = Symbol.Kind.MODULE + + override fun visitValueParameterDescriptor(desc: ValueParameterDescriptor, nothing: Unit?) = Symbol.Kind.VARIABLE + + override fun visitTypeParameterDescriptor(desc: TypeParameterDescriptor, nothing: Unit?) = Symbol.Kind.VARIABLE + + override fun visitScriptDescriptor(desc: ScriptDescriptor, nothing: Unit?) = Symbol.Kind.MODULE + + override fun visitTypeAliasDescriptor(desc: TypeAliasDescriptor, nothing: Unit?) = Symbol.Kind.VARIABLE + + override fun visitPropertyGetterDescriptor(desc: PropertyGetterDescriptor, nothing: Unit?) = Symbol.Kind.VARIABLE + + override fun visitVariableDescriptor(desc: VariableDescriptor, nothing: Unit?) = Symbol.Kind.VARIABLE + + override fun visitPropertyDescriptor(desc: PropertyDescriptor, nothing: Unit?) = Symbol.Kind.VARIABLE +} diff --git a/server/src/main/kotlin/org/javacs/kt/index/ExtractSymbolVisibility.kt b/server/src/main/kotlin/org/javacs/kt/index/ExtractSymbolVisibility.kt new file mode 100644 index 000000000..17f5d28a8 --- /dev/null +++ b/server/src/main/kotlin/org/javacs/kt/index/ExtractSymbolVisibility.kt @@ -0,0 +1,44 @@ +package org.javacs.kt.index + +import org.jetbrains.kotlin.descriptors.* + +object ExtractSymbolVisibility : DeclarationDescriptorVisitor { + private fun convert(visibility: DescriptorVisibility): Symbol.Visibility = when (visibility.delegate) { + Visibilities.PrivateToThis -> Symbol.Visibility.PRIAVTE_TO_THIS + Visibilities.Private -> Symbol.Visibility.PRIVATE + Visibilities.Internal -> Symbol.Visibility.INTERNAL + Visibilities.Protected -> Symbol.Visibility.PROTECTED + Visibilities.Public -> Symbol.Visibility.PUBLIC + else -> Symbol.Visibility.UNKNOWN + } + + override fun visitPropertySetterDescriptor(desc: PropertySetterDescriptor, nothing: Unit?) = convert(desc.visibility) + + override fun visitConstructorDescriptor(desc: ConstructorDescriptor, nothing: Unit?) = convert(desc.visibility) + + override fun visitReceiverParameterDescriptor(desc: ReceiverParameterDescriptor, nothing: Unit?) = convert(desc.visibility) + + override fun visitPackageViewDescriptor(desc: PackageViewDescriptor, nothing: Unit?) = Symbol.Visibility.PUBLIC + + override fun visitFunctionDescriptor(desc: FunctionDescriptor, nothing: Unit?) = convert(desc.visibility) + + override fun visitModuleDeclaration(desc: ModuleDescriptor, nothing: Unit?) = Symbol.Visibility.PUBLIC + + override fun visitClassDescriptor(desc: ClassDescriptor, nothing: Unit?) = convert(desc.visibility) + + override fun visitPackageFragmentDescriptor(desc: PackageFragmentDescriptor, nothing: Unit?) = Symbol.Visibility.PUBLIC + + override fun visitValueParameterDescriptor(desc: ValueParameterDescriptor, nothing: Unit?) = convert(desc.visibility) + + override fun visitTypeParameterDescriptor(desc: TypeParameterDescriptor, nothing: Unit?) = Symbol.Visibility.PUBLIC + + override fun visitScriptDescriptor(desc: ScriptDescriptor, nothing: Unit?) = convert(desc.visibility) + + override fun visitTypeAliasDescriptor(desc: TypeAliasDescriptor, nothing: Unit?) = convert(desc.visibility) + + override fun visitPropertyGetterDescriptor(desc: PropertyGetterDescriptor, nothing: Unit?) = convert(desc.visibility) + + override fun visitVariableDescriptor(desc: VariableDescriptor, nothing: Unit?) = convert(desc.visibility) + + override fun visitPropertyDescriptor(desc: PropertyDescriptor, nothing: Unit?) = convert(desc.visibility) +} diff --git a/server/src/main/kotlin/org/javacs/kt/index/Symbol.kt b/server/src/main/kotlin/org/javacs/kt/index/Symbol.kt new file mode 100644 index 000000000..ed6c47161 --- /dev/null +++ b/server/src/main/kotlin/org/javacs/kt/index/Symbol.kt @@ -0,0 +1,41 @@ +package org.javacs.kt.index + +import org.jetbrains.kotlin.name.FqName + +data class Symbol( + // TODO: Store location (e.g. using a URI) + val fqName: FqName, + val kind: Kind, + val visibility: Visibility, + val extensionReceiverType: FqName? +) { + enum class Kind(val rawValue: Int) { + CLASS(0), + INTERFACE(1), + FUNCTION(2), + VARIABLE(3), + MODULE(4), + ENUM(5), + ENUM_MEMBER(6), + CONSTRUCTOR(7), + FIELD(8), + UNKNOWN(9); + + companion object { + fun fromRaw(rawValue: Int) = Kind.values().firstOrNull { it.rawValue == rawValue } ?: Kind.UNKNOWN + } + } + + enum class Visibility(val rawValue: Int) { + PRIAVTE_TO_THIS(0), + PRIVATE(1), + INTERNAL(2), + PROTECTED(3), + PUBLIC(4), + UNKNOWN(5); + + companion object { + fun fromRaw(rawValue: Int) = Visibility.values().firstOrNull { it.rawValue == rawValue } ?: Visibility.UNKNOWN + } + } +} diff --git a/server/src/main/kotlin/org/javacs/kt/index/SymbolIndex.kt b/server/src/main/kotlin/org/javacs/kt/index/SymbolIndex.kt new file mode 100644 index 000000000..096cdbd60 --- /dev/null +++ b/server/src/main/kotlin/org/javacs/kt/index/SymbolIndex.kt @@ -0,0 +1,126 @@ +package org.javacs.kt.index + +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.kotlin.descriptors.ModuleDescriptor +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter +import org.jetbrains.kotlin.resolve.scopes.MemberScope +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi2ir.intermediate.extensionReceiverType +import org.javacs.kt.LOG +import org.javacs.kt.progress.Progress +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SqlExpressionBuilder.like +import org.jetbrains.exposed.sql.insert + +private object Symbols : Table() { + val fqName = varchar("fqname", length = 255) references FqNames.fqName + val kind = integer("kind") + val visibility = integer("visibility") + val extensionReceiverType = varchar("extensionreceivertype", length = 255).nullable() + + override val primaryKey = PrimaryKey(fqName) +} + +private object FqNames : Table() { + val fqName = varchar("fqname", length = 255) + val shortName = varchar("shortname", length = 80) + + override val primaryKey = PrimaryKey(fqName) +} + +/** + * A global view of all available symbols across all packages. + */ +class SymbolIndex { + private val db = Database.connect("jdbc:h2:mem:symbolindex;DB_CLOSE_DELAY=-1", "org.h2.Driver") + + var progressFactory: Progress.Factory = Progress.Factory.None + + init { + transaction(db) { + SchemaUtils.create(Symbols, FqNames) + } + } + + /** Rebuilds the entire index. May take a while. */ + fun refresh(module: ModuleDescriptor) { + val started = System.currentTimeMillis() + LOG.info("Updating symbol index...") + + progressFactory.create("Indexing").thenApply { progress -> + try { + val descriptors = allDescriptors(module) + + // TODO: Incremental updates + transaction(db) { + Symbols.deleteAll() + + for (descriptor in descriptors) { + val fqn = descriptor.fqNameSafe + val extensionReceiverFqn = descriptor.accept(ExtractSymbolExtensionReceiverType, Unit) + + FqNames.replace { + it[fqName] = fqn.toString() + it[shortName] = fqn.shortName().toString() + } + + extensionReceiverFqn?.let { rFqn -> + FqNames.replace { + it[fqName] = rFqn.toString() + it[shortName] = rFqn.shortName().toString() + } + } + + Symbols.replace { + it[fqName] = fqn.toString() + it[kind] = descriptor.accept(ExtractSymbolKind, Unit).rawValue + it[visibility] = descriptor.accept(ExtractSymbolVisibility, Unit).rawValue + it[extensionReceiverType] = extensionReceiverFqn?.toString() + } + } + + val finished = System.currentTimeMillis() + val count = Symbols.slice(Symbols.fqName.count()).selectAll().first()[Symbols.fqName.count()] + LOG.info("Updated symbol index in ${finished - started} ms! (${count} symbol(s))") + } + } catch (e: Exception) { + LOG.error("Error while updating symbol index") + LOG.printStackTrace(e) + } + + progress.close() + } + } + + fun query(prefix: String, receiverType: FqName? = null, limit: Int = 20): List = transaction(db) { + // TODO: Extension completion currently only works if the receiver matches exactly, + // ideally this should work with subtypes as well + (Symbols innerJoin FqNames) + .select { FqNames.shortName.like("$prefix%") and (Symbols.extensionReceiverType eq receiverType?.toString()) } + .limit(limit) + .map { Symbol( + fqName = FqName(it[Symbols.fqName]), + kind = Symbol.Kind.fromRaw(it[Symbols.kind]), + visibility = Symbol.Visibility.fromRaw(it[Symbols.visibility]), + extensionReceiverType = it[Symbols.extensionReceiverType]?.let(::FqName) + ) } + } + + private fun allDescriptors(module: ModuleDescriptor): Collection = allPackages(module) + .map(module::getPackage) + .flatMap { + try { + it.memberScope.getContributedDescriptors(DescriptorKindFilter.ALL, MemberScope.ALL_NAME_FILTER) + } catch (e: IllegalStateException) { + LOG.warn("Could not query descriptors in package $it") + emptyList() + } + } + + private fun allPackages(module: ModuleDescriptor, pkgName: FqName = FqName.ROOT): Collection = module + .getSubPackagesOf(pkgName) { it.toString() != "META-INF" } + .flatMap { setOf(it) + allPackages(module, it) } +} diff --git a/server/src/main/kotlin/org/javacs/kt/progress/LanguageClientProgress.kt b/server/src/main/kotlin/org/javacs/kt/progress/LanguageClientProgress.kt new file mode 100644 index 000000000..a57be17d4 --- /dev/null +++ b/server/src/main/kotlin/org/javacs/kt/progress/LanguageClientProgress.kt @@ -0,0 +1,51 @@ +package org.javacs.kt.progress + +import org.eclipse.lsp4j.services.LanguageClient +import org.eclipse.lsp4j.jsonrpc.messages.Either +import org.eclipse.lsp4j.ProgressParams +import org.eclipse.lsp4j.WorkDoneProgressNotification +import org.eclipse.lsp4j.WorkDoneProgressBegin +import org.eclipse.lsp4j.WorkDoneProgressReport +import org.eclipse.lsp4j.WorkDoneProgressEnd +import org.eclipse.lsp4j.WorkDoneProgressCreateParams +import java.util.concurrent.CompletableFuture +import java.util.UUID + +class LanguageClientProgress( + private val label: String, + private val token: Either, + private val client: LanguageClient +) : Progress { + init { + reportProgress(WorkDoneProgressBegin().also { + it.title = "Kotlin: $label" + it.percentage = 0 + }) + } + + override fun update(message: String?, percent: Int?) { + reportProgress(WorkDoneProgressReport().also { + it.message = message + it.percentage = percent + }) + } + + override fun close() { + reportProgress(WorkDoneProgressEnd()) + } + + private fun reportProgress(notification: WorkDoneProgressNotification) { + client.notifyProgress(ProgressParams(token, notification)) + } + + class Factory(private val client: LanguageClient) : Progress.Factory { + override fun create(label: String): CompletableFuture { + val token = Either.forLeft(UUID.randomUUID().toString()) + return client + .createProgress(WorkDoneProgressCreateParams().also { + it.token = token + }) + .thenApply { LanguageClientProgress(label, token, client) } + } + } +} diff --git a/server/src/main/kotlin/org/javacs/kt/progress/Progress.kt b/server/src/main/kotlin/org/javacs/kt/progress/Progress.kt new file mode 100644 index 000000000..b7c64ee15 --- /dev/null +++ b/server/src/main/kotlin/org/javacs/kt/progress/Progress.kt @@ -0,0 +1,32 @@ +package org.javacs.kt.progress + +import java.io.Closeable +import java.util.concurrent.CompletableFuture + +/** A facility for emitting progress notifications. */ +interface Progress : Closeable { + /** + * Updates the progress percentage. The + * value should be in the range [0, 100]. + */ + fun update(message: String? = null, percent: Int? = null) + + object None : Progress { + override fun update(message: String?, percent: Int?) {} + + override fun close() {} + } + + interface Factory { + /** + * Creates a new progress listener with + * the given label. The label is intended + * to be human-readable. + */ + fun create(label: String): CompletableFuture + + object None : Factory { + override fun create(label: String): CompletableFuture = CompletableFuture.completedFuture(Progress.None) + } + } +} diff --git a/server/src/main/resources/kotlinDSLClassPathFinder.gradle b/server/src/main/resources/kotlinDSLClassPathFinder.gradle index e4cfea175..24bfc3281 100644 --- a/server/src/main/resources/kotlinDSLClassPathFinder.gradle +++ b/server/src/main/resources/kotlinDSLClassPathFinder.gradle @@ -1,5 +1,5 @@ -import org.gradle.kotlin.dsl.accessors.AccessorsClassPathKt -import org.gradle.kotlin.dsl.accessors.PluginAccessorsClassPathKt +import org.gradle.kotlin.dsl.accessors.ProjectAccessorsClassPathGenerator +import org.gradle.kotlin.dsl.accessors.PluginAccessorClassPathGenerator import org.gradle.internal.classpath.ClassPath allprojects { project -> @@ -12,12 +12,12 @@ allprojects { project -> .forEach { System.out.println "kotlin-lsp-gradle $it" } // List dynamically generated Kotlin DSL accessors (e.g. the 'compile' configuration method) - AccessorsClassPathKt.projectAccessorsClassPath(project, ClassPath.EMPTY) + gradle.services.get(ProjectAccessorsClassPathGenerator.class).projectAccessorsClassPath(project, ClassPath.EMPTY) .bin .asFiles .forEach { System.out.println "kotlin-lsp-gradle $it" } - PluginAccessorsClassPathKt.pluginSpecBuildersClassPath(project) + gradle.services.get(PluginAccessorClassPathGenerator.class).pluginSpecBuildersClassPath(project) .bin .asFiles .forEach { System.out.println "kotlin-lsp-gradle $it" } diff --git a/server/src/test/kotlin/org/javacs/kt/LanguageServerTestFixture.kt b/server/src/test/kotlin/org/javacs/kt/LanguageServerTestFixture.kt index 6ceb706a2..06bc35cd3 100644 --- a/server/src/test/kotlin/org/javacs/kt/LanguageServerTestFixture.kt +++ b/server/src/test/kotlin/org/javacs/kt/LanguageServerTestFixture.kt @@ -36,6 +36,7 @@ abstract class LanguageServerTestFixture(relativeWorkspaceRoot: String) : Langua name = workspaceRoot.fileName.toString() uri = workspaceRoot.toUri().toString() }) + languageServer.sourcePath.indexEnabled = false languageServer.connect(this) languageServer.initialize(init).join()