diff --git a/server/src/main/kotlin/org/javacs/kt/CompiledFile.kt b/server/src/main/kotlin/org/javacs/kt/CompiledFile.kt index a6ba9de50..3cfbcf25b 100644 --- a/server/src/main/kotlin/org/javacs/kt/CompiledFile.kt +++ b/server/src/main/kotlin/org/javacs/kt/CompiledFile.kt @@ -42,7 +42,7 @@ class CompiledFile( bindingContextOf(expression, scopeWithImports).getType(expression) fun bindingContextOf(expression: KtExpression, scopeWithImports: LexicalScope): BindingContext = - classPath.compiler.compileExpression(expression, scopeWithImports, sourcePath, kind).first + classPath.compiler.compileKtExpression(expression, scopeWithImports, sourcePath, kind).first private fun expandForType(cursor: Int, surroundingExpr: KtExpression): KtExpression { val dotParent = surroundingExpr.parent as? KtDotQualifiedExpression @@ -98,7 +98,7 @@ class CompiledFile( val (surroundingContent, offset) = contentAndOffsetFromElement(psi, oldParent, asReference) val padOffset = " ".repeat(offset) - val recompile = classPath.compiler.createFile(padOffset + surroundingContent, Paths.get("dummy.virtual" + if (isScript) ".kts" else ".kt"), kind) + val recompile = classPath.compiler.createKtFile(padOffset + surroundingContent, Paths.get("dummy.virtual" + if (isScript) ".kts" else ".kt"), kind) return recompile.findElementAt(cursor)?.findParent() } diff --git a/server/src/main/kotlin/org/javacs/kt/Compiler.kt b/server/src/main/kotlin/org/javacs/kt/Compiler.kt index 4bd4eb834..d64a9f273 100644 --- a/server/src/main/kotlin/org/javacs/kt/Compiler.kt +++ b/server/src/main/kotlin/org/javacs/kt/Compiler.kt @@ -1,11 +1,13 @@ package org.javacs.kt import org.jetbrains.kotlin.com.intellij.codeInsight.NullableNotNullManager +import org.jetbrains.kotlin.com.intellij.lang.Language import org.jetbrains.kotlin.com.intellij.openapi.Disposable import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer import org.jetbrains.kotlin.com.intellij.openapi.vfs.StandardFileSystems import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFileManager import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFileSystem +import org.jetbrains.kotlin.com.intellij.psi.PsiFile import org.jetbrains.kotlin.com.intellij.psi.PsiFileFactory import org.jetbrains.kotlin.com.intellij.mock.MockProject import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys @@ -14,6 +16,7 @@ import org.jetbrains.kotlin.cli.jvm.compiler.CliBindingTrace import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM +import org.jetbrains.kotlin.cli.jvm.config.addJavaSourceRoots import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots import org.jetbrains.kotlin.cli.jvm.plugins.PluginCliParser import org.jetbrains.kotlin.config.CommonConfigurationKeys @@ -33,7 +36,7 @@ import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.BindingTraceContext import org.jetbrains.kotlin.resolve.LazyTopDownAnalyzer -import org.jetbrains.kotlin.resolve.TopDownAnalysisMode.TopLevelDeclarations +import org.jetbrains.kotlin.resolve.TopDownAnalysisMode import org.jetbrains.kotlin.resolve.calls.components.InferenceSession import org.jetbrains.kotlin.resolve.calls.smartcasts.DataFlowInfo import org.jetbrains.kotlin.resolve.extensions.ExtraImportsProviderExtension @@ -84,6 +87,7 @@ private val GRADLE_DSL_DEPENDENCY_PATTERN = Regex("^gradle-(?:kotlin-dsl|core).* * files and expressions. */ private class CompilationEnvironment( + javaSourcePath: Set, classPath: Set ) : Closeable { private val disposable = Disposer.newDisposable() @@ -110,7 +114,9 @@ private class CompilationEnvironment( put(CommonConfigurationKeys.LANGUAGE_VERSION_SETTINGS, languageVersionSettings) put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, LoggingMessageCollector) add(ComponentRegistrar.PLUGIN_COMPONENT_REGISTRARS, ScriptingCompilerConfigurationComponentRegistrar()) + addJvmClasspathRoots(classPath.map { it.toFile() }) + addJavaSourceRoots(javaSourcePath.map { it.toFile() }) // Setup script templates (e.g. used by Gradle's Kotlin DSL) val scriptDefinitions: MutableList = mutableListOf(ScriptDefinition.getDefault(defaultJvmScriptingHostConfiguration)) @@ -187,13 +193,14 @@ private class CompilationEnvironment( fun createContainer(sourcePath: Collection): Pair { val trace = CliBindingTrace() val container = TopDownAnalyzerFacadeForJVM.createContainer( - project = environment.project, - files = listOf(), - trace = trace, - configuration = environment.configuration, - packagePartProvider = environment::createPackagePartProvider, - // TODO FileBasedDeclarationProviderFactory keeps indices, re-use it across calls - declarationProviderFactory = { storageManager, _ -> FileBasedDeclarationProviderFactory(storageManager, sourcePath) }) + project = environment.project, + files = sourcePath, + trace = trace, + configuration = environment.configuration, + packagePartProvider = environment::createPackagePartProvider, + // TODO FileBasedDeclarationProviderFactory keeps indices, re-use it across calls + declarationProviderFactory = ::FileBasedDeclarationProviderFactory + ) return Pair(container, trace) } @@ -217,12 +224,12 @@ enum class CompilationKind { * Incrementally compiles files and expressions. * The basic strategy for compiling one file at-a-time is outlined in OneFilePerformance. */ -class Compiler(classPath: Set, buildScriptClassPath: Set = emptySet()) : Closeable { +class Compiler(javaSourcePath: Set, classPath: Set, buildScriptClassPath: Set = emptySet()) : Closeable { private var closed = false private val localFileSystem: VirtualFileSystem - private val defaultCompileEnvironment = CompilationEnvironment(classPath) - private val buildScriptCompileEnvironment = buildScriptClassPath.takeIf { it.isNotEmpty() }?.let(::CompilationEnvironment) + private val defaultCompileEnvironment = CompilationEnvironment(javaSourcePath, classPath) + private val buildScriptCompileEnvironment = buildScriptClassPath.takeIf { it.isNotEmpty() }?.let { CompilationEnvironment(emptySet(), it) } private val compileLock = ReentrantLock() // TODO: Lock at file-level companion object { @@ -244,26 +251,25 @@ class Compiler(classPath: Set, buildScriptClassPath: Set = emptySet( buildScriptCompileEnvironment?.updateConfiguration(config) } - fun createFile(content: String, file: Path = Paths.get("dummy.virtual.kt"), kind: CompilationKind = CompilationKind.DEFAULT): KtFile { + fun createPsiFile(content: String, file: Path = Paths.get("dummy.virtual.kt"), language: Language = KotlinLanguage.INSTANCE, kind: CompilationKind = CompilationKind.DEFAULT): PsiFile { assert(!content.contains('\r')) - val new = psiFileFactoryFor(kind).createFileFromText(file.toString(), KotlinLanguage.INSTANCE, content, true, false) as KtFile + val new = psiFileFactoryFor(kind).createFileFromText(file.toString(), language, content, true, false) assert(new.virtualFile != null) return new } - fun createExpression(content: String, file: Path = Paths.get("dummy.virtual.kt"), kind: CompilationKind = CompilationKind.DEFAULT): KtExpression { - val property = parseDeclaration("val x = $content", file, kind) as KtProperty + fun createKtFile(content: String, file: Path = Paths.get("dummy.virtual.kt"), kind: CompilationKind = CompilationKind.DEFAULT): KtFile = + createPsiFile(content, file, language = KotlinLanguage.INSTANCE, kind = kind) as KtFile + fun createKtExpression(content: String, file: Path = Paths.get("dummy.virtual.kt"), kind: CompilationKind = CompilationKind.DEFAULT): KtExpression { + val property = createKtDeclaration("val x = $content", file, kind) as KtProperty return property.initializer!! } - fun createDeclaration(content: String, file: Path = Paths.get("dummy.virtual.kt"), kind: CompilationKind = CompilationKind.DEFAULT): KtDeclaration = - parseDeclaration(content, file, kind) - - private fun parseDeclaration(content: String, file: Path, kind: CompilationKind = CompilationKind.DEFAULT): KtDeclaration { - val parse = createFile(content, file, kind) + fun createKtDeclaration(content: String, file: Path = Paths.get("dummy.virtual.kt"), kind: CompilationKind = CompilationKind.DEFAULT): KtDeclaration { + val parse = createKtFile(content, file, kind) val declarations = parse.declarations assert(declarations.size == 1) { "${declarations.size} declarations in $content" } @@ -288,25 +294,24 @@ class Compiler(classPath: Set, buildScriptClassPath: Set = emptySet( fun psiFileFactoryFor(kind: CompilationKind): PsiFileFactory = PsiFileFactory.getInstance(compileEnvironmentFor(kind).environment.project) - fun compileFile(file: KtFile, sourcePath: Collection, kind: CompilationKind = CompilationKind.DEFAULT): Pair = - compileFiles(listOf(file), sourcePath, kind) + fun compileKtFile(file: KtFile, sourcePath: Collection, kind: CompilationKind = CompilationKind.DEFAULT): Pair = + compileKtFiles(listOf(file), sourcePath, kind) - fun compileFiles(files: Collection, sourcePath: Collection, kind: CompilationKind = CompilationKind.DEFAULT): Pair { + fun compileKtFiles(files: Collection, sourcePath: Collection, kind: CompilationKind = CompilationKind.DEFAULT): Pair { if (kind == CompilationKind.BUILD_SCRIPT) { // Print the (legacy) script template used by the compiled Kotlin DSL build file files.forEach { LOG.debug { "$it -> ScriptDefinition: ${it.findScriptDefinition()?.asLegacyOrNull()?.template?.simpleName}" } } } compileLock.withLock { - val (container, trace) = compileEnvironmentFor(kind).createContainer(sourcePath) - val topDownAnalyzer = container.get() - topDownAnalyzer.analyzeDeclarations(TopLevelDeclarations, files) - + val compileEnv = compileEnvironmentFor(kind) + val (container, trace) = compileEnv.createContainer(sourcePath) + container.get().analyzeDeclarations(TopDownAnalysisMode.TopLevelDeclarations, files) return Pair(trace.bindingContext, container) } } - fun compileExpression(expression: KtExpression, scopeWithImports: LexicalScope, sourcePath: Collection, kind: CompilationKind = CompilationKind.DEFAULT): Pair { + fun compileKtExpression(expression: KtExpression, scopeWithImports: LexicalScope, sourcePath: Collection, kind: CompilationKind = CompilationKind.DEFAULT): Pair { try { // Use same lock as 'compileFile' to avoid concurrency issues such as #42 compileLock.withLock { diff --git a/server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt b/server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt index 83274a42b..d2370f77e 100644 --- a/server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt +++ b/server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt @@ -2,51 +2,70 @@ package org.javacs.kt import org.javacs.kt.classpath.defaultClassPathResolver import java.io.Closeable +import java.nio.file.Files +import java.nio.file.FileSystems import java.nio.file.Path +/** + * Manages the class path (compiled JARs, etc), the Java source path + * and the compiler. Note that Kotlin sources are stored in SourcePath. + */ class CompilerClassPath(private val config: CompilerConfiguration) : Closeable { private val workspaceRoots = mutableSetOf() + private val javaSourcePath = mutableSetOf() private val classPath = mutableSetOf() private val buildScriptClassPath = mutableSetOf() - var compiler = Compiler(classPath, buildScriptClassPath) + var compiler = Compiler(javaSourcePath, classPath, buildScriptClassPath) private set init { compiler.updateConfiguration(config) } - private fun refresh(updateBuildScriptClassPath: Boolean = true) { + /** Updates and possibly reinstantiates the compiler using new paths. */ + private fun refresh( + updateClassPath: Boolean = true, + updateBuildScriptClassPath: Boolean = true, + updateJavaSourcePath: Boolean = false + ): Boolean { // TODO: Fetch class path and build script class path concurrently (and asynchronously) val resolver = defaultClassPathResolver(workspaceRoots) - var refreshCompiler = false + var refreshCompiler = updateJavaSourcePath - val newClassPath = resolver.classpathOrEmpty - if (newClassPath != classPath) { - syncClassPath(classPath, newClassPath) - refreshCompiler = true + if (updateClassPath) { + val newClassPath = resolver.classpathOrEmpty + if (newClassPath != classPath) { + syncPaths(classPath, newClassPath, "class path") + refreshCompiler = true + } } if (updateBuildScriptClassPath) { + LOG.info("Update build script path") val newBuildScriptClassPath = resolver.buildScriptClasspathOrEmpty if (newBuildScriptClassPath != buildScriptClassPath) { - syncClassPath(buildScriptClassPath, newBuildScriptClassPath) + syncPaths(buildScriptClassPath, newBuildScriptClassPath, "class path") refreshCompiler = true } } if (refreshCompiler) { + LOG.info("Reinstantiating compiler") compiler.close() - compiler = Compiler(classPath, buildScriptClassPath) + compiler = Compiler(javaSourcePath, classPath, buildScriptClassPath) updateCompilerConfiguration() } + + return refreshCompiler } - private fun syncClassPath(dest: MutableSet, new: Set) { + /** Synchronizes the given two path sets and logs the differences. */ + private fun syncPaths(dest: MutableSet, new: Set, name: String) { val added = new - dest val removed = dest - new - logAdded(added) - logRemoved(removed) + logAdded(added, name) + logRemoved(removed, name) dest.removeAll(removed) dest.addAll(added) @@ -56,53 +75,77 @@ class CompilerClassPath(private val config: CompilerConfiguration) : Closeable { compiler.updateConfiguration(config) } - fun addWorkspaceRoot(root: Path) { - LOG.info("Searching for dependencies in workspace root {}", root) + fun addWorkspaceRoot(root: Path): Boolean { + LOG.info("Searching for dependencies and Java sources in workspace root {}", root) workspaceRoots.add(root) + javaSourcePath.addAll(findJavaSourceFiles(root)) - refresh() + return refresh(updateJavaSourcePath = true) } - fun removeWorkspaceRoot(root: Path) { - LOG.info("Remove dependencies from workspace root {}", root) + fun removeWorkspaceRoot(root: Path): Boolean { + LOG.info("Removing dependencies and Java source path from workspace root {}", root) workspaceRoots.remove(root) + javaSourcePath.removeAll(findJavaSourceFiles(root)) - refresh() + return refresh(updateJavaSourcePath = true) } - fun createdOnDisk(file: Path) { - changedOnDisk(file) + fun createdOnDisk(file: Path): Boolean { + if (isJavaSource(file)) { + javaSourcePath.add(file) + } + return changedOnDisk(file) } - fun deletedOnDisk(file: Path) { - changedOnDisk(file) + fun deletedOnDisk(file: Path): Boolean { + if (isJavaSource(file)) { + javaSourcePath.remove(file) + } + return changedOnDisk(file) } - fun changedOnDisk(file: Path) { - val name = file.fileName.toString() - if (name == "pom.xml" || name == "build.gradle" || name == "build.gradle.kts") - refresh(updateBuildScriptClassPath = false) + fun changedOnDisk(file: Path): Boolean { + val buildScript = isBuildScript(file) + val javaSource = isJavaSource(file) + if (buildScript || javaSource) { + return refresh(updateClassPath = buildScript, updateBuildScriptClassPath = false, updateJavaSourcePath = javaSource) + } else { + return false + } } + private fun isJavaSource(file: Path): Boolean = file.fileName.toString().endsWith(".java") + + private fun isBuildScript(file: Path): Boolean = file.fileName.toString().let { it == "pom.xml" || it == "build.gradle" || it == "build.gradle.kts" } + override fun close() { compiler.close() } } -private fun logAdded(sources: Collection) { +private fun findJavaSourceFiles(root: Path): Set { + val sourceMatcher = FileSystems.getDefault().getPathMatcher("glob:*.java") + return SourceExclusions(root) + .walkIncluded() + .filter { sourceMatcher.matches(it.fileName) } + .toSet() +} + +private fun logAdded(sources: Collection, name: String) { when { sources.isEmpty() -> return - sources.size > 5 -> LOG.info("Adding {} files to class path", sources.size) - else -> LOG.info("Adding {} to class path", sources) + sources.size > 5 -> LOG.info("Adding {} files to {}", sources.size, name) + else -> LOG.info("Adding {} to {}", sources, name) } } -private fun logRemoved(sources: Collection) { +private fun logRemoved(sources: Collection, name: String) { when { sources.isEmpty() -> return - sources.size > 5 -> LOG.info("Removing {} files from class path", sources.size) - else -> LOG.info("Removing {} from class path", sources) + sources.size > 5 -> LOG.info("Removing {} files from {}", sources.size, name) + else -> LOG.info("Removing {} from {}", sources, name) } } diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt b/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt index 7a184c650..5c98cdc8d 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt @@ -73,7 +73,10 @@ class KotlinLanguageServer : LanguageServer, LanguageClientAware, Closeable { val root = Paths.get(parseURI(params.rootUri)) sourceFiles.addWorkspaceRoot(root) - classPath.addWorkspaceRoot(root) + val refreshed = classPath.addWorkspaceRoot(root) + if (refreshed) { + sourcePath.refresh() + } } return completedFuture(InitializeResult(serverCapabilities)) diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt b/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt index f69192f16..5acca1cbc 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt @@ -66,15 +66,15 @@ class KotlinWorkspaceService( when (change.type) { FileChangeType.Created -> { sf.createdOnDisk(uri) - path?.let(cp::createdOnDisk) + path?.let(cp::createdOnDisk)?.let { if (it) sp.refresh() } } FileChangeType.Deleted -> { sf.deletedOnDisk(uri) - path?.let(cp::deletedOnDisk) + path?.let(cp::deletedOnDisk)?.let { if (it) sp.refresh() } } FileChangeType.Changed -> { sf.changedOnDisk(uri) - path?.let(cp::changedOnDisk) + path?.let(cp::changedOnDisk)?.let { if (it) sp.refresh() } } null -> { // Nothing to do @@ -147,7 +147,10 @@ class KotlinWorkspaceService( val root = Paths.get(parseURI(change.uri)) sf.addWorkspaceRoot(root) - cp.addWorkspaceRoot(root) + val refreshed = cp.addWorkspaceRoot(root) + if (refreshed) { + sp.refresh() + } } for (change in params.event.removed) { LOG.info("Dropping workspace {} from source path", change.uri) @@ -155,7 +158,10 @@ class KotlinWorkspaceService( val root = Paths.get(parseURI(change.uri)) sf.removeWorkspaceRoot(root) - cp.removeWorkspaceRoot(root) + val refreshed = cp.removeWorkspaceRoot(root) + if (refreshed) { + sp.refresh() + } } } } diff --git a/server/src/main/kotlin/org/javacs/kt/SourceFiles.kt b/server/src/main/kotlin/org/javacs/kt/SourceFiles.kt index d07b69e72..b1d67e9b0 100644 --- a/server/src/main/kotlin/org/javacs/kt/SourceFiles.kt +++ b/server/src/main/kotlin/org/javacs/kt/SourceFiles.kt @@ -1,6 +1,9 @@ package org.javacs.kt import org.jetbrains.kotlin.com.intellij.openapi.util.text.StringUtil.convertLineSeparators +import org.jetbrains.kotlin.com.intellij.lang.java.JavaLanguage +import org.jetbrains.kotlin.com.intellij.lang.Language +import org.jetbrains.kotlin.idea.KotlinLanguage import org.eclipse.lsp4j.TextDocumentContentChangeEvent import org.javacs.kt.util.KotlinLSException import org.javacs.kt.util.filePath @@ -17,9 +20,8 @@ import java.nio.file.FileSystems import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths -import java.util.stream.Collectors -private class SourceVersion(val content: String, val version: Int, val isTemporary: Boolean) +private class SourceVersion(val content: String, val version: Int, val language: Language?, val isTemporary: Boolean) /** * Notify SourcePath whenever a file changes @@ -33,7 +35,7 @@ private class NotifySourcePath(private val sp: SourcePath) { val content = convertLineSeparators(source.content) files[uri] = source - sp.put(uri, content, source.isTemporary) + sp.put(uri, content, source.language, source.isTemporary) } fun remove(uri: URI) { @@ -71,7 +73,7 @@ class SourceFiles( private val open = mutableSetOf() fun open(uri: URI, content: String, version: Int) { - files[uri] = SourceVersion(content, version, isTemporary = !exclusions.isURIIncluded(uri)) + files[uri] = SourceVersion(content, version, languageOf(uri), isTemporary = !exclusions.isURIIncluded(uri)) open.add(uri) } @@ -107,7 +109,7 @@ class SourceFiles( else newText = patch(newText, change) } - files[uri] = SourceVersion(newText, newVersion, existing.isTemporary) + files[uri] = SourceVersion(newText, newVersion, existing.language, existing.isTemporary) } } @@ -116,19 +118,21 @@ class SourceFiles( } fun deletedOnDisk(uri: URI) { - if (isSource(uri)) + if (isSource(uri)) { files.remove(uri) + } } fun changedOnDisk(uri: URI) { - if (isSource(uri)) + if (isSource(uri)) { files[uri] = readFromDisk(uri, files[uri]?.isTemporary ?: true) ?: throw KotlinLSException("Could not read source file '$uri' after being changed on disk") + } } private fun readFromDisk(uri: URI, temporary: Boolean): SourceVersion? = try { val content = contentProvider.contentOf(uri) - SourceVersion(content, -1, isTemporary = temporary) + SourceVersion(content, -1, languageOf(uri), isTemporary = temporary) } catch (e: FileNotFoundException) { null } catch (e: IOException) { @@ -136,9 +140,14 @@ class SourceFiles( null } - private fun isSource(uri: URI): Boolean { - val path = uri.path - return (path.endsWith(".kt") || path.endsWith(".kts")) && exclusions.isURIIncluded(uri) + private fun isSource(uri: URI): Boolean = exclusions.isURIIncluded(uri) && languageOf(uri) != null + + private fun languageOf(uri: URI): Language? { + val fileName = uri.filePath?.fileName?.toString() ?: return null + return when { + fileName.endsWith(".kt") || fileName.endsWith(".kts") -> KotlinLanguage.INSTANCE + else -> null + } } fun addWorkspaceRoot(root: Path) { @@ -146,10 +155,10 @@ class SourceFiles( logAdded(addSources, root) - for (file in addSources) { - readFromDisk(file.toUri(), temporary = false)?.let { - files[file.toUri()] = it - } ?: LOG.warn("Could not read source file '{}'", file) + for (uri in addSources) { + readFromDisk(uri, temporary = false)?.let { + files[uri] = it + } ?: LOG.warn("Could not read source file '{}'", uri.path) } workspaceRoots.add(root) @@ -159,7 +168,7 @@ class SourceFiles( fun removeWorkspaceRoot(root: Path) { val rmSources = files.keys.filter { it.filePath?.startsWith(root) ?: false } - logRemoved(rmSources.map(Paths::get), root) + logRemoved(rmSources, root) files.removeAll(rmSources) workspaceRoots.remove(root) @@ -205,18 +214,19 @@ private fun patch(sourceText: String, change: TextDocumentContentChangeEvent): S } } -private fun findSourceFiles(root: Path): Set { - val pattern = FileSystems.getDefault().getPathMatcher("glob:*.{kt,kts}") - val exclusions = SourceExclusions(root) - return Files.walk(root) - .filter { pattern.matches(it.fileName) && exclusions.isPathIncluded(it) } - .collect(Collectors.toSet()) +private fun findSourceFiles(root: Path): Set { + val sourceMatcher = FileSystems.getDefault().getPathMatcher("glob:*.{kt,kts}") + return SourceExclusions(root) + .walkIncluded() + .filter { sourceMatcher.matches(it.fileName) } + .map(Path::toUri) + .toSet() } -private fun logAdded(sources: Collection, rootPath: Path?) { - LOG.info("Adding {} under {} to source path", describeURIs(sources.map(Path::toUri)), rootPath) +private fun logAdded(sources: Collection, rootPath: Path?) { + LOG.info("Adding {} under {} to source path", describeURIs(sources), rootPath) } -private fun logRemoved(sources: Collection, rootPath: Path?) { - LOG.info("Removing {} under {} to source path", describeURIs(sources.map(Path::toUri)), rootPath) +private fun logRemoved(sources: Collection, rootPath: Path?) { + LOG.info("Removing {} under {} to source path", describeURIs(sources), rootPath) } diff --git a/server/src/main/kotlin/org/javacs/kt/SourcePath.kt b/server/src/main/kotlin/org/javacs/kt/SourcePath.kt index fb89a9a21..d49956c34 100644 --- a/server/src/main/kotlin/org/javacs/kt/SourcePath.kt +++ b/server/src/main/kotlin/org/javacs/kt/SourcePath.kt @@ -1,7 +1,10 @@ package org.javacs.kt +import org.javacs.kt.util.fileExtension import org.javacs.kt.util.filePath import org.javacs.kt.util.describeURI +import org.jetbrains.kotlin.com.intellij.lang.Language +import org.jetbrains.kotlin.com.intellij.psi.PsiFile import org.jetbrains.kotlin.container.ComponentProvider import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.resolve.BindingContext @@ -29,9 +32,11 @@ class SourcePath( var compiledFile: KtFile? = null, var compiledContext: BindingContext? = null, var compiledContainer: ComponentProvider? = null, + val language: Language? = null, val isTemporary: Boolean = false // A temporary source file will not be returned by .all() ) { - val isScript: Boolean = uri.toString().endsWith(".kts") + val extension: String? = uri.fileExtension ?: language?.associatedFileType?.defaultExtension + val isScript: Boolean = extension == "kts" val kind: CompilationKind = if (path?.fileName?.toString()?.endsWith(".gradle.kts") ?: false) CompilationKind.BUILD_SCRIPT else CompilationKind.DEFAULT @@ -40,43 +45,55 @@ class SourcePath( content = newContent } - fun parseIfChanged(): SourceFile { + fun clean() { + parsed = null + compiledFile = null + compiledContext = null + compiledContainer = null + } + + fun parse() { + // TODO: Create PsiFile using the stored language instead + parsed = cp.compiler.createKtFile(content, path ?: Paths.get("sourceFile.virtual.$extension"), kind) + } + + fun parseIfChanged() { if (content != parsed?.text) { - parsed = cp.compiler.createFile(content, path ?: Paths.get("sourceFile.virtual.kt"), kind) + parse() } - - return this } - fun compileIfNull(): SourceFile = - parseIfChanged().doCompileIfNull() + fun compileIfNull() = parseIfChanged().apply { doCompileIfNull() } - private fun doCompileIfNull(): SourceFile = - if (compiledFile == null) + private fun doCompileIfNull() { + if (compiledFile == null) { doCompileIfChanged() - else - this + } + } - fun compileIfChanged(): SourceFile = - parseIfChanged().doCompileIfChanged() + fun compileIfChanged() = parseIfChanged().apply { doCompileIfChanged() } - private fun doCompileIfChanged(): SourceFile { - if (parsed?.text != compiledFile?.text) { - LOG.debug("Compiling {}", path?.fileName) + fun compile() = parse().apply { doCompile() } - val (context, container) = cp.compiler.compileFile(parsed!!, allIncludingThis(), kind) - parseDataWriteLock.withLock { - compiledContext = context - compiledContainer = container - compiledFile = parsed - } + private fun doCompile() { + LOG.debug("Compiling {}", path?.fileName) + + val (context, container) = cp.compiler.compileKtFile(parsed!!, allIncludingThis(), kind) + parseDataWriteLock.withLock { + compiledContext = context + compiledContainer = container + compiledFile = parsed } + } - return this + private fun doCompileIfChanged() { + if (parsed?.text != compiledFile?.text) { + doCompile() + } } fun prepareCompiledFile(): CompiledFile = - parseIfChanged().compileIfNull().doPrepareCompiledFile() + parseIfChanged().apply { compileIfNull() }.let { doPrepareCompiledFile() } private fun doPrepareCompiledFile(): CompiledFile = CompiledFile(content, compiledFile!!, compiledContext!!, compiledContainer!!, allIncludingThis(), cp, isScript, kind) @@ -92,12 +109,12 @@ class SourcePath( // Fallback solution, usually *all* source files // should be added/opened through SourceFiles LOG.warn("Requested source file {} is not on source path, this is most likely a bug. Adding it now temporarily...", describeURI(uri)) - put(uri, contentProvider.contentOf(uri), temporary = true) + put(uri, contentProvider.contentOf(uri), null, temporary = true) } return files[uri]!! } - fun put(uri: URI, content: String, temporary: Boolean = false) { + fun put(uri: URI, content: String, language: Language?, temporary: Boolean = false) { assert(!content.contains('\r')) if (temporary) { @@ -107,7 +124,7 @@ class SourcePath( if (uri in files) { sourceFile(uri).put(content) } else { - files[uri] = SourceFile(uri, content, isTemporary = temporary) + files[uri] = SourceFile(uri, content, language = language, isTemporary = temporary) } } @@ -129,13 +146,13 @@ class SourcePath( */ fun content(uri: URI): String = sourceFile(uri).content - fun parsedFile(uri: URI): KtFile = sourceFile(uri).parseIfChanged().parsed!! + fun parsedFile(uri: URI): KtFile = sourceFile(uri).apply { parseIfChanged() }.parsed!! /** * Compile the latest version of a file */ fun currentVersion(uri: URI): CompiledFile = - sourceFile(uri).compileIfChanged().prepareCompiledFile() + sourceFile(uri).apply { compileIfChanged() }.prepareCompiledFile() /** * Return whatever is the most-recent already-compiled version of `file` @@ -155,10 +172,10 @@ class SourcePath( // Compile changed files fun compileAndUpdate(changed: List, kind: CompilationKind): BindingContext? { if (changed.isEmpty()) return null - val parse = changed.associateWith { it.parseIfChanged().parsed!! } + val parse = changed.associateWith { it.apply { parseIfChanged() }.parsed!! } val allFiles = all() beforeCompileCallback.invoke() - val (context, container) = cp.compiler.compileFiles(parse.values, allFiles, kind) + val (context, container) = cp.compiler.compileKtFiles(parse.values, allFiles, kind) // Update cache for ((f, parsed) in parse) { @@ -185,11 +202,23 @@ class SourcePath( return CompositeBindingContext.create(combined) } + /** + * Recompiles all source files that are initialized. + */ + fun refresh() { + val initialized = files.values.any { it.parsed != null } + if (initialized) { + LOG.info("Refreshing source path") + files.values.forEach { it.clean() } + files.values.forEach { it.compile() } + } + } + /** * Get parsed trees for all .kt files on source path */ fun all(includeHidden: Boolean = false): Collection = files.values .filter { includeHidden || !it.isTemporary } - .map { it.parseIfChanged().parsed!! } + .map { it.apply { parseIfChanged() }.parsed!! } } diff --git a/server/src/test/kotlin/org/javacs/kt/CompiledFileTest.kt b/server/src/test/kotlin/org/javacs/kt/CompiledFileTest.kt index dcabede4a..5fc830c1c 100644 --- a/server/src/test/kotlin/org/javacs/kt/CompiledFileTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/CompiledFileTest.kt @@ -15,13 +15,13 @@ class CompiledFileTest { } } - fun compileFile(): CompiledFile = Compiler(setOf()).use { compiler -> + fun compileFile(): CompiledFile = Compiler(setOf(), setOf()).use { compiler -> val file = testResourcesRoot().resolve("compiledFile/CompiledFileExample.kt") val content = Files.readAllLines(file).joinToString("\n") - val parse = compiler.createFile(content, file) + val parse = compiler.createKtFile(content, file) val classPath = CompilerClassPath(CompilerConfiguration()) val sourcePath = listOf(parse) - val (context, container) = compiler.compileFiles(sourcePath, sourcePath) + val (context, container) = compiler.compileKtFiles(sourcePath, sourcePath) CompiledFile(content, parse, context, container, sourcePath, classPath) } diff --git a/server/src/test/kotlin/org/javacs/kt/CompilerTest.kt b/server/src/test/kotlin/org/javacs/kt/CompilerTest.kt index 10e8c2b31..8cfe4035d 100644 --- a/server/src/test/kotlin/org/javacs/kt/CompilerTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/CompilerTest.kt @@ -13,7 +13,7 @@ import org.junit.BeforeClass import java.nio.file.Files class CompilerTest { - val compiler = Compiler(setOf()) + val compiler = Compiler(setOf(), setOf()) val myTestResources = testResourcesRoot().resolve("compiler") val file = myTestResources.resolve("FileToEdit.kt") val editedText = """ @@ -29,8 +29,8 @@ private class FileToEdit { @Test fun compileFile() { val content = Files.readAllLines(file).joinToString("\n") - val original = compiler.createFile(content, file) - val (context, _) = compiler.compileFile(original, listOf(original)) + val original = compiler.createKtFile(content, file) + val (context, _) = compiler.compileKtFile(original, listOf(original)) val psi = original.findElementAt(45)!! val kt = psi.parentsWithSelf.filterIsInstance().first() @@ -38,8 +38,8 @@ private class FileToEdit { } @Test fun newFile() { - val original = compiler.createFile(editedText, file) - val (context, _) = compiler.compileFile(original, listOf(original)) + val original = compiler.createKtFile(editedText, file) + val (context, _) = compiler.compileKtFile(original, listOf(original)) val psi = original.findElementAt(46)!! val kt = psi.parentsWithSelf.filterIsInstance().first() @@ -48,15 +48,15 @@ private class FileToEdit { @Test fun editFile() { val content = Files.readAllLines(file).joinToString("\n") - val original = compiler.createFile(content, file) - var (context, _) = compiler.compileFile(original, listOf(original)) + val original = compiler.createKtFile(content, file) + var (context, _) = compiler.compileKtFile(original, listOf(original)) var psi = original.findElementAt(46)!! var kt = psi.parentsWithSelf.filterIsInstance().first() assertThat(context.getType(kt), hasToString("String")) - val edited = compiler.createFile(editedText, file) - context = compiler.compileFile(edited, listOf(edited)).first + val edited = compiler.createKtFile(editedText, file) + context = compiler.compileKtFile(edited, listOf(edited)).first psi = edited.findElementAt(46)!! kt = psi.parentsWithSelf.filterIsInstance().first() @@ -66,12 +66,12 @@ private class FileToEdit { @Test fun editRef() { val file1 = testResourcesRoot().resolve("hover/Recover.kt") val content = Files.readAllLines(file1).joinToString("\n") - val original = compiler.createFile(content, file1) - val (context, _) = compiler.compileFile(original, listOf(original)) + val original = compiler.createKtFile(content, file1) + val (context, _) = compiler.compileKtFile(original, listOf(original)) val function = original.findElementAt(49)!!.parentsWithSelf.filterIsInstance().first() val scope = context.get(BindingContext.LEXICAL_SCOPE, function.bodyExpression)!! - val recompile = compiler.createDeclaration("""private fun singleExpressionFunction() = intFunction()""") - val (recompileContext, _) = compiler.compileExpression(recompile, scope, setOf(original)) + val recompile = compiler.createKtDeclaration("""private fun singleExpressionFunction() = intFunction()""") + val (recompileContext, _) = compiler.compileKtExpression(recompile, scope, setOf(original)) val intFunctionRef = recompile.findElementAt(41)!!.parentsWithSelf.filterIsInstance().first() val target = recompileContext.get(BindingContext.REFERENCE_TARGET, intFunctionRef)!! diff --git a/shared/src/main/kotlin/org/javacs/kt/SourceExclusions.kt b/shared/src/main/kotlin/org/javacs/kt/SourceExclusions.kt index f969cfa7a..82cc6098f 100644 --- a/shared/src/main/kotlin/org/javacs/kt/SourceExclusions.kt +++ b/shared/src/main/kotlin/org/javacs/kt/SourceExclusions.kt @@ -1,24 +1,36 @@ package org.javacs.kt import org.javacs.kt.util.filePath +import java.io.File import java.net.URI +import java.nio.file.FileSystems import java.nio.file.Path import java.nio.file.Paths // TODO: Read exclusions from gitignore/settings.json/... instead of // hardcoding them class SourceExclusions(private val workspaceRoots: Collection) { - private val excludedFolders = listOf("bin", "build", "target", "node_modules") + private val excludedPatterns = listOf(".*", "bin", "build", "node_modules", "target").map { FileSystems.getDefault().getPathMatcher("glob:$it") } - constructor(workspaceRoot: Path) : this(listOf(workspaceRoot)) {} + constructor(workspaceRoot: Path) : this(listOf(workspaceRoot)) {} + /** Finds all non-excluded files recursively. */ + fun walkIncluded(): Sequence = workspaceRoots.asSequence().flatMap { root -> + root.toFile() + .walk() + .onEnter { isPathIncluded(it.toPath()) } + .map { it.toPath() } + } + + /** Tests whether the given URI is not excluded. */ fun isURIIncluded(uri: URI) = uri.filePath?.let(this::isPathIncluded) ?: false + /** Tests whether the given path is not excluded. */ fun isPathIncluded(file: Path): Boolean = workspaceRoots.any { file.startsWith(it) } - && excludedFolders.none { + && excludedPatterns.none { pattern -> workspaceRoots .mapNotNull { if (file.startsWith(it)) it.relativize(file) else null } .flatMap { it } // Extract path segments - .any { segment -> segment.toString() == it } + .any(pattern::matches) } } diff --git a/shared/src/main/kotlin/org/javacs/kt/util/URIs.kt b/shared/src/main/kotlin/org/javacs/kt/util/URIs.kt index 34cc9145d..1320f6c18 100644 --- a/shared/src/main/kotlin/org/javacs/kt/util/URIs.kt +++ b/shared/src/main/kotlin/org/javacs/kt/util/URIs.kt @@ -15,6 +15,14 @@ fun parseURI(uri: String): URI = URI.create(runCatching { URLDecoder.decode(uri, val URI.filePath: Path? get() = runCatching { Paths.get(this) }.getOrNull() +/** Fetches the file extension WITHOUT the dot. */ +val URI.fileExtension: String? + get() { + val str = toString() + val dotOffset = str.lastIndexOf(".") + return if (dotOffset < 0) null else str.substring(dotOffset + 1) + } + fun describeURIs(uris: Collection): String = if (uris.isEmpty()) "0 files" else if (uris.size > 5) "${uris.size} files" diff --git a/shared/src/main/kotlin/org/javacs/kt/util/Utils.kt b/shared/src/main/kotlin/org/javacs/kt/util/Utils.kt index 45de0a846..f6c1cbadb 100644 --- a/shared/src/main/kotlin/org/javacs/kt/util/Utils.kt +++ b/shared/src/main/kotlin/org/javacs/kt/util/Utils.kt @@ -2,10 +2,12 @@ package org.javacs.kt.util import org.javacs.kt.LOG import java.io.PrintStream +import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths import java.net.URI import java.util.concurrent.CompletableFuture +import java.util.stream.Stream fun execAndReadStdout(shellCommand: String, directory: Path): String { val process = Runtime.getRuntime().exec(shellCommand, null, directory.toFile())