Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 54
ADFA-3320: introduce KtFileManager to manage parsed KtFile instances#1143
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
1b069cba70fd664ca1e9743286a6f1fe62f6e6d8b3b208658c844ad258db2cbbf7acd1dc62a514b1c8e4b14f6ee54ca7a96e4d4585d841a34b7b0f2e2f137a09fc9eadd3d519d6defa61586b8f85807caabecf1a68e316a4988c26d54e63a6d858b3File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.itsaky.androidide.lsp.kotlin | ||
| import java.nio.file.Path | ||
| interface FileEventConsumer { | ||
| fun onFileOpened(path: Path, content: String) | ||
| fun onFileClosed(path: Path) | ||
| fun onFileContentChanged(path: Path, content: String) | ||
| fun onFileSaved(path: Path) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -22,13 +22,14 @@ import com.itsaky.androidide.app.configuration.IJdkDistributionProvider | ||
| import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent | ||
| import com.itsaky.androidide.eventbus.events.editor.DocumentCloseEvent | ||
| import com.itsaky.androidide.eventbus.events.editor.DocumentOpenEvent | ||
| import com.itsaky.androidide.eventbus.events.editor.DocumentSaveEvent | ||
| import com.itsaky.androidide.eventbus.events.editor.DocumentSelectedEvent | ||
| import com.itsaky.androidide.lsp.api.ILanguageClient | ||
| import com.itsaky.androidide.lsp.api.ILanguageServer | ||
| import com.itsaky.androidide.lsp.api.IServerSettings | ||
| import com.itsaky.androidide.lsp.kotlin.compiler.Compiler | ||
| import com.itsaky.androidide.lsp.kotlin.compiler.KotlinProjectModel | ||
| import com.itsaky.androidide.lsp.kotlin.diagnostic.KotlinDiagnosticProvider | ||
| import com.itsaky.androidide.lsp.kotlin.diagnostic.collectDiagnosticsFor | ||
| import com.itsaky.androidide.lsp.models.CompletionParams | ||
| import com.itsaky.androidide.lsp.models.CompletionResult | ||
| import com.itsaky.androidide.lsp.models.DefinitionParams | ||
| @@ -76,7 +77,6 @@ class KotlinLanguageServer : ILanguageServer { | ||
| CoroutineScope(SupervisorJob() + CoroutineName(KotlinLanguageServer::class.simpleName!!)) | ||
| private var projectModel: KotlinProjectModel? = null | ||
| private var compiler: Compiler? = null | ||
| private var diagnosticProvider: KotlinDiagnosticProvider? = null | ||
| private var analyzeJob: Job? = null | ||
| override val serverId: String = SERVER_ID | ||
| @@ -106,7 +106,6 @@ class KotlinLanguageServer : ILanguageServer { | ||
| override fun shutdown() { | ||
| EventBus.getDefault().unregister(this) | ||
| scope.cancel("LSP is being shut down") | ||
| diagnosticProvider?.close() | ||
| compiler?.close() | ||
| initialized = false | ||
| } | ||
| @@ -150,7 +149,6 @@ class KotlinLanguageServer : ILanguageServer { | ||
| ) | ||
| this.compiler = compiler | ||
| this.diagnosticProvider = KotlinDiagnosticProvider(compiler, scope) | ||
| } else { | ||
| logger.info("Updating project model") | ||
| @@ -221,7 +219,7 @@ class KotlinLanguageServer : ILanguageServer { | ||
| return DiagnosticResult.NO_UPDATE | ||
| } | ||
| return diagnosticProvider?.analyze(file) | ||
| return compiler?.compilationEnvironmentFor(file)?.collectDiagnosticsFor(file) | ||
| ?: DiagnosticResult.NO_UPDATE | ||
| } | ||
| @@ -232,6 +230,11 @@ class KotlinLanguageServer : ILanguageServer { | ||
| return | ||
| } | ||
| compiler?.compilationEnvironmentFor(event.openedFile)?.apply { | ||
| val content = FileManager.getDocumentContents(event.openedFile) | ||
| fileManager.onFileOpened(event.openedFile, content) | ||
| } | ||
| selectedFile = event.openedFile | ||
| debouncingAnalyze() | ||
| } | ||
| @@ -262,6 +265,13 @@ class KotlinLanguageServer : ILanguageServer { | ||
| if (!DocumentUtils.isKotlinFile(event.changedFile)) { | ||
| return | ||
| } | ||
| compiler?.compilationEnvironmentFor(event.changedFile)?.apply { | ||
| val content = FileManager.getDocumentContents(event.changedFile) | ||
| logger.info("Notifying KtFileManager for file {} with contents {}", event.changedFile, content) | ||
| fileManager.onFileContentChanged(event.changedFile, content) | ||
| } | ||
| debouncingAnalyze() | ||
| } | ||
| @@ -272,13 +282,29 @@ class KotlinLanguageServer : ILanguageServer { | ||
| return | ||
| } | ||
| diagnosticProvider?.clearTimestamp(event.closedFile) | ||
| compiler?.compilationEnvironmentFor(event.closedFile)?.apply { | ||
| fileManager.onFileClosed(event.closedFile) | ||
| fileManager.clearAnalyzeTimestampOf(event.closedFile) | ||
| } | ||
| if (FileManager.getActiveDocumentCount() == 0) { | ||
| selectedFile = null | ||
| analyzeJob?.cancel("No active files") | ||
| } | ||
| } | ||
| @Subscribe(threadMode = ThreadMode.ASYNC) | ||
itsaky-adfa marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| @Suppress("unused") | ||
| fun onDocumentSaved(event: DocumentSaveEvent) { | ||
| if (!DocumentUtils.isKotlinFile(event.savedFile)) { | ||
| return | ||
| } | ||
| compiler?.compilationEnvironmentFor(event.savedFile)?.apply { | ||
| fileManager.onFileSaved(event.savedFile) | ||
| } | ||
| } | ||
| @Subscribe(threadMode = ThreadMode.ASYNC) | ||
| @Suppress("unused") | ||
| fun onDocumentSelected(event: DocumentSelectedEvent) { | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| package com.itsaky.androidide.lsp.kotlin | ||
| import org.jetbrains.kotlin.analysis.api.KaSession | ||
| import org.jetbrains.kotlin.analysis.api.analyze | ||
| import org.jetbrains.kotlin.analysis.api.analyzeCopy | ||
| import org.jetbrains.kotlin.analysis.api.projectStructure.KaDanglingFileResolutionMode | ||
| import org.jetbrains.kotlin.com.intellij.openapi.editor.Document | ||
| import org.jetbrains.kotlin.com.intellij.openapi.vfs.StandardFileSystems | ||
| import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFileManager | ||
| import org.jetbrains.kotlin.com.intellij.psi.PsiDocumentManager | ||
| import org.jetbrains.kotlin.com.intellij.psi.PsiManager | ||
| import org.jetbrains.kotlin.psi.KtFile | ||
| import org.jetbrains.kotlin.psi.KtPsiFactory | ||
| import org.slf4j.LoggerFactory | ||
| import java.nio.file.Path | ||
| import java.util.concurrent.ConcurrentHashMap | ||
| import kotlin.io.path.name | ||
| import kotlin.io.path.pathString | ||
| import kotlin.time.Clock | ||
| import kotlin.time.Instant | ||
| /** | ||
| * Manages [KtFile] instances for all open files. | ||
| */ | ||
| class KtFileManager( | ||
| private val psiFactory: KtPsiFactory, | ||
| private val psiManager: PsiManager, | ||
| private val psiDocumentManager: PsiDocumentManager, | ||
| ) : FileEventConsumer, AutoCloseable { | ||
| companion object { | ||
| private val logger = LoggerFactory.getLogger(KtFileManager::class.java) | ||
| } | ||
| private val entries = ConcurrentHashMap<Path, ManagedFile>() | ||
| @ConsistentCopyVisibility | ||
| data class ManagedFile @Deprecated("Use ManagedFile.create instead") internal constructor( | ||
| val file: Path, | ||
| val diskKtFile: KtFile, | ||
| @Volatile var inMemoryKtFile: KtFile, | ||
| val document: Document, | ||
| @Volatile var lastModified: Instant, | ||
| @Volatile var isDirty: Boolean, | ||
| @Volatile var analyzeTimestamp: Instant, | ||
| ) { | ||
| /** | ||
| * Analyze this [ManagedFile] contents. | ||
| * | ||
| * @param action The analysis action. | ||
| */ | ||
| fun <R> analyze(action: KaSession.(file: KtFile) -> R): R { | ||
| if (diskKtFile === inMemoryKtFile) { | ||
| return analyze(useSiteElement = inMemoryKtFile) { action(inMemoryKtFile) } | ||
| } | ||
| return analyzeCopy( | ||
| useSiteElement = inMemoryKtFile, | ||
| resolutionMode = KaDanglingFileResolutionMode.PREFER_SELF | ||
| ) { | ||
| action(inMemoryKtFile) | ||
| } | ||
| } | ||
| fun createInMemoryFileWithContent(psiFactory: KtPsiFactory, content: String): KtFile { | ||
| val inMemoryFile = psiFactory.createFile(file.name, content) | ||
| inMemoryFile.originalFile = diskKtFile | ||
| return inMemoryFile | ||
| } | ||
| companion object { | ||
| @Suppress("DEPRECATION") | ||
| fun create( | ||
| file: Path, | ||
| ktFile: KtFile, | ||
| document: Document, | ||
| inMemoryKtFile: KtFile = ktFile, | ||
| lastModified: Instant = Clock.System.now(), | ||
| isDirty: Boolean = false, | ||
| analyzeTimestamp: Instant = Instant.DISTANT_PAST, | ||
| ) = | ||
| ManagedFile( | ||
| file = file, | ||
| diskKtFile = ktFile, | ||
| inMemoryKtFile = inMemoryKtFile, | ||
| document = document, | ||
| lastModified = lastModified, | ||
| isDirty = isDirty, | ||
| analyzeTimestamp = analyzeTimestamp, | ||
| ) | ||
| } | ||
| } | ||
| override fun onFileOpened(path: Path, content: String) { | ||
| logger.debug("onFileOpened: {}", path) | ||
| entries[path]?.let { existing -> | ||
| logger.info("File is already opened, updating content") | ||
| updateDocumentContent(existing, content) | ||
| return | ||
| } | ||
| val ktFile = resolveKtFile(path) | ||
| if (ktFile == null) { | ||
| logger.warn("Cannot resolve KtFile for: {}", path) | ||
| return | ||
| } | ||
| val document = getOrCreateDocument(ktFile) | ||
| if (document == null) { | ||
| logger.warn("Cannot obtain Document for: {}", path) | ||
| return | ||
| } | ||
| logger.info("Creating managed file entry") | ||
| val entry = ManagedFile.create( | ||
| file = path, | ||
| ktFile = ktFile, | ||
| document = document, | ||
| ) | ||
| entries[path] = entry | ||
| updateDocumentContent(entry, content) | ||
| logger.debug("File opened and managed: {}", path) | ||
| return | ||
| } | ||
| override fun onFileContentChanged(path: Path, content: String) { | ||
| logger.debug("onFileContentChanged: {}", path) | ||
| val entry = entries[path] ?: run { | ||
| logger.debug("Content changed for unmanaged file: {}. Ignoring.", path) | ||
| return | ||
| } | ||
| updateDocumentContent(entry, content) | ||
| } | ||
| override fun onFileSaved(path: Path) { | ||
| val entry = entries[path] ?: return | ||
| entry.isDirty = false | ||
| logger.debug("File saved: {}", path) | ||
| } | ||
| override fun onFileClosed(path: Path) { | ||
| entries.remove(path) ?: return | ||
| logger.debug("File closed: {}", path) | ||
| } | ||
| fun getOpenFile(path: Path): ManagedFile? = entries[path] | ||
| fun allOpenFiles(): Collection<ManagedFile> = | ||
| entries.values.toList() | ||
| fun clearAnalyzeTimestampOf(file: Path) { | ||
| val managed = getOpenFile(file) ?: return | ||
| managed.analyzeTimestamp = Instant.DISTANT_PAST | ||
| } | ||
| private fun resolveKtFile(path: Path): KtFile? { | ||
| val vfs = VirtualFileManager.getInstance() | ||
| .getFileSystem(StandardFileSystems.FILE_PROTOCOL) | ||
| val virtualFile = vfs.refreshAndFindFileByPath(path.pathString) | ||
itsaky-adfa marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| ?: return null | ||
| val psiFile = psiManager.findFile(virtualFile) | ||
| return psiFile as? KtFile | ||
| } | ||
| private fun getOrCreateDocument(ktFile: KtFile): Document? { | ||
| return psiDocumentManager.getDocument(ktFile) | ||
| } | ||
| private fun updateDocumentContent(entry: ManagedFile, content: String) { | ||
| logger.info("Updating doc content for {}", entry.file) | ||
| val normalized = content.replace("\r", "") | ||
| if (entry.inMemoryKtFile.text == normalized) return | ||
| entry.inMemoryKtFile = entry.createInMemoryFileWithContent(psiFactory, content) | ||
| entry.lastModified = Clock.System.now() | ||
| entry.isDirty = true | ||
| } | ||
| override fun close() { | ||
| entries.clear() | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.