Clean up of plugin-common.xml and convert components to services
This commit is contained in:
+4
-6
@@ -16,6 +16,7 @@ import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.rootManager
|
||||
import com.intellij.openapi.roots.ModuleRootEvent
|
||||
import com.intellij.openapi.roots.ModuleRootListener
|
||||
import com.intellij.openapi.startup.StartupActivity
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.openapi.vfs.VfsUtilCore
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
@@ -47,14 +48,14 @@ import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentMap
|
||||
|
||||
class KotlinPackageContentModificationListener(private val project: Project) : Disposable {
|
||||
val connection = project.messageBus.connect()
|
||||
class KotlinPackageContentModificationListener : StartupActivity {
|
||||
|
||||
companion object {
|
||||
val LOG = Logger.getInstance(this::class.java)
|
||||
}
|
||||
|
||||
init {
|
||||
override fun runActivity(project: Project) {
|
||||
val connection = project.messageBus.connect(project)
|
||||
connection.subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener {
|
||||
override fun before(events: MutableList<out VFileEvent>) = onEvents(events, false)
|
||||
override fun after(events: List<VFileEvent>) = onEvents(events, true)
|
||||
@@ -116,9 +117,6 @@ class KotlinPackageContentModificationListener(private val project: Project) : D
|
||||
})
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
connection.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
class KotlinPackageStatementPsiTreeChangePreprocessor(private val project: Project) : PsiTreeChangePreprocessor {
|
||||
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
/*
|
||||
* Copyright 2000-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.caches
|
||||
|
||||
import com.intellij.ProjectTopics
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.components.ServiceManager
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.progress.ProcessCanceledException
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.rootManager
|
||||
import com.intellij.openapi.roots.ModuleRootEvent
|
||||
import com.intellij.openapi.roots.ModuleRootListener
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.openapi.vfs.VfsUtilCore
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.openapi.vfs.VirtualFileManager
|
||||
import com.intellij.openapi.vfs.newvfs.BulkFileListener
|
||||
import com.intellij.openapi.vfs.newvfs.events.*
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.PsiManager
|
||||
import com.intellij.psi.impl.PsiManagerEx
|
||||
import com.intellij.psi.impl.PsiTreeChangeEventImpl
|
||||
import com.intellij.psi.impl.PsiTreeChangePreprocessor
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
import com.intellij.util.containers.ContainerUtil
|
||||
import org.jetbrains.kotlin.idea.KotlinFileType
|
||||
import org.jetbrains.kotlin.idea.caches.PerModulePackageCacheService.Companion.DEBUG_LOG_ENABLE_PerModulePackageCache
|
||||
import org.jetbrains.kotlin.idea.caches.PerModulePackageCacheService.Companion.FULL_DROP_THRESHOLD
|
||||
import org.jetbrains.kotlin.idea.caches.project.ModuleSourceInfo
|
||||
import org.jetbrains.kotlin.idea.caches.project.getModuleInfoByVirtualFile
|
||||
import org.jetbrains.kotlin.idea.caches.project.getNullableModuleInfo
|
||||
import org.jetbrains.kotlin.idea.stubindex.PackageIndexUtil
|
||||
import org.jetbrains.kotlin.idea.util.getSourceRoot
|
||||
import org.jetbrains.kotlin.idea.util.sourceRoot
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import org.jetbrains.kotlin.psi.KtFile
|
||||
import org.jetbrains.kotlin.psi.KtPackageDirective
|
||||
import org.jetbrains.kotlin.psi.NotNullableUserDataProperty
|
||||
import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType
|
||||
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentMap
|
||||
|
||||
class KotlinPackageContentModificationListener(private val project: Project) : Disposable {
|
||||
val connection = project.messageBus.connect()
|
||||
|
||||
companion object {
|
||||
val LOG = Logger.getInstance(this::class.java)
|
||||
}
|
||||
|
||||
init {
|
||||
connection.subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener {
|
||||
override fun before(events: MutableList<out VFileEvent>) = onEvents(events, false)
|
||||
override fun after(events: List<VFileEvent>) = onEvents(events, true)
|
||||
|
||||
private fun isRelevant(event: VFileEvent): Boolean = when (event) {
|
||||
is VFilePropertyChangeEvent -> false
|
||||
is VFileCreateEvent -> true
|
||||
is VFileMoveEvent -> true
|
||||
is VFileDeleteEvent -> true
|
||||
is VFileContentChangeEvent -> true
|
||||
is VFileCopyEvent -> true
|
||||
else -> {
|
||||
LOG.warn("Unknown vfs event: ${event.javaClass}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun onEvents(events: List<VFileEvent>, isAfter: Boolean) {
|
||||
val service = PerModulePackageCacheService.getInstance(project)
|
||||
val fileManager = PsiManagerEx.getInstanceEx(project).fileManager
|
||||
if (events.size >= FULL_DROP_THRESHOLD) {
|
||||
service.onTooComplexChange()
|
||||
} else {
|
||||
events.asSequence()
|
||||
.filter(::isRelevant)
|
||||
.filter {
|
||||
(it.isValid || it !is VFileCreateEvent) && it.file != null
|
||||
}
|
||||
.filter {
|
||||
val vFile = it.file!!
|
||||
vFile.isDirectory || vFile.fileType == KotlinFileType.INSTANCE
|
||||
}
|
||||
.filter {
|
||||
// It expected that content change events will be duplicated with more precise PSI events and processed
|
||||
// in KotlinPackageStatementPsiTreeChangePreprocessor, but events might have been missing if PSI view provider
|
||||
// is absent.
|
||||
if (it is VFileContentChangeEvent) {
|
||||
isAfter && fileManager.findCachedViewProvider(it.file) == null
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
.filter {
|
||||
when (val origin = it.requestor) {
|
||||
is Project -> origin == project
|
||||
is PsiManager -> origin.project == project
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
.forEach { event -> service.notifyPackageChange(event) }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
connection.subscribe(ProjectTopics.PROJECT_ROOTS, object : ModuleRootListener {
|
||||
override fun rootsChanged(event: ModuleRootEvent) {
|
||||
PerModulePackageCacheService.getInstance(project).onTooComplexChange()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
connection.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
class KotlinPackageStatementPsiTreeChangePreprocessor(private val project: Project) : PsiTreeChangePreprocessor {
|
||||
override fun treeChanged(event: PsiTreeChangeEventImpl) {
|
||||
val eFile = event.file ?: event.child as? PsiFile
|
||||
if (eFile == null) {
|
||||
LOG.debugIfEnabled(project, true) { "Got PsiEvent: $event without file" }
|
||||
}
|
||||
val file = eFile as? KtFile ?: return
|
||||
|
||||
when (event.code) {
|
||||
PsiTreeChangeEventImpl.PsiEventType.CHILD_ADDED,
|
||||
PsiTreeChangeEventImpl.PsiEventType.CHILD_MOVED,
|
||||
PsiTreeChangeEventImpl.PsiEventType.CHILD_REPLACED,
|
||||
PsiTreeChangeEventImpl.PsiEventType.CHILD_REMOVED -> {
|
||||
val child = event.child ?: run {
|
||||
LOG.debugIfEnabled(project, true) { "Got PsiEvent: $event without child" }
|
||||
return
|
||||
}
|
||||
if (child.getParentOfType<KtPackageDirective>(false) != null)
|
||||
ServiceManager.getService(project, PerModulePackageCacheService::class.java).notifyPackageChange(file)
|
||||
}
|
||||
PsiTreeChangeEventImpl.PsiEventType.CHILDREN_CHANGED -> {
|
||||
val parent = event.parent ?: run {
|
||||
LOG.debugIfEnabled(project, true) { "Got PsiEvent: $event without parent" }
|
||||
return
|
||||
}
|
||||
val childrenOfType = parent.getChildrenOfType<KtPackageDirective>()
|
||||
if (
|
||||
(!event.isGenericChange && (childrenOfType.any() || parent is KtPackageDirective)) ||
|
||||
(childrenOfType.any { it.name.isEmpty() } && parent is KtFile)
|
||||
) {
|
||||
ServiceManager.getService(project, PerModulePackageCacheService::class.java).notifyPackageChange(file)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val LOG = Logger.getInstance(this::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private typealias ImplicitPackageData = MutableMap<FqName, MutableList<VirtualFile>>
|
||||
|
||||
class ImplicitPackagePrefixCache(private val project: Project) {
|
||||
private val implicitPackageCache = ConcurrentHashMap<VirtualFile, ImplicitPackageData>()
|
||||
|
||||
fun getPrefix(sourceRoot: VirtualFile): FqName {
|
||||
val implicitPackageMap = implicitPackageCache.getOrPut(sourceRoot) { analyzeImplicitPackagePrefixes(sourceRoot) }
|
||||
return implicitPackageMap.keys.singleOrNull() ?: FqName.ROOT
|
||||
}
|
||||
|
||||
internal fun clear() {
|
||||
implicitPackageCache.clear()
|
||||
}
|
||||
|
||||
private fun analyzeImplicitPackagePrefixes(sourceRoot: VirtualFile): MutableMap<FqName, MutableList<VirtualFile>> {
|
||||
val result = mutableMapOf<FqName, MutableList<VirtualFile>>()
|
||||
val ktFiles = sourceRoot.children.filter { it.fileType == KotlinFileType.INSTANCE }
|
||||
for (ktFile in ktFiles) {
|
||||
result.addFile(ktFile)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun ImplicitPackageData.addFile(ktFile: VirtualFile) {
|
||||
synchronized(this) {
|
||||
val psiFile = PsiManager.getInstance(project).findFile(ktFile) as? KtFile ?: return
|
||||
addPsiFile(psiFile, ktFile)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ImplicitPackageData.addPsiFile(
|
||||
psiFile: KtFile,
|
||||
ktFile: VirtualFile
|
||||
) = getOrPut(psiFile.packageFqName) { mutableListOf() }.add(ktFile)
|
||||
|
||||
private fun ImplicitPackageData.removeFile(file: VirtualFile) {
|
||||
synchronized(this) {
|
||||
for ((key, value) in this) {
|
||||
if (value.remove(file)) {
|
||||
if (value.isEmpty()) remove(key)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ImplicitPackageData.updateFile(file: KtFile) {
|
||||
synchronized(this) {
|
||||
removeFile(file.virtualFile)
|
||||
addPsiFile(file, file.virtualFile)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun update(event: VFileEvent) {
|
||||
when (event) {
|
||||
is VFileCreateEvent -> checkNewFileInSourceRoot(event.file)
|
||||
is VFileDeleteEvent -> checkDeletedFileInSourceRoot(event.file)
|
||||
is VFileCopyEvent -> {
|
||||
val newParent = event.newParent
|
||||
if (newParent.isValid) {
|
||||
checkNewFileInSourceRoot(newParent.findChild(event.newChildName))
|
||||
}
|
||||
}
|
||||
is VFileMoveEvent -> {
|
||||
checkNewFileInSourceRoot(event.file)
|
||||
if (event.oldParent.getSourceRoot(project) == event.oldParent) {
|
||||
implicitPackageCache[event.oldParent]?.removeFile(event.file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkNewFileInSourceRoot(file: VirtualFile?) {
|
||||
if (file == null) return
|
||||
if (file.getSourceRoot(project) == file.parent) {
|
||||
implicitPackageCache[file.parent]?.addFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkDeletedFileInSourceRoot(file: VirtualFile?) {
|
||||
val directory = file?.parent
|
||||
if (directory == null || !directory.isValid) return
|
||||
if (directory.getSourceRoot(project) == directory) {
|
||||
implicitPackageCache[directory]?.removeFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun update(ktFile: KtFile) {
|
||||
val parent = ktFile.virtualFile?.parent ?: return
|
||||
if (ktFile.sourceRoot == parent) {
|
||||
implicitPackageCache[parent]?.updateFile(ktFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PerModulePackageCacheService(private val project: Project) : Disposable {
|
||||
|
||||
/*
|
||||
* Actually an WeakMap<Module, SoftMap<ModuleSourceInfo, SoftMap<FqName, Boolean>>>
|
||||
*/
|
||||
private val cache = ContainerUtil.createConcurrentWeakMap<Module, ConcurrentMap<ModuleSourceInfo, ConcurrentMap<FqName, Boolean>>>()
|
||||
private val implicitPackagePrefixCache = ImplicitPackagePrefixCache(project)
|
||||
|
||||
private val pendingVFileChanges: MutableSet<VFileEvent> = mutableSetOf()
|
||||
private val pendingKtFileChanges: MutableSet<KtFile> = mutableSetOf()
|
||||
|
||||
private val projectScope = GlobalSearchScope.projectScope(project)
|
||||
|
||||
internal fun onTooComplexChange() {
|
||||
clear()
|
||||
}
|
||||
|
||||
private fun clear() {
|
||||
synchronized(this) {
|
||||
pendingVFileChanges.clear()
|
||||
pendingKtFileChanges.clear()
|
||||
cache.clear()
|
||||
implicitPackagePrefixCache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun notifyPackageChange(file: VFileEvent): Unit = synchronized(this) {
|
||||
pendingVFileChanges += file
|
||||
}
|
||||
|
||||
internal fun notifyPackageChange(file: KtFile): Unit = synchronized(this) {
|
||||
pendingKtFileChanges += file
|
||||
}
|
||||
|
||||
private fun invalidateCacheForModuleSourceInfo(moduleSourceInfo: ModuleSourceInfo) {
|
||||
LOG.debugIfEnabled(project) { "Invalidated cache for $moduleSourceInfo" }
|
||||
val perSourceInfoData = cache[moduleSourceInfo.module] ?: return
|
||||
val dataForSourceInfo = perSourceInfoData[moduleSourceInfo] ?: return
|
||||
dataForSourceInfo.clear()
|
||||
}
|
||||
|
||||
private fun checkPendingChanges() = synchronized(this) {
|
||||
if (pendingVFileChanges.size + pendingKtFileChanges.size >= FULL_DROP_THRESHOLD) {
|
||||
onTooComplexChange()
|
||||
} else {
|
||||
pendingVFileChanges.processPending { event ->
|
||||
val vfile = event.file ?: return@processPending
|
||||
// When VirtualFile !isValid (deleted for example), it impossible to use getModuleInfoByVirtualFile
|
||||
// For directory we must check both is it in some sourceRoot, and is it contains some sourceRoot
|
||||
if (vfile.isDirectory || !vfile.isValid) {
|
||||
for ((module, data) in cache) {
|
||||
val sourceRootUrls = module.rootManager.sourceRootUrls
|
||||
if (sourceRootUrls.any { url ->
|
||||
vfile.containedInOrContains(url)
|
||||
}) {
|
||||
LOG.debugIfEnabled(project) { "Invalidated cache for $module" }
|
||||
data.clear()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val infoByVirtualFile = getModuleInfoByVirtualFile(project, vfile)
|
||||
if (infoByVirtualFile == null || infoByVirtualFile !is ModuleSourceInfo) {
|
||||
LOG.debugIfEnabled(project) { "Skip $vfile as it has mismatched ModuleInfo=$infoByVirtualFile" }
|
||||
}
|
||||
(infoByVirtualFile as? ModuleSourceInfo)?.let {
|
||||
invalidateCacheForModuleSourceInfo(it)
|
||||
}
|
||||
}
|
||||
|
||||
implicitPackagePrefixCache.update(event)
|
||||
}
|
||||
|
||||
pendingKtFileChanges.processPending { file ->
|
||||
if (file.virtualFile != null && file.virtualFile !in projectScope) {
|
||||
LOG.debugIfEnabled(project) {
|
||||
"Skip $file without vFile, or not in scope: ${file.virtualFile?.let { it !in projectScope }}"
|
||||
}
|
||||
return@processPending
|
||||
}
|
||||
val nullableModuleInfo = file.getNullableModuleInfo()
|
||||
(nullableModuleInfo as? ModuleSourceInfo)?.let { invalidateCacheForModuleSourceInfo(it) }
|
||||
if (nullableModuleInfo == null || nullableModuleInfo !is ModuleSourceInfo) {
|
||||
LOG.debugIfEnabled(project) { "Skip $file as it has mismatched ModuleInfo=$nullableModuleInfo" }
|
||||
}
|
||||
implicitPackagePrefixCache.update(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <T> MutableCollection<T>.processPending(crossinline body: (T) -> Unit) {
|
||||
this.removeIf { value ->
|
||||
try {
|
||||
body(value)
|
||||
} catch (pce: ProcessCanceledException) {
|
||||
throw pce
|
||||
} catch (exc: Exception) {
|
||||
// Log and proceed. Otherwise pending object processing won't be cleared and exception will be thrown forever.
|
||||
LOG.error(exc)
|
||||
}
|
||||
|
||||
return@removeIf true
|
||||
}
|
||||
}
|
||||
|
||||
private fun VirtualFile.containedInOrContains(root: String) =
|
||||
(VfsUtilCore.isEqualOrAncestor(url, root)
|
||||
|| isDirectory && VfsUtilCore.isEqualOrAncestor(root, url))
|
||||
|
||||
|
||||
fun packageExists(packageFqName: FqName, moduleInfo: ModuleSourceInfo): Boolean {
|
||||
val module = moduleInfo.module
|
||||
checkPendingChanges()
|
||||
|
||||
val perSourceInfoCache = cache.getOrPut(module) {
|
||||
ContainerUtil.createConcurrentSoftMap()
|
||||
}
|
||||
val cacheForCurrentModuleInfo = perSourceInfoCache.getOrPut(moduleInfo) {
|
||||
ContainerUtil.createConcurrentSoftMap()
|
||||
}
|
||||
|
||||
return cacheForCurrentModuleInfo.getOrPut(packageFqName) {
|
||||
val packageExists = PackageIndexUtil.packageExists(packageFqName, moduleInfo.contentScope(), project)
|
||||
LOG.debugIfEnabled(project) { "Computed cache value for $packageFqName in $moduleInfo is $packageExists" }
|
||||
packageExists
|
||||
}
|
||||
}
|
||||
|
||||
fun getImplicitPackagePrefix(sourceRoot: VirtualFile): FqName {
|
||||
checkPendingChanges()
|
||||
return implicitPackagePrefixCache.getPrefix(sourceRoot)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
clear()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val FULL_DROP_THRESHOLD = 1000
|
||||
private val LOG = Logger.getInstance(this::class.java)
|
||||
|
||||
fun getInstance(project: Project): PerModulePackageCacheService =
|
||||
ServiceManager.getService(project, PerModulePackageCacheService::class.java)
|
||||
|
||||
var Project.DEBUG_LOG_ENABLE_PerModulePackageCache: Boolean
|
||||
by NotNullableUserDataProperty<Project, Boolean>(Key.create("debug.PerModulePackageCache"), false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Logger.debugIfEnabled(project: Project, withCurrentTrace: Boolean = false, message: () -> String) {
|
||||
if (ApplicationManager.getApplication().isUnitTestMode && project.DEBUG_LOG_ENABLE_PerModulePackageCache) {
|
||||
val msg = message()
|
||||
if (withCurrentTrace) {
|
||||
val e = Exception().apply { fillInStackTrace() }
|
||||
this.debug(msg, e)
|
||||
} else {
|
||||
this.debug(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -7,7 +7,6 @@ package org.jetbrains.kotlin.idea.caches.project
|
||||
|
||||
import com.intellij.ide.highlighter.ArchiveFileType
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.components.ServiceManager
|
||||
import com.intellij.openapi.fileTypes.FileTypeEvent
|
||||
import com.intellij.openapi.fileTypes.FileTypeListener
|
||||
import com.intellij.openapi.fileTypes.FileTypeManager
|
||||
@@ -23,15 +22,16 @@ import com.intellij.openapi.vfs.newvfs.events.VFileCopyEvent
|
||||
import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent
|
||||
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
|
||||
import com.intellij.openapi.vfs.newvfs.events.VFileMoveEvent
|
||||
import org.jetbrains.kotlin.idea.util.application.getServiceSafe
|
||||
|
||||
class LibraryModificationTracker(project: Project) : SimpleModificationTracker() {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun getInstance(project: Project) = ServiceManager.getService(project, LibraryModificationTracker::class.java)!!
|
||||
fun getInstance(project: Project) = project.getServiceSafe(LibraryModificationTracker::class.java)
|
||||
}
|
||||
|
||||
init {
|
||||
val connection = project.messageBus.connect()
|
||||
val connection = project.messageBus.connect(project)
|
||||
connection.subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener {
|
||||
override fun after(events: List<VFileEvent>) {
|
||||
events.filter(::isRelevantEvent).let { createEvents ->
|
||||
|
||||
+2
-2
@@ -72,7 +72,7 @@ class KotlinCodeBlockModificationListener(
|
||||
|
||||
init {
|
||||
val model = PomManager.getModel(project)
|
||||
val messageBusConnection = project.messageBus.connect()
|
||||
val messageBusConnection = project.messageBus.connect(project)
|
||||
|
||||
if (isLanguageTrackerEnabled) {
|
||||
(PsiManager.getInstance(project) as PsiManagerImpl).addTreeChangePreprocessor(this)
|
||||
@@ -175,7 +175,7 @@ class KotlinCodeBlockModificationListener(
|
||||
// When a code fragment is reparsed, Intellij doesn't do an AST diff and considers the entire
|
||||
// contents to be replaced, which is represented in a POM event as an empty list of changed elements
|
||||
|
||||
return elements.mapNotNull { element ->
|
||||
return elements.map { element ->
|
||||
val modificationScope = getInsideCodeBlockModificationScope(element.psi) ?: return emptyList()
|
||||
modificationScope.blockDeclaration
|
||||
}
|
||||
|
||||
+362
@@ -0,0 +1,362 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.caches.trackers
|
||||
|
||||
import com.intellij.lang.ASTNode
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.openapi.util.ModificationTracker
|
||||
import com.intellij.openapi.util.SimpleModificationTracker
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.pom.PomManager
|
||||
import com.intellij.pom.PomModelAspect
|
||||
import com.intellij.pom.event.PomModelEvent
|
||||
import com.intellij.pom.event.PomModelListener
|
||||
import com.intellij.pom.tree.TreeAspect
|
||||
import com.intellij.pom.tree.events.TreeChangeEvent
|
||||
import com.intellij.pom.tree.events.impl.ChangeInfoImpl
|
||||
import com.intellij.psi.*
|
||||
import com.intellij.psi.impl.PsiManagerImpl
|
||||
import com.intellij.psi.impl.PsiModificationTrackerImpl
|
||||
import com.intellij.psi.impl.PsiTreeChangeEventImpl
|
||||
import com.intellij.psi.impl.PsiTreeChangeEventImpl.PsiEventType.CHILD_MOVED
|
||||
import com.intellij.psi.impl.PsiTreeChangeEventImpl.PsiEventType.PROPERTY_CHANGED
|
||||
import com.intellij.psi.impl.PsiTreeChangePreprocessor
|
||||
import com.intellij.psi.util.PsiModificationTracker
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import org.jetbrains.kotlin.idea.KotlinLanguage
|
||||
import org.jetbrains.kotlin.idea.util.application.getServiceSafe
|
||||
import org.jetbrains.kotlin.kdoc.psi.api.KDoc
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.getTopmostParentOfType
|
||||
import org.jetbrains.kotlin.psi.psiUtil.isAncestor
|
||||
|
||||
val KOTLIN_CONSOLE_KEY = Key.create<Boolean>("kotlin.console")
|
||||
|
||||
/**
|
||||
* Tested in OutOfBlockModificationTestGenerated
|
||||
*/
|
||||
class KotlinCodeBlockModificationListener(project: Project) : PsiTreeChangePreprocessor {
|
||||
private val treeAspect: TreeAspect = TreeAspect.getInstance(project)
|
||||
|
||||
private val modificationTrackerImpl: PsiModificationTrackerImpl =
|
||||
PsiModificationTracker.SERVICE.getInstance(project) as PsiModificationTrackerImpl
|
||||
|
||||
@Volatile
|
||||
private var kotlinModificationTracker: Long = 0
|
||||
|
||||
private val kotlinOutOfCodeBlockTrackerImpl: SimpleModificationTracker = SimpleModificationTracker()
|
||||
|
||||
val kotlinOutOfCodeBlockTracker: ModificationTracker = kotlinOutOfCodeBlockTrackerImpl
|
||||
|
||||
internal val perModuleOutOfCodeBlockTrackerUpdater = KotlinModuleOutOfCodeBlockModificationTracker.Updater(project)
|
||||
|
||||
init {
|
||||
val model = PomManager.getModel(project)
|
||||
val messageBusConnection = project.messageBus.connect(project)
|
||||
|
||||
val psiManager = PsiManager.getInstance(project) as PsiManagerImpl
|
||||
psiManager.addTreeChangePreprocessor(this)
|
||||
|
||||
model.addModelListener(object : PomModelListener {
|
||||
override fun isAspectChangeInteresting(aspect: PomModelAspect): Boolean {
|
||||
return aspect == treeAspect
|
||||
}
|
||||
|
||||
override fun modelChanged(event: PomModelEvent) {
|
||||
val changeSet = event.getChangeSet(treeAspect) as TreeChangeEvent? ?: return
|
||||
val ktFile = changeSet.rootElement.psi.containingFile as? KtFile ?: return
|
||||
|
||||
incFileModificationCount(ktFile)
|
||||
|
||||
val changedElements = changeSet.changedElements
|
||||
|
||||
// skip change if it contains only virtual/fake change
|
||||
if (changedElements.isNotEmpty()) {
|
||||
// ignore formatting (whitespaces etc)
|
||||
if (isFormattingChange(changeSet) || isCommentChange(changeSet)) return
|
||||
}
|
||||
|
||||
val inBlockElements = inBlockModifications(changedElements)
|
||||
|
||||
val physical = ktFile.isPhysical
|
||||
if (inBlockElements.isEmpty()) {
|
||||
messageBusConnection.deliverImmediately()
|
||||
|
||||
if (physical && !isReplLine(ktFile.virtualFile) && ktFile !is KtTypeCodeFragment) {
|
||||
kotlinOutOfCodeBlockTrackerImpl.incModificationCount()
|
||||
perModuleOutOfCodeBlockTrackerUpdater.onKotlinPhysicalFileOutOfBlockChange(ktFile, true)
|
||||
}
|
||||
|
||||
ktFile.incOutOfBlockModificationCount()
|
||||
} else if (physical) {
|
||||
inBlockElements.forEach { it.containingKtFile.addInBlockModifiedItem(it) }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
messageBusConnection.subscribe(PsiModificationTracker.TOPIC, PsiModificationTracker.Listener {
|
||||
val kotlinTrackerInternalIDECount =
|
||||
modificationTrackerImpl.forLanguage(KotlinLanguage.INSTANCE).modificationCount
|
||||
if (kotlinModificationTracker == kotlinTrackerInternalIDECount) {
|
||||
// Some update that we are not sure is from Kotlin language, as Kotlin language tracker wasn't changed
|
||||
kotlinOutOfCodeBlockTrackerImpl.incModificationCount()
|
||||
} else {
|
||||
kotlinModificationTracker = kotlinTrackerInternalIDECount
|
||||
}
|
||||
|
||||
perModuleOutOfCodeBlockTrackerUpdater.onPsiModificationTrackerUpdate()
|
||||
})
|
||||
}
|
||||
|
||||
override fun treeChanged(event: PsiTreeChangeEventImpl) {
|
||||
if (!PsiModificationTrackerImpl.canAffectPsi(event)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Copy logic from PsiModificationTrackerImpl.treeChanged(). Some out-of-code-block events are written to language modification
|
||||
// tracker in PsiModificationTrackerImpl but don't have correspondent PomModelEvent. Increase kotlinOutOfCodeBlockTracker
|
||||
// manually if needed.
|
||||
val outOfCodeBlock = when (event.code) {
|
||||
PROPERTY_CHANGED ->
|
||||
event.propertyName === PsiTreeChangeEvent.PROP_UNLOADED_PSI || event.propertyName === PsiTreeChangeEvent.PROP_ROOTS
|
||||
CHILD_MOVED -> event.oldParent is PsiDirectory || event.newParent is PsiDirectory
|
||||
else -> event.parent is PsiDirectory
|
||||
}
|
||||
|
||||
if (outOfCodeBlock) {
|
||||
kotlinOutOfCodeBlockTrackerImpl.incModificationCount()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun isReplLine(file: VirtualFile): Boolean {
|
||||
return file.getUserData(KOTLIN_CONSOLE_KEY) == true
|
||||
}
|
||||
|
||||
private fun incFileModificationCount(file: KtFile) {
|
||||
val tracker = file.getUserData(PER_FILE_MODIFICATION_TRACKER)
|
||||
?: file.putUserDataIfAbsent(PER_FILE_MODIFICATION_TRACKER, SimpleModificationTracker())
|
||||
tracker.incModificationCount()
|
||||
}
|
||||
|
||||
private fun inBlockModifications(elements: Array<ASTNode>): List<KtElement> {
|
||||
// When a code fragment is reparsed, Intellij doesn't do an AST diff and considers the entire
|
||||
// contents to be replaced, which is represented in a POM event as an empty list of changed elements
|
||||
|
||||
return elements.map { element ->
|
||||
val modificationScope = getInsideCodeBlockModificationScope(element.psi) ?: return emptyList()
|
||||
modificationScope.blockDeclaration
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSpecificChange(changeSet: TreeChangeEvent, precondition: (ASTNode?) -> Boolean): Boolean =
|
||||
changeSet.changedElements.all { changedElement ->
|
||||
val changesByElement = changeSet.getChangesByElement(changedElement)
|
||||
changesByElement.affectedChildren.all { affectedChild ->
|
||||
if (!precondition(affectedChild)) return@all false
|
||||
val changeByChild = changesByElement.getChangeByChild(affectedChild)
|
||||
return@all if (changeByChild is ChangeInfoImpl) {
|
||||
val oldChild = changeByChild.oldChild
|
||||
precondition(oldChild)
|
||||
} else false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCommentChange(changeSet: TreeChangeEvent): Boolean =
|
||||
isSpecificChange(changeSet) { it is PsiComment || it is KDoc }
|
||||
|
||||
private fun isFormattingChange(changeSet: TreeChangeEvent): Boolean =
|
||||
isSpecificChange(changeSet) { it is PsiWhiteSpace }
|
||||
|
||||
/**
|
||||
* Has to be aligned with [getInsideCodeBlockModificationScope] :
|
||||
*
|
||||
* result of analysis has to be reflected in dirty scope,
|
||||
* the only difference is whitespaces and comments
|
||||
*/
|
||||
fun getInsideCodeBlockModificationDirtyScope(element: PsiElement): PsiElement? {
|
||||
if (!element.isPhysical) return null
|
||||
// dirty scope for whitespaces and comments is the element itself
|
||||
if (element is PsiWhiteSpace || element is PsiComment || element is KDoc) return element
|
||||
|
||||
return getInsideCodeBlockModificationScope(element)?.blockDeclaration ?: null
|
||||
}
|
||||
|
||||
fun getInsideCodeBlockModificationScope(element: PsiElement): BlockModificationScopeElement? {
|
||||
val lambda = element.getTopmostParentOfType<KtLambdaExpression>()
|
||||
if (lambda is KtLambdaExpression) {
|
||||
lambda.getTopmostParentOfType<KtSuperTypeCallEntry>()?.getTopmostParentOfType<KtClassOrObject>()?.let {
|
||||
return BlockModificationScopeElement(it, it)
|
||||
}
|
||||
}
|
||||
|
||||
val blockDeclaration =
|
||||
KtPsiUtil.getTopmostParentOfTypes(element, *BLOCK_DECLARATION_TYPES) as? KtDeclaration ?: return null
|
||||
// KtPsiUtil.getTopmostParentOfType<KtClassOrObject>(element) as? KtDeclaration ?: return null
|
||||
|
||||
// should not be local declaration
|
||||
if (KtPsiUtil.isLocal(blockDeclaration))
|
||||
return null
|
||||
|
||||
when (blockDeclaration) {
|
||||
is KtNamedFunction -> {
|
||||
// if (blockDeclaration.visibilityModifierType()?.toVisibility() == Visibilities.PRIVATE) {
|
||||
// topClassLikeDeclaration(blockDeclaration)?.let {
|
||||
// return BlockModificationScopeElement(it, it)
|
||||
// }
|
||||
// }
|
||||
if (blockDeclaration.hasBlockBody()) {
|
||||
// case like `fun foo(): String {...<caret>...}`
|
||||
return blockDeclaration.bodyExpression
|
||||
?.takeIf { it.isAncestor(element) }
|
||||
?.let { BlockModificationScopeElement(blockDeclaration, it) }
|
||||
} else if (blockDeclaration.hasDeclaredReturnType()) {
|
||||
// case like `fun foo(): String = b<caret>labla`
|
||||
return blockDeclaration.initializer
|
||||
?.takeIf { it.isAncestor(element) }
|
||||
?.let { BlockModificationScopeElement(blockDeclaration, it) }
|
||||
}
|
||||
}
|
||||
|
||||
is KtProperty -> {
|
||||
// if (blockDeclaration.visibilityModifierType()?.toVisibility() == Visibilities.PRIVATE) {
|
||||
// topClassLikeDeclaration(blockDeclaration)?.let {
|
||||
// return BlockModificationScopeElement(it, it)
|
||||
// }
|
||||
// }
|
||||
if (blockDeclaration.typeReference != null) {
|
||||
val accessors =
|
||||
blockDeclaration.accessors.map { it.initializer ?: it.bodyExpression } + blockDeclaration.initializer
|
||||
for (accessor in accessors) {
|
||||
accessor?.takeIf {
|
||||
it.isAncestor(element) &&
|
||||
// adding annotations to accessor is the same as change contract of property
|
||||
(element !is KtAnnotated || element.annotationEntries.isEmpty())
|
||||
}
|
||||
?.let { expression ->
|
||||
val declaration =
|
||||
KtPsiUtil.getTopmostParentOfTypes(blockDeclaration, KtClassOrObject::class.java) as? KtElement ?:
|
||||
// ktFile to check top level property declarations
|
||||
return null
|
||||
return BlockModificationScopeElement(declaration, expression)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is KtScriptInitializer -> {
|
||||
return (blockDeclaration.body as? KtCallExpression)
|
||||
?.lambdaArguments
|
||||
?.lastOrNull()
|
||||
?.getLambdaExpression()
|
||||
?.takeIf { it.isAncestor(element) }
|
||||
?.let { BlockModificationScopeElement(blockDeclaration, it) }
|
||||
}
|
||||
|
||||
is KtClassInitializer -> {
|
||||
blockDeclaration
|
||||
.takeIf { it.isAncestor(element) }
|
||||
?.let { ktClassInitializer ->
|
||||
(PsiTreeUtil.getParentOfType(blockDeclaration, KtClassOrObject::class.java))?.let {
|
||||
return BlockModificationScopeElement(it, ktClassInitializer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is KtSecondaryConstructor -> {
|
||||
blockDeclaration
|
||||
?.takeIf {
|
||||
it.bodyExpression?.isAncestor(element) ?: false || it.getDelegationCallOrNull()?.isAncestor(element) ?: false
|
||||
}?.let { ktConstructor ->
|
||||
PsiTreeUtil.getParentOfType(blockDeclaration, KtClassOrObject::class.java)?.let {
|
||||
return BlockModificationScopeElement(it, ktConstructor)
|
||||
}
|
||||
}
|
||||
}
|
||||
// is KtClassOrObject -> {
|
||||
// return when (element) {
|
||||
// is KtProperty, is KtNamedFunction -> {
|
||||
// if ((element as? KtModifierListOwner)?.visibilityModifierType()?.toVisibility() == Visibilities.PRIVATE)
|
||||
// BlockModificationScopeElement(blockDeclaration, blockDeclaration) else null
|
||||
// }
|
||||
// else -> null
|
||||
// }
|
||||
// }
|
||||
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
data class BlockModificationScopeElement(val blockDeclaration: KtElement, val element: KtElement)
|
||||
|
||||
fun isBlockDeclaration(declaration: KtDeclaration): Boolean {
|
||||
return BLOCK_DECLARATION_TYPES.any { it.isInstance(declaration) }
|
||||
}
|
||||
|
||||
private val BLOCK_DECLARATION_TYPES = arrayOf<Class<out KtDeclaration>>(
|
||||
KtProperty::class.java,
|
||||
KtNamedFunction::class.java,
|
||||
KtClassInitializer::class.java,
|
||||
KtSecondaryConstructor::class.java,
|
||||
KtScriptInitializer::class.java
|
||||
)
|
||||
|
||||
fun getInstance(project: Project): KotlinCodeBlockModificationListener =
|
||||
project.getServiceSafe(KotlinCodeBlockModificationListener::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private val PER_FILE_MODIFICATION_TRACKER = Key<SimpleModificationTracker>("FILE_OUT_OF_BLOCK_MODIFICATION_COUNT")
|
||||
|
||||
val KtFile.perFileModificationTracker: ModificationTracker
|
||||
get() = putUserDataIfAbsent(PER_FILE_MODIFICATION_TRACKER, SimpleModificationTracker())
|
||||
|
||||
private val FILE_OUT_OF_BLOCK_MODIFICATION_COUNT = Key<Long>("FILE_OUT_OF_BLOCK_MODIFICATION_COUNT")
|
||||
|
||||
val KtFile.outOfBlockModificationCount: Long by NotNullableUserDataProperty(FILE_OUT_OF_BLOCK_MODIFICATION_COUNT, 0)
|
||||
|
||||
private fun KtFile.incOutOfBlockModificationCount() {
|
||||
clearInBlockModifications()
|
||||
|
||||
val count = getUserData(FILE_OUT_OF_BLOCK_MODIFICATION_COUNT) ?: 0
|
||||
putUserData(FILE_OUT_OF_BLOCK_MODIFICATION_COUNT, count + 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* inBlockModifications is a collection of block elements those have in-block modifications
|
||||
*/
|
||||
private val IN_BLOCK_MODIFICATIONS = Key<MutableCollection<KtElement>>("IN_BLOCK_MODIFICATIONS")
|
||||
private val FILE_IN_BLOCK_MODIFICATION_COUNT = Key<Long>("FILE_IN_BLOCK_MODIFICATION_COUNT")
|
||||
|
||||
val KtFile.inBlockModificationCount: Long by NotNullableUserDataProperty(FILE_IN_BLOCK_MODIFICATION_COUNT, 0)
|
||||
|
||||
val KtFile.inBlockModifications: Collection<KtElement>
|
||||
get() {
|
||||
val collection = getUserData(IN_BLOCK_MODIFICATIONS)
|
||||
return collection ?: emptySet()
|
||||
}
|
||||
|
||||
private fun KtFile.addInBlockModifiedItem(element: KtElement) {
|
||||
val collection = putUserDataIfAbsent(IN_BLOCK_MODIFICATIONS, mutableSetOf())
|
||||
synchronized(collection) {
|
||||
collection.add(element)
|
||||
}
|
||||
val count = getUserData(FILE_IN_BLOCK_MODIFICATION_COUNT) ?: 0
|
||||
putUserData(FILE_IN_BLOCK_MODIFICATION_COUNT, count + 1)
|
||||
}
|
||||
|
||||
fun KtFile.clearInBlockModifications() {
|
||||
val collection = getUserData(IN_BLOCK_MODIFICATIONS)
|
||||
collection?.let {
|
||||
synchronized(it) {
|
||||
it.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -95,7 +95,7 @@ class KotlinModuleOutOfCodeBlockModificationTracker private constructor(private
|
||||
}
|
||||
}
|
||||
|
||||
internal fun onPsiModificationTrackerUpdate(customIncrement: Int) {
|
||||
internal fun onPsiModificationTrackerUpdate(customIncrement: Int = 0) {
|
||||
val newModCount = kotlinOfOfCodeBlockTracker.modificationCount
|
||||
val affectedModule = lastAffectedModule
|
||||
if (affectedModule != null && newModCount == lastAffectedModuleModCount + customIncrement) {
|
||||
|
||||
@@ -50,4 +50,7 @@ inline fun invokeLater(crossinline action: () -> Unit) {
|
||||
ApplicationManager.getApplication().invokeLater { action() }
|
||||
}
|
||||
|
||||
inline fun isUnitTestMode(): Boolean = ApplicationManager.getApplication().isUnitTestMode
|
||||
inline fun isUnitTestMode(): Boolean = ApplicationManager.getApplication().isUnitTestMode
|
||||
|
||||
fun <T> Project.getServiceSafe(serviceClass: Class<T>): T =
|
||||
this.getService(serviceClass) ?: error("Unable to locate service ${serviceClass.name}")
|
||||
@@ -18,6 +18,7 @@ package org.jetbrains.kotlin.idea.util.application
|
||||
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.command.CommandProcessor
|
||||
import com.intellij.openapi.components.ServiceManager
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressIndicatorProvider
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
@@ -52,4 +53,7 @@ inline fun invokeLater(crossinline action: () -> Unit) {
|
||||
ApplicationManager.getApplication().invokeLater { action() }
|
||||
}
|
||||
|
||||
inline fun isUnitTestMode(): Boolean = ApplicationManager.getApplication().isUnitTestMode
|
||||
inline fun isUnitTestMode(): Boolean = ApplicationManager.getApplication().isUnitTestMode
|
||||
|
||||
fun <T> Project.getServiceSafe(serviceClass: Class<T>): T =
|
||||
ServiceManager.getService(this, serviceClass) ?: error("Unable to locate service ${serviceClass.name}")
|
||||
|
||||
@@ -50,4 +50,7 @@ inline fun invokeLater(crossinline action: () -> Unit) {
|
||||
ApplicationManager.getApplication().invokeLater { action() }
|
||||
}
|
||||
|
||||
inline fun isUnitTestMode(): Boolean = ApplicationManager.getApplication().isUnitTestMode
|
||||
inline fun isUnitTestMode(): Boolean = ApplicationManager.getApplication().isUnitTestMode
|
||||
|
||||
fun <T> Project.getServiceSafe(serviceClass: Class<T>) =
|
||||
this.getService(serviceClass) ?: error("Unable to locate service ${serviceClass.name}")
|
||||
@@ -18,6 +18,7 @@ package org.jetbrains.kotlin.idea.util.application
|
||||
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.command.CommandProcessor
|
||||
import com.intellij.openapi.components.ServiceManager
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressIndicatorProvider
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
@@ -52,4 +53,10 @@ inline fun invokeLater(crossinline action: () -> Unit) {
|
||||
ApplicationManager.getApplication().invokeLater { action() }
|
||||
}
|
||||
|
||||
inline fun isUnitTestMode(): Boolean = ApplicationManager.getApplication().isUnitTestMode
|
||||
inline fun isUnitTestMode(): Boolean = ApplicationManager.getApplication().isUnitTestMode
|
||||
|
||||
fun <T> Project.getServiceSafe(serviceClass: Class<T>): T =
|
||||
ServiceManager.getService(this, serviceClass) ?: error("Unable to locate service ${serviceClass.name}")
|
||||
|
||||
fun <T> Project.getServiceIfCreated(serviceClass: Class<T>): T? =
|
||||
ServiceManager.getServiceIfCreated(this, serviceClass)
|
||||
Reference in New Issue
Block a user