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