[Analysis API] Perform out-of-block for import changes in code fragments

^KT-65600 Fixed
diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/LLFirDeclarationModificationService.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/LLFirDeclarationModificationService.kt
index 8fc163d..2dc1bfe 100644
--- a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/LLFirDeclarationModificationService.kt
+++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/LLFirDeclarationModificationService.kt
@@ -185,8 +185,12 @@
         val inBlockModificationOwner = nonLocalDeclarationForLocalChange(element) ?: return ChangeType.OutOfBlock
 
         if (inBlockModificationOwner is KtCodeFragment) {
-            // All code fragment content is local
-            return ChangeType.InBlock(inBlockModificationOwner, project)
+            return if (handleCodeFragmentImportsChange(inBlockModificationOwner)) {
+                ChangeType.OutOfBlock
+            } else {
+                // All changes inside the code fragment are local
+                ChangeType.InBlock(inBlockModificationOwner, project)
+            }
         }
 
         val isOutOfBlockChange = element.isNewDirectChildOf(inBlockModificationOwner, modificationType)
@@ -199,6 +203,33 @@
     }
 
     /**
+     * The function changes whether new import directives were added to the code fragment since the last invalidation event.
+     * While changes in the code fragment itself are local as only the body of 'FirCodeFragment' is changed,
+     * imports are projected to 'FirImports' which are outside the 'FirCodeFragment', and those additional imports won't be added to
+     * the 'FirFile' unless an out-of-block modification happens, causing re-creation of the whole 'FirFile'.
+     *
+     * Warning: the function is not pure, as it manipulates the import counter.
+     * Even if it returned 'true' once, consequent calls will most likely return 'false'.
+     *
+     * Note: because of the implementation, the first modification event in a code fragment will trigger the out-of-block event,
+     * as the user data is not set. Additional API in the 'KtCodeFragment' itself is needed to fix this.
+     *
+     * @return true if the imports changed in a code fragment since the last invalidation event.
+     */
+    private fun handleCodeFragmentImportsChange(codeFragment: KtCodeFragment): Boolean {
+        val currentImportCount = codeFragment.importDirectives.size
+        val lastImportCount = codeFragment.getUserData(cachedCodeFragmentImportCountKey)
+
+        /** In [KtCodeFragment], imports can only be added, and not removed/changed.  */
+        if (lastImportCount == null || lastImportCount != currentImportCount) {
+            codeFragment.putUserData(cachedCodeFragmentImportCountKey, currentImportCount)
+            return true
+        }
+
+        return false
+    }
+
+    /**
      * This check covers cases such as a new body that was added to a function, which should cause an out-of-block modification.
      */
     private fun PsiElement.isNewDirectChildOf(inBlockModificationOwner: KtAnnotated, modificationType: ModificationType): Boolean =
@@ -345,6 +376,8 @@
     }
 }
 
+private val cachedCodeFragmentImportCountKey = Key.create<Int>("CACHED_CODE_FRAGMENT_IMPORTS")
+
 /**
  * The purpose of this property as user data is to avoid FIR building in case the [KtAnnotated]
  * doesn't have an associated FIR with body.