Provide incremental analysis of a file when it is applicable
#KT32868 Fixed
This commit is contained in:
+5
-14
@@ -246,24 +246,15 @@ class KotlinCacheServiceImpl(val project: Project) : KotlinCacheService {
|
||||
val specialModuleInfo = files.map(KtFile::getModuleInfo).toSet().single()
|
||||
val settings = specialModuleInfo.platformSettings(specialModuleInfo.platform ?: targetPlatform)
|
||||
|
||||
// File copies are created during completion and receive correct modification events through POM.
|
||||
// Dummy files created e.g. by J2K do not receive events.
|
||||
val filesModificationTracker = if (files.all { it.originalFile != it }) {
|
||||
ModificationTracker {
|
||||
files.sumByLong { it.outOfBlockModificationCount }
|
||||
}
|
||||
val dependenciesForSyntheticFileCache = if (files.all { it.originalFile != it }) {
|
||||
emptyList()
|
||||
} else {
|
||||
ModificationTracker {
|
||||
files.sumByLong { it.outOfBlockModificationCount + it.modificationStamp }
|
||||
}
|
||||
listOf(ModificationTracker {
|
||||
files.sumByLong { it.modificationStamp }
|
||||
})
|
||||
}
|
||||
|
||||
val dependenciesForSyntheticFileCache =
|
||||
listOf(
|
||||
KotlinCodeBlockModificationListener.getInstance(project).kotlinOutOfCodeBlockTracker,
|
||||
filesModificationTracker
|
||||
)
|
||||
|
||||
val resolverDebugName =
|
||||
"$resolverForSpecialInfoName $specialModuleInfo for files ${files.joinToString { it.name }} for platform $targetPlatform"
|
||||
|
||||
|
||||
+222
-34
@@ -16,12 +16,15 @@
|
||||
|
||||
package org.jetbrains.kotlin.idea.caches.resolve
|
||||
|
||||
import com.google.common.collect.ImmutableMap
|
||||
import com.intellij.openapi.progress.ProcessCanceledException
|
||||
import com.intellij.openapi.progress.ProgressIndicatorProvider
|
||||
import com.intellij.openapi.project.DumbService
|
||||
import com.intellij.openapi.project.IndexNotReadyException
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.ModificationTracker
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import com.intellij.psi.util.*
|
||||
import org.jetbrains.kotlin.analyzer.AnalysisResult
|
||||
import org.jetbrains.kotlin.container.ComponentProvider
|
||||
import org.jetbrains.kotlin.container.get
|
||||
@@ -29,18 +32,25 @@ import org.jetbrains.kotlin.context.GlobalContext
|
||||
import org.jetbrains.kotlin.context.withModule
|
||||
import org.jetbrains.kotlin.context.withProject
|
||||
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
|
||||
import org.jetbrains.kotlin.diagnostics.Diagnostic
|
||||
import org.jetbrains.kotlin.diagnostics.DiagnosticUtils
|
||||
import org.jetbrains.kotlin.frontend.di.createContainerForLazyBodyResolve
|
||||
import org.jetbrains.kotlin.idea.caches.project.getModuleInfo
|
||||
import org.jetbrains.kotlin.idea.caches.trackers.clearInBlockModifications
|
||||
import org.jetbrains.kotlin.idea.caches.trackers.inBlockModifications
|
||||
import org.jetbrains.kotlin.idea.project.IdeaModuleStructureOracle
|
||||
import org.jetbrains.kotlin.idea.project.TargetPlatformDetector
|
||||
import org.jetbrains.kotlin.idea.project.findAnalyzerServices
|
||||
import org.jetbrains.kotlin.idea.project.languageVersionSettings
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf
|
||||
import org.jetbrains.kotlin.resolve.*
|
||||
import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics
|
||||
import org.jetbrains.kotlin.resolve.diagnostics.DiagnosticsElementsCache
|
||||
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
|
||||
import org.jetbrains.kotlin.resolve.lazy.ResolveSession
|
||||
import org.jetbrains.kotlin.types.KotlinType
|
||||
import org.jetbrains.kotlin.util.slicedMap.ReadOnlySlice
|
||||
import org.jetbrains.kotlin.util.slicedMap.WritableSlice
|
||||
import java.util.*
|
||||
|
||||
internal class PerFileAnalysisCache(val file: KtFile, componentProvider: ComponentProvider) {
|
||||
@@ -51,6 +61,89 @@ internal class PerFileAnalysisCache(val file: KtFile, componentProvider: Compone
|
||||
private val bodyResolveCache = componentProvider.get<BodyResolveCache>()
|
||||
|
||||
private val cache = HashMap<PsiElement, AnalysisResult>()
|
||||
private var fileResult: AnalysisResult? = null
|
||||
|
||||
fun getAnalysisResults(element: KtElement): AnalysisResult {
|
||||
assert(element.containingKtFile == file) { "Wrong file. Expected $file, but was ${element.containingKtFile}" }
|
||||
|
||||
val analyzableParent = KotlinResolveDataProvider.findAnalyzableParent(element)
|
||||
|
||||
return synchronized(this) {
|
||||
ProgressIndicatorProvider.checkCanceled()
|
||||
|
||||
// step 1: perform incremental analysis IF it is applicable
|
||||
getIncrementalAnalysisResult()?.let { return it }
|
||||
|
||||
// cache does not contain AnalysisResult per each kt/psi element
|
||||
// instead it looks up analysis for its parents - see lookUp(analyzableElement)
|
||||
|
||||
// step 2: return result if it is cached
|
||||
lookUp(analyzableParent)?.let {
|
||||
return@synchronized it
|
||||
}
|
||||
|
||||
// step 3: perform analyze of analyzableParent as nothing has been cached yet
|
||||
val result = analyze(analyzableParent)
|
||||
cache[analyzableParent] = result
|
||||
|
||||
return@synchronized result
|
||||
}
|
||||
}
|
||||
|
||||
private fun getIncrementalAnalysisResult(): AnalysisResult? {
|
||||
// move fileResult from cache if it is stored there
|
||||
if (fileResult == null && cache.containsKey(file)) {
|
||||
fileResult = cache[file]
|
||||
|
||||
// drop existed results for entire cache:
|
||||
// if incremental analysis is applicable it will produce a single value for file
|
||||
// otherwise those results are potentially stale
|
||||
cache.clear()
|
||||
}
|
||||
|
||||
val inBlockModifications = file.inBlockModifications
|
||||
if (inBlockModifications.isNotEmpty()) {
|
||||
try {
|
||||
// IF there is a cached result for ktFile and there are inBlockModifications
|
||||
fileResult = fileResult?.let { result ->
|
||||
var analysisResult = result
|
||||
for (inBlockModification in inBlockModifications) {
|
||||
val resultCtx = analysisResult.bindingContext
|
||||
|
||||
val stackedCtx =
|
||||
if (resultCtx is StackedCompositeBindingContextTrace.StackedCompositeBindingContext) resultCtx else null
|
||||
|
||||
// no incremental analysis IF it is not applicable
|
||||
if (stackedCtx?.isIncrementalAnalysisApplicable() == false) return@let null
|
||||
|
||||
val trace: StackedCompositeBindingContextTrace =
|
||||
if (stackedCtx != null && stackedCtx.element() == inBlockModification) {
|
||||
val trace = stackedCtx.bindingTrace()
|
||||
trace.clear()
|
||||
trace
|
||||
} else {
|
||||
// to reflect a depth of stacked binding context
|
||||
val depth = (stackedCtx?.depth() ?: 0) + 1
|
||||
|
||||
StackedCompositeBindingContextTrace(
|
||||
depth,
|
||||
element = inBlockModification,
|
||||
resolveContext = resolveSession.bindingContext,
|
||||
parentContext = resultCtx
|
||||
)
|
||||
}
|
||||
|
||||
val newResult = analyze(inBlockModification, trace)
|
||||
analysisResult = wrapResult(result, newResult, trace)
|
||||
}
|
||||
analysisResult
|
||||
}
|
||||
} finally {
|
||||
file.clearInBlockModifications()
|
||||
}
|
||||
}
|
||||
return fileResult
|
||||
}
|
||||
|
||||
private fun lookUp(analyzableElement: KtElement): AnalysisResult? {
|
||||
// Looking for parent elements that are already analyzed
|
||||
@@ -75,25 +168,26 @@ internal class PerFileAnalysisCache(val file: KtFile, componentProvider: Compone
|
||||
return result
|
||||
}
|
||||
|
||||
fun getAnalysisResults(element: KtElement): AnalysisResult {
|
||||
assert(element.containingKtFile == file) { "Wrong file. Expected $file, but was ${element.containingKtFile}" }
|
||||
|
||||
val analyzableParent = KotlinResolveDataProvider.findAnalyzableParent(element)
|
||||
|
||||
return synchronized<AnalysisResult>(this) {
|
||||
|
||||
val cached = lookUp(analyzableParent)
|
||||
if (cached != null) return@synchronized cached
|
||||
|
||||
val result = analyze(analyzableParent)
|
||||
|
||||
cache[analyzableParent] = result
|
||||
|
||||
return@synchronized result
|
||||
private fun wrapResult(
|
||||
oldResult: AnalysisResult,
|
||||
newResult: AnalysisResult,
|
||||
elementBindingTrace: StackedCompositeBindingContextTrace
|
||||
): AnalysisResult {
|
||||
val newBindingCtx = elementBindingTrace.stackedContext
|
||||
return when {
|
||||
oldResult.isError() -> AnalysisResult.internalError(newBindingCtx, oldResult.error)
|
||||
newResult.isError() -> AnalysisResult.internalError(newBindingCtx, newResult.error)
|
||||
else -> AnalysisResult.success(
|
||||
newBindingCtx,
|
||||
oldResult.moduleDescriptor,
|
||||
oldResult.shouldGenerateCode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun analyze(analyzableElement: KtElement): AnalysisResult {
|
||||
private fun analyze(analyzableElement: KtElement, bindingTrace: BindingTrace? = null): AnalysisResult {
|
||||
ProgressIndicatorProvider.checkCanceled()
|
||||
|
||||
val project = analyzableElement.project
|
||||
if (DumbService.isDumb(project)) {
|
||||
return AnalysisResult.EMPTY
|
||||
@@ -107,7 +201,8 @@ internal class PerFileAnalysisCache(val file: KtFile, componentProvider: Compone
|
||||
resolveSession,
|
||||
codeFragmentAnalyzer,
|
||||
bodyResolveCache,
|
||||
analyzableElement
|
||||
analyzableElement,
|
||||
bindingTrace
|
||||
)
|
||||
} catch (e: ProcessCanceledException) {
|
||||
throw e
|
||||
@@ -122,6 +217,99 @@ internal class PerFileAnalysisCache(val file: KtFile, componentProvider: Compone
|
||||
}
|
||||
}
|
||||
|
||||
private class MergedDiagnostics(val diagnostics: Collection<Diagnostic>, override val modificationTracker: ModificationTracker) : Diagnostics {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private val elementsCache = DiagnosticsElementsCache(this) { true }
|
||||
|
||||
override fun all() = diagnostics
|
||||
|
||||
override fun forElement(psiElement: PsiElement): MutableCollection<Diagnostic> = elementsCache.getDiagnostics(psiElement)
|
||||
|
||||
override fun noSuppression() = this
|
||||
}
|
||||
|
||||
private class StackedCompositeBindingContextTrace(
|
||||
val depth: Int, // depth of stack over original ktFile bindingContext
|
||||
val element: KtElement,
|
||||
val resolveContext: BindingContext,
|
||||
val parentContext: BindingContext
|
||||
) : DelegatingBindingTrace(
|
||||
resolveContext,
|
||||
"Stacked trace for resolution of $element",
|
||||
allowSliceRewrite = true
|
||||
) {
|
||||
/**
|
||||
* Effectively StackedCompositeBindingContext holds up-to-date and partially outdated contexts (parentContext)
|
||||
*
|
||||
* The most up-to-date results for element are stored here (in a DelegatingBindingTrace#map)
|
||||
*
|
||||
* Note: It does not delete outdated results rather hide it therefore there is some extra memory footprint.
|
||||
*
|
||||
* Note: stackedContext differs from DelegatingBindingTrace#bindingContext:
|
||||
* if result is not present in this context it goes to parentContext rather to resolveContext
|
||||
* diagnostics are aggregated from this context and parentContext
|
||||
*/
|
||||
val stackedContext = StackedCompositeBindingContext()
|
||||
|
||||
/**
|
||||
* All diagnostics from parentContext apart those diagnostics those belongs to the element or its descendants
|
||||
*/
|
||||
val parentDiagnosticsApartElement: List<Diagnostic> = parentContext.diagnostics.all().filter { d ->
|
||||
d.psiElement.parentsWithSelf.none { it == element }
|
||||
}.toList()
|
||||
|
||||
inner class StackedCompositeBindingContext : BindingContext {
|
||||
var cachedDiagnostics: Diagnostics? = null
|
||||
|
||||
fun bindingTrace(): StackedCompositeBindingContextTrace = this@StackedCompositeBindingContextTrace
|
||||
|
||||
fun element(): KtElement = this@StackedCompositeBindingContextTrace.element
|
||||
|
||||
fun depth(): Int = this@StackedCompositeBindingContextTrace.depth
|
||||
|
||||
// to prevent too deep stacked binding context
|
||||
fun isIncrementalAnalysisApplicable(): Boolean = this@StackedCompositeBindingContextTrace.depth < 16
|
||||
|
||||
override fun getDiagnostics(): Diagnostics {
|
||||
if (cachedDiagnostics == null) {
|
||||
val diagnosticList =
|
||||
parentDiagnosticsApartElement + (this@StackedCompositeBindingContextTrace.mutableDiagnostics?.all() ?: emptyList())
|
||||
cachedDiagnostics = MergedDiagnostics(diagnosticList, parentContext.diagnostics.modificationTracker)
|
||||
}
|
||||
return cachedDiagnostics!!
|
||||
}
|
||||
|
||||
override fun <K : Any?, V : Any?> get(slice: ReadOnlySlice<K, V>, key: K): V? {
|
||||
return selfGet(slice, key) ?: parentContext.get(slice, key)
|
||||
}
|
||||
|
||||
override fun getType(expression: KtExpression): KotlinType? {
|
||||
val typeInfo = get(BindingContext.EXPRESSION_TYPE_INFO, expression)
|
||||
return typeInfo?.type
|
||||
}
|
||||
|
||||
override fun <K, V> getKeys(slice: WritableSlice<K, V>): Collection<K> {
|
||||
val keys = map.getKeys(slice)
|
||||
val fromParent = parentContext.getKeys(slice)
|
||||
if (keys.isEmpty()) return fromParent
|
||||
if (fromParent.isEmpty()) return keys
|
||||
|
||||
return keys + fromParent
|
||||
}
|
||||
|
||||
override fun <K : Any?, V : Any?> getSliceContents(slice: ReadOnlySlice<K, V>): ImmutableMap<K, V> {
|
||||
return ImmutableMap.copyOf(parentContext.getSliceContents(slice) + map.getSliceContents(slice))
|
||||
}
|
||||
|
||||
override fun addOwnDataTo(trace: BindingTrace, commitDiagnostics: Boolean) = throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
super.clear()
|
||||
stackedContext.cachedDiagnostics = null
|
||||
}
|
||||
}
|
||||
|
||||
private object KotlinResolveDataProvider {
|
||||
private val topmostElementTypes = arrayOf<Class<out PsiElement?>?>(
|
||||
KtNamedFunction::class.java,
|
||||
@@ -159,9 +347,9 @@ private object KotlinResolveDataProvider {
|
||||
if (analyzableElement is KtClassInitializer) return analyzableElement.containingDeclaration
|
||||
return analyzableElement
|
||||
// if none of the above worked, take the outermost declaration
|
||||
?: PsiTreeUtil.getTopmostParentOfType(element, KtDeclaration::class.java)
|
||||
// if even that didn't work, take the whole file
|
||||
?: element.containingKtFile
|
||||
?: PsiTreeUtil.getTopmostParentOfType(element, KtDeclaration::class.java)
|
||||
// if even that didn't work, take the whole file
|
||||
?: element.containingKtFile
|
||||
}
|
||||
|
||||
fun analyze(
|
||||
@@ -171,7 +359,8 @@ private object KotlinResolveDataProvider {
|
||||
resolveSession: ResolveSession,
|
||||
codeFragmentAnalyzer: CodeFragmentAnalyzer,
|
||||
bodyResolveCache: BodyResolveCache,
|
||||
analyzableElement: KtElement
|
||||
analyzableElement: KtElement,
|
||||
bindingTrace: BindingTrace?
|
||||
): AnalysisResult {
|
||||
try {
|
||||
if (analyzableElement is KtCodeFragment) {
|
||||
@@ -180,6 +369,16 @@ private object KotlinResolveDataProvider {
|
||||
return AnalysisResult.success(bindingContext, moduleDescriptor)
|
||||
}
|
||||
|
||||
val trace = bindingTrace ?: DelegatingBindingTrace(
|
||||
resolveSession.bindingContext,
|
||||
"Trace for resolution of $analyzableElement",
|
||||
allowSliceRewrite = true
|
||||
)
|
||||
|
||||
val moduleInfo = analyzableElement.containingKtFile.getModuleInfo()
|
||||
|
||||
val targetPlatform = moduleInfo.platform
|
||||
|
||||
/*
|
||||
Note that currently we *have* to re-create LazyTopDownAnalyzer with custom trace in order to disallow resolution of
|
||||
bodies in top-level trace (trace from DI-container).
|
||||
@@ -190,17 +389,6 @@ private object KotlinResolveDataProvider {
|
||||
(see 'functionAdditionalResolve'). However, this trace is still needed, because we have other
|
||||
codepaths for other KtDeclarationWithBodies (like property accessors/secondary constructors/class initializers)
|
||||
*/
|
||||
val trace = DelegatingBindingTrace(
|
||||
resolveSession.bindingContext,
|
||||
"Trace for resolution of " + analyzableElement,
|
||||
allowSliceRewrite = true
|
||||
)
|
||||
|
||||
val moduleInfo = analyzableElement.containingKtFile.getModuleInfo()
|
||||
|
||||
// TODO: should return proper platform!
|
||||
val targetPlatform = moduleInfo.platform ?: TargetPlatformDetector.getPlatform(analyzableElement.containingKtFile)
|
||||
|
||||
val lazyTopDownAnalyzer = createContainerForLazyBodyResolve(
|
||||
//TODO: should get ModuleContext
|
||||
globalContext.withProject(project).withModule(moduleDescriptor),
|
||||
|
||||
+3
-2
@@ -20,7 +20,6 @@ import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.util.CachedValueProvider
|
||||
import com.intellij.psi.util.CachedValuesManager
|
||||
import com.intellij.psi.util.PsiModificationTracker
|
||||
import com.intellij.util.containers.SLRUCache
|
||||
import org.jetbrains.kotlin.analyzer.*
|
||||
import org.jetbrains.kotlin.context.GlobalContextImpl
|
||||
@@ -28,6 +27,7 @@ import org.jetbrains.kotlin.context.withProject
|
||||
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
|
||||
import org.jetbrains.kotlin.idea.caches.project.*
|
||||
import org.jetbrains.kotlin.idea.caches.project.IdeaModuleInfo
|
||||
import org.jetbrains.kotlin.idea.caches.trackers.KotlinCodeBlockModificationListener
|
||||
import org.jetbrains.kotlin.psi.KtElement
|
||||
import org.jetbrains.kotlin.psi.KtFile
|
||||
import org.jetbrains.kotlin.resolve.CompositeBindingContext
|
||||
@@ -69,7 +69,8 @@ internal class ProjectResolutionFacade(
|
||||
}
|
||||
}
|
||||
|
||||
val allDependencies = resolverForProjectDependencies + listOf(PsiModificationTracker.MODIFICATION_COUNT)
|
||||
val allDependencies =
|
||||
resolverForProjectDependencies + listOf(KotlinCodeBlockModificationListener.getInstance(project).kotlinOutOfCodeBlockTracker)
|
||||
CachedValueProvider.Result.create(results, allDependencies)
|
||||
}, false
|
||||
)
|
||||
|
||||
+117
-19
@@ -5,6 +5,7 @@
|
||||
|
||||
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
|
||||
@@ -16,10 +17,7 @@ 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.psi.PsiDirectory
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiManager
|
||||
import com.intellij.psi.PsiTreeChangeEvent
|
||||
import com.intellij.psi.*
|
||||
import com.intellij.psi.impl.PsiManagerImpl
|
||||
import com.intellij.psi.impl.PsiModificationTrackerImpl
|
||||
import com.intellij.psi.impl.PsiTreeChangeEventImpl
|
||||
@@ -89,9 +87,17 @@ class KotlinCodeBlockModificationListener(
|
||||
incFileModificationCount(ktFile)
|
||||
|
||||
val changedElements = changeSet.changedElements
|
||||
// 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
|
||||
if (changedElements.any { getInsideCodeBlockModificationScope(it.psi) == null } || changedElements.isEmpty()) {
|
||||
|
||||
// skip change if it contains only virtual/fake change
|
||||
if (changedElements.isNotEmpty() &&
|
||||
// ignore formatting (whitespaces etc)
|
||||
(isFormattingChange(changeSet) ||
|
||||
changedElements.all { !it.psi.isPhysical })
|
||||
) return
|
||||
|
||||
val inBlockChange = inBlockModifications(changedElements)
|
||||
|
||||
if (!inBlockChange) {
|
||||
messageBusConnection.deliverImmediately()
|
||||
|
||||
if (ktFile.isPhysical && !isReplLine(ktFile.virtualFile)) {
|
||||
@@ -155,6 +161,8 @@ class KotlinCodeBlockModificationListener(
|
||||
}
|
||||
|
||||
private fun incOutOfBlockModificationCount(file: KtFile) {
|
||||
file.clearInBlockModifications()
|
||||
|
||||
val count = file.getUserData(FILE_OUT_OF_BLOCK_MODIFICATION_COUNT) ?: 0
|
||||
file.putUserData(FILE_OUT_OF_BLOCK_MODIFICATION_COUNT, count + 1)
|
||||
}
|
||||
@@ -165,32 +173,72 @@ class KotlinCodeBlockModificationListener(
|
||||
tracker.incModificationCount()
|
||||
}
|
||||
|
||||
fun getInsideCodeBlockModificationScope(element: PsiElement): KtElement? {
|
||||
private fun inBlockModifications(elements: Array<ASTNode>): Boolean {
|
||||
// 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
|
||||
if (elements.isEmpty()) return false
|
||||
|
||||
val inBlockElements = mutableSetOf<KtElement>()
|
||||
for (element in elements) {
|
||||
// skip fake PSI elements like `IntellijIdeaRulezzz$`
|
||||
val psi = element.psi
|
||||
if (!psi.isPhysical) continue
|
||||
|
||||
val modificationScope = getInsideCodeBlockModificationScope(psi) ?: return false
|
||||
|
||||
inBlockElements.add(modificationScope.blockDeclaration)
|
||||
}
|
||||
|
||||
inBlockElements.forEach { it.containingKtFile.addInBlockModifiedItem(it) }
|
||||
return inBlockElements.isNotEmpty()
|
||||
}
|
||||
|
||||
fun isFormattingChange(changeSet: TreeChangeEvent): Boolean =
|
||||
changeSet.changedElements.all {
|
||||
changeSet.getChangesByElement(it).affectedChildren.all { c -> (c is PsiWhiteSpace || c is PsiComment) }
|
||||
}
|
||||
|
||||
fun getInsideCodeBlockModificationScope(element: PsiElement): BlockModificationScopeElement? {
|
||||
val lambda = element.getTopmostParentOfType<KtLambdaExpression>()
|
||||
if (lambda is KtLambdaExpression) {
|
||||
lambda.getTopmostParentOfType<KtSuperTypeCallEntry>()?.let {
|
||||
return it
|
||||
return BlockModificationScopeElement(it, it)
|
||||
}
|
||||
}
|
||||
|
||||
val blockDeclaration = KtPsiUtil.getTopmostParentOfTypes(element, *BLOCK_DECLARATION_TYPES) as? KtDeclaration ?: return null
|
||||
if (KtPsiUtil.isLocal(blockDeclaration)) return null // should not be local declaration
|
||||
val blockDeclaration =
|
||||
KtPsiUtil.getTopmostParentOfTypes(element, *BLOCK_DECLARATION_TYPES) as? KtDeclaration ?: return null
|
||||
|
||||
// should not be local declaration
|
||||
if (KtPsiUtil.isLocal(blockDeclaration))
|
||||
return null
|
||||
|
||||
when (blockDeclaration) {
|
||||
is KtNamedFunction -> {
|
||||
if (blockDeclaration.hasBlockBody()) {
|
||||
return blockDeclaration.bodyExpression?.takeIf { it.isAncestor(element) }
|
||||
// case like `fun foo(): String {...<caret>...}`
|
||||
return blockDeclaration.bodyExpression
|
||||
?.takeIf { it.isAncestor(element) }
|
||||
?.let { BlockModificationScopeElement(blockDeclaration, it) }
|
||||
} else if (blockDeclaration.hasDeclaredReturnType()) {
|
||||
return blockDeclaration.initializer?.takeIf { it.isAncestor(element) }
|
||||
// case like `fun foo(): String = b<caret>labla`
|
||||
return blockDeclaration.initializer
|
||||
?.takeIf { it.isAncestor(element) }
|
||||
?.let { BlockModificationScopeElement(blockDeclaration, it) }
|
||||
}
|
||||
}
|
||||
|
||||
is KtProperty -> {
|
||||
if (blockDeclaration.typeReference != null) {
|
||||
for (accessor in blockDeclaration.accessors) {
|
||||
(accessor.initializer ?: accessor.bodyExpression)
|
||||
?.takeIf { it.isAncestor(element) }
|
||||
?.let { return it }
|
||||
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 { return BlockModificationScopeElement(blockDeclaration, it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,14 +249,37 @@ class KotlinCodeBlockModificationListener(
|
||||
?.lastOrNull()
|
||||
?.getLambdaExpression()
|
||||
?.takeIf { it.isAncestor(element) }
|
||||
?.let { BlockModificationScopeElement(blockDeclaration, it) }
|
||||
}
|
||||
|
||||
is KtClassInitializer -> {
|
||||
blockDeclaration
|
||||
.takeIf { it.isAncestor(element) }
|
||||
?.let { ktClassInitializer ->
|
||||
(KtPsiUtil.getTopmostParentOfTypes(blockDeclaration, KtClass::class.java) as? KtElement)?.let {
|
||||
return BlockModificationScopeElement(it, ktClassInitializer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: still under consideration - is it worth to track changes of private properties / methods
|
||||
// problem could be in diagnostics - it is worth to manage it with modTracker
|
||||
// is KtClass -> {
|
||||
// return when (element) {
|
||||
// is KtProperty -> if (element.visibilityModifierType()?.toVisibility() == Visibilities.PRIVATE) blockDeclaration else null
|
||||
// is KtNamedFunction -> if (element.visibilityModifierType()?.toVisibility() == Visibilities.PRIVATE) 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) }
|
||||
}
|
||||
@@ -216,6 +287,7 @@ class KotlinCodeBlockModificationListener(
|
||||
private val BLOCK_DECLARATION_TYPES = arrayOf<Class<out KtDeclaration>>(
|
||||
KtProperty::class.java,
|
||||
KtNamedFunction::class.java,
|
||||
KtClassInitializer::class.java,
|
||||
KtScriptInitializer::class.java
|
||||
)
|
||||
|
||||
@@ -231,6 +303,32 @@ val KtFile.perFileModificationTracker: ModificationTracker
|
||||
|
||||
private val FILE_OUT_OF_BLOCK_MODIFICATION_COUNT = Key<Long>("FILE_OUT_OF_BLOCK_MODIFICATION_COUNT")
|
||||
|
||||
val KtFile.outOfBlockModificationCount: Long
|
||||
get() = getUserData(FILE_OUT_OF_BLOCK_MODIFICATION_COUNT) ?: 0
|
||||
val KtFile.outOfBlockModificationCount: Long by NotNullableUserDataProperty(FILE_OUT_OF_BLOCK_MODIFICATION_COUNT, 0)
|
||||
|
||||
|
||||
/**
|
||||
* inBlockModifications is a collection of block elements those have in-block modifications
|
||||
*/
|
||||
private val IN_BLOCK_MODIFICATIONS = Key<MutableCollection<KtElement>>("IN_BLOCK_MODIFICATIONS")
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fun KtFile.clearInBlockModifications() {
|
||||
val collection = getUserData(IN_BLOCK_MODIFICATIONS)
|
||||
collection?.let {
|
||||
synchronized(it) {
|
||||
it.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-9
@@ -18,18 +18,13 @@ package org.jetbrains.kotlin.idea.highlighter
|
||||
|
||||
import com.intellij.codeInsight.daemon.ChangeLocalityDetector
|
||||
import com.intellij.psi.PsiElement
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.parents
|
||||
import org.jetbrains.kotlin.idea.caches.trackers.KotlinCodeBlockModificationListener.Companion.getInsideCodeBlockModificationScope
|
||||
|
||||
class KotlinChangeLocalityDetector : ChangeLocalityDetector {
|
||||
override fun getChangeHighlightingDirtyScopeFor(element: PsiElement): PsiElement? {
|
||||
val parent = element.parent
|
||||
if (element is KtBlockExpression && parent is KtNamedFunction && parent.name != null) {
|
||||
if (parent.parents.all { it is KtClassBody || it is KtClassOrObject || it is KtFile || it is KtScript }) {
|
||||
return parent
|
||||
}
|
||||
}
|
||||
val modificationScope =
|
||||
getInsideCodeBlockModificationScope(element) ?: return null
|
||||
|
||||
return null
|
||||
return modificationScope.blockDeclaration
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user