Write documents instead of binary files during transaction commit

This prevents sync issues with de corresponding document which might
show to the user as dialog asking how the conflict should be resolved.
diff --git a/org.eclipse.scout.sdk.core.ecj/src/main/java/org/eclipse/scout/sdk/core/model/ecj/JavaEnvironmentWithEcj.java b/org.eclipse.scout.sdk.core.ecj/src/main/java/org/eclipse/scout/sdk/core/model/ecj/JavaEnvironmentWithEcj.java
index 166a02e..509b40f 100644
--- a/org.eclipse.scout.sdk.core.ecj/src/main/java/org/eclipse/scout/sdk/core/model/ecj/JavaEnvironmentWithEcj.java
+++ b/org.eclipse.scout.sdk.core.ecj/src/main/java/org/eclipse/scout/sdk/core/model/ecj/JavaEnvironmentWithEcj.java
@@ -14,10 +14,12 @@
 import static java.util.Collections.unmodifiableMap;
 import static java.util.stream.Collectors.collectingAndThen;
 import static java.util.stream.Collectors.toList;
+import static org.eclipse.scout.sdk.core.log.SdkLog.onTrace;
 import static org.eclipse.scout.sdk.core.util.Ensure.fail;
 import static org.eclipse.scout.sdk.core.util.Ensure.newFail;
 
 import java.nio.CharBuffer;
+import java.nio.file.FileSystemAlreadyExistsException;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -56,6 +58,7 @@
 import org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding;
 import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
 import org.eclipse.jdt.internal.compiler.lookup.TypeVariableBinding;
+import org.eclipse.scout.sdk.core.log.SdkLog;
 import org.eclipse.scout.sdk.core.model.api.ISourceRange;
 import org.eclipse.scout.sdk.core.model.api.internal.SourceRange;
 import org.eclipse.scout.sdk.core.model.ecj.SourcePositionComparators.MethodBindingComparator;
@@ -392,7 +395,22 @@
   }
 
   protected FileSystemWithOverride getNameEnvironment() {
-    return m_fs.computeIfAbsentAndGet(() -> new FileSystemWithOverride(new ClasspathBuilder(javaHome(), m_rawClassPath)));
+    return m_fs.computeIfAbsentAndGet(this::buildNameEnvironment);
+  }
+
+  private FileSystemWithOverride buildNameEnvironment() {
+    // classpath registers a system wide file system but does not handle the fact that it might already have been created.
+    // see org.eclipse.jdt.internal.compiler.batch.ClasspathMultiReleaseJar.initialize
+    ClasspathBuilder cp = new ClasspathBuilder(javaHome(), m_rawClassPath);
+    while (true) {
+      try {
+        // optimistic creation without locking
+        return new FileSystemWithOverride(cp);
+      }
+      catch (FileSystemAlreadyExistsException e) {
+        SdkLog.debug("Concurrent registration of process wide filesystem.", onTrace(e));
+      }
+    }
   }
 
   /**
diff --git a/org.eclipse.scout.sdk.s2i/build.gradle.kts b/org.eclipse.scout.sdk.s2i/build.gradle.kts
index 06af3e1..6df25e2 100644
--- a/org.eclipse.scout.sdk.s2i/build.gradle.kts
+++ b/org.eclipse.scout.sdk.s2i/build.gradle.kts
@@ -45,7 +45,6 @@
     api("org.eclipse.scout.sdk", "org.eclipse.scout.sdk.core.s", SCOUT_SDK_VERSION)
     api("org.eclipse.scout.sdk", "org.eclipse.scout.sdk.core.ecj", SCOUT_SDK_VERSION)
     implementation("org.jetbrains.kotlin", "kotlin-stdlib-jdk8", KOTLIN_VERSION)
-    implementation("org.jetbrains.kotlin", "kotlin-reflect", KOTLIN_VERSION)
     testImplementation("org.mockito", "mockito-core", "3.3.3")
 }
 
diff --git a/org.eclipse.scout.sdk.s2i/org.eclipse.scout.sdk.s2i.iml b/org.eclipse.scout.sdk.s2i/org.eclipse.scout.sdk.s2i.iml
index bf8a52f..69a607b 100644
--- a/org.eclipse.scout.sdk.s2i/org.eclipse.scout.sdk.s2i.iml
+++ b/org.eclipse.scout.sdk.s2i/org.eclipse.scout.sdk.s2i.iml
@@ -355,7 +355,6 @@
       </library>
     </orderEntry>
     <orderEntry type="library" name="Gradle: org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.61" level="project" />
-    <orderEntry type="library" name="Gradle: org.jetbrains.kotlin:kotlin-reflect:1.3.61" level="project" />
     <orderEntry type="library" name="Gradle: wsdl4j:wsdl4j:1.6.2" level="project" />
     <orderEntry type="library" name="Gradle: org.eclipse.jdt:ecj:3.21.0" level="project" />
     <orderEntry type="library" name="Gradle: org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.61" level="project" />
diff --git a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/EclipseScoutBundle.kt b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/EclipseScoutBundle.kt
index fe1b4e9..0138301 100644
--- a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/EclipseScoutBundle.kt
+++ b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/EclipseScoutBundle.kt
@@ -29,7 +29,6 @@
      */
     override fun runActivity(project: Project) {
         derivedResourceManager(project).start() // it will dispose itself
-        classIdCache(project).scheduleCacheSetupIfEnabled() // it will dispose itself
     }
 
     companion object {
diff --git a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/Extensions.kt b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/Extensions.kt
index ba50999..600eb70 100644
--- a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/Extensions.kt
+++ b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/Extensions.kt
@@ -19,7 +19,7 @@
 import com.intellij.openapi.projectRoots.JavaSdk
 import com.intellij.openapi.roots.ModuleRootManager
 import com.intellij.openapi.roots.ProjectFileIndex
-import com.intellij.openapi.vfs.LocalFileSystem
+import com.intellij.openapi.vfs.VfsUtil
 import com.intellij.openapi.vfs.VfsUtilCore
 import com.intellij.openapi.vfs.VirtualFile
 import com.intellij.psi.JavaPsiFacade
@@ -136,8 +136,7 @@
     return CollectionQuery(resultWithRoot)
 }
 
-fun Path.toVirtualFile() = LocalFileSystem.getInstance()
-        .findFileByIoFile(this.toFile())
+fun Path.toVirtualFile() = VfsUtil.findFile(this, true)
         ?.takeIf { it.isValid }
 
 fun Project.findAllTypesAnnotatedWith(annotation: String, scope: SearchScope) = findAllTypesAnnotatedWith(annotation, scope, null)
@@ -198,6 +197,7 @@
     return SdkException(original)
 }
 
+// Can be removed if the supported min. IJ version is 2020.1
 private fun isUseLegacyMatcher() =
         try {
             findMatchesMethodNew()
diff --git a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/IdeaSettingsCommentGenerator.kt b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/IdeaSettingsCommentGenerator.kt
index d3612fd..003fee2 100644
--- a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/IdeaSettingsCommentGenerator.kt
+++ b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/IdeaSettingsCommentGenerator.kt
@@ -19,7 +19,6 @@
 import com.intellij.openapi.project.Project
 import com.intellij.openapi.startup.StartupActivity
 import com.intellij.openapi.util.text.StringUtil
-import com.intellij.openapi.vfs.LocalFileSystem
 import com.intellij.psi.PsiManager
 import com.maddyhome.idea.copyright.pattern.EntityUtil
 import com.maddyhome.idea.copyright.pattern.VelocityHelper
@@ -97,7 +96,7 @@
         }
 
         val project = module.project
-        val virtualFile = LocalFileSystem.getInstance().findFileByIoFile(path.toFile()) ?: return null
+        val virtualFile = path.toVirtualFile() ?: return null
         val psiFile = IdeaEnvironment.computeInReadAction(project) { psiManager.findFile(virtualFile) } ?: return null
 
         val raw = copyrightManager.getCopyrightOptions(psiFile)?.notice ?: return null
diff --git a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/classid/AddMissingClassIdQuickFix.kt b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/classid/AddMissingClassIdQuickFix.kt
index cfcbffe..e346e78 100644
--- a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/classid/AddMissingClassIdQuickFix.kt
+++ b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/classid/AddMissingClassIdQuickFix.kt
@@ -35,7 +35,7 @@
 
     override fun getFamilyName(): String = quickFixName
 
-    override fun applyFix(project: Project, descriptor: ProblemDescriptor) = runInNewTransaction(project) {
+    override fun applyFix(project: Project, descriptor: ProblemDescriptor) = runInNewTransaction(project, quickFixName) {
         val psiClass = PsiTreeUtil.getParentOfType(descriptor.psiElement, PsiClass::class.java)
                 ?: throw Ensure.newFail("No class found to add @ClassId. Element: '{}'.", descriptor.psiElement)
         val psiElementFactory = JavaPsiFacade.getElementFactory(project)
diff --git a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/classid/ChangeClassIdValueQuickFix.kt b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/classid/ChangeClassIdValueQuickFix.kt
index ed1efbc..bf554ad 100644
--- a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/classid/ChangeClassIdValueQuickFix.kt
+++ b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/classid/ChangeClassIdValueQuickFix.kt
@@ -30,7 +30,7 @@
 
     override fun getFamilyName(): String = quickFixName
 
-    override fun applyFix(project: Project, descriptor: ProblemDescriptor) = runInNewTransaction(project) {
+    override fun applyFix(project: Project, descriptor: ProblemDescriptor) = runInNewTransaction(project, quickFixName) {
         val newClassIdValue = ClassIds.next(annotation.psiClass.qualifiedName)
         val javaAnnotationSupport = LanguageAnnotationSupport.INSTANCE.forLanguage(annotation.psiClass.language)
         val value = javaAnnotationSupport.createLiteralValue(newClassIdValue, annotation.psiAnnotation)
diff --git a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/classid/ClassIdCache.kt b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/classid/ClassIdCache.kt
index 4b2ac9a..b5f51c3 100644
--- a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/classid/ClassIdCache.kt
+++ b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/classid/ClassIdCache.kt
@@ -13,7 +13,6 @@
 import com.intellij.openapi.Disposable
 import com.intellij.openapi.progress.ProgressIndicator
 import com.intellij.psi.search.SearchScope
-import java.util.concurrent.Future
 
 /**
  * Class to search for @ClassIds
@@ -35,12 +34,6 @@
     fun findAllClassIds(scope: SearchScope, indicator: ProgressIndicator? = null): Sequence<ClassIdAnnotation>
 
     /**
-     * Schedules the setup of the @ClassId cache if the DuplicateClassId inspection is active on the project.
-     * @return The [Future] of the asynchronous cache creation.
-     */
-    fun scheduleCacheSetupIfEnabled(): Future<*>
-
-    /**
      * Builds the cache. If the cache is already ready, this method does nothing.
      */
     fun setup()
diff --git a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/classid/ClassIdCacheImplementor.kt b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/classid/ClassIdCacheImplementor.kt
index 8460ce3..894dd1b 100644
--- a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/classid/ClassIdCacheImplementor.kt
+++ b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/classid/ClassIdCacheImplementor.kt
@@ -10,28 +10,22 @@
  */
 package org.eclipse.scout.sdk.s2i.classid
 
-import com.intellij.codeInsight.daemon.HighlightDisplayKey
+import com.intellij.openapi.progress.ProcessCanceledException
 import com.intellij.openapi.progress.ProgressIndicator
-import com.intellij.openapi.project.DumbService
 import com.intellij.openapi.project.Project
 import com.intellij.openapi.util.Disposer
-import com.intellij.profile.codeInspection.InspectionProjectProfileManager
 import com.intellij.psi.*
 import com.intellij.psi.search.GlobalSearchScope
 import com.intellij.psi.search.SearchScope
 import com.intellij.psi.util.PsiTreeUtil
-import com.intellij.util.concurrency.AppExecutorUtil
 import org.eclipse.scout.sdk.core.log.SdkLog
+import org.eclipse.scout.sdk.core.log.SdkLog.onTrace
 import org.eclipse.scout.sdk.core.s.IScoutRuntimeTypes
 import org.eclipse.scout.sdk.core.s.dto.AbstractDtoGenerator
-import org.eclipse.scout.sdk.s2i.environment.IdeaEnvironment.Factory.computeInReadAction
 import org.eclipse.scout.sdk.s2i.environment.TransactionManager
 import org.eclipse.scout.sdk.s2i.findAllTypesAnnotatedWith
 import java.util.Collections.newSetFromMap
-import java.util.concurrent.CompletableFuture.completedFuture
 import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.Future
-import java.util.concurrent.TimeUnit
 
 class ClassIdCacheImplementor(val project: Project) : PsiTreeChangeAdapter(), ClassIdCache {
 
@@ -45,9 +39,6 @@
         Disposer.register(project, this) // ensure it is disposed when the project closes
     }
 
-    fun isCacheRequired(): Boolean = !project.isDisposed && project.isOpen
-            && InspectionProjectProfileManager.getInstance(project).currentProfile.isToolEnabled(HighlightDisplayKey.find(DuplicateClassIdInspection.SHORT_NAME))
-
     override fun findAllClassIds(scope: SearchScope, indicator: ProgressIndicator?) =
             project.findAllTypesAnnotatedWith(IScoutRuntimeTypes.ClassId, scope, indicator)
                     .filter { it.isValid }
@@ -67,26 +58,20 @@
 
     override fun isCacheReady() = m_cacheReady
 
-    override fun scheduleCacheSetupIfEnabled(): Future<*> {
-        if (!isCacheRequired()) {
-            return completedFuture(null)
-        }
-        return AppExecutorUtil.getAppScheduledExecutorService().schedule(this::setup, 5, TimeUnit.SECONDS)
-    }
-
     override fun setup() = synchronized(m_classIdCache) {
         if (isCacheReady()) {
             return
         }
 
         try {
-            TransactionManager.repeatUntilPassesWithIndex(project) {
-                DumbService.getInstance(project).waitForSmartMode()
+            TransactionManager.repeatUntilPassesWithIndex(project, false) {
                 trySetupCache()
                 PsiManager.getInstance(project).addPsiTreeChangeListener(this) // initial cache is ready, register listener to keep it up to date
                 m_cacheReady = true
             }
             duplicates().forEach { SdkLog.debug("Duplicate @ClassId value '{}' found for types {}.", it.key, it.value) }
+        } catch (e: ProcessCanceledException) {
+            SdkLog.debug("@ClassId value cache creation canceled. Retry on next use.", onTrace(e))
         } catch (t: Exception) {
             SdkLog.warning("Error building @ClassId value cache.", t)
         }
@@ -109,11 +94,9 @@
         SdkLog.debug("Starting to build @ClassId value cache.")
         val start = System.currentTimeMillis()
         val fileCache = fileCache()
-        val allClassIds = computeInReadAction(project) {
-            findAllClassIds(GlobalSearchScope.projectScope(project))
-                    .filter { !ignoreClassId(it) }
-                    .toList() // collect to list so that it is executed in the read action (terminal operation)
-        }
+        val allClassIds = findAllClassIds(GlobalSearchScope.projectScope(project))
+                .filter { !ignoreClassId(it) }
+                .toList() // collect to list so that it is executed in the read action (terminal operation)
 
         allClassIds.forEach {
             val fqn = it.ownerFqn() ?: return@forEach
diff --git a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/classid/DuplicateClassIdInspection.kt b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/classid/DuplicateClassIdInspection.kt
index 1986579..f1a9b27 100644
--- a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/classid/DuplicateClassIdInspection.kt
+++ b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/classid/DuplicateClassIdInspection.kt
@@ -25,10 +25,6 @@
 
 open class DuplicateClassIdInspection : LocalInspectionTool() {
 
-    companion object {
-        const val SHORT_NAME = "DuplicateClassId"
-    }
-
     override fun checkFile(file: PsiFile, manager: InspectionManager, isOnTheFly: Boolean): Array<ProblemDescriptor> {
         return try {
             val javaFile = if (file is PsiJavaFile) file else null ?: return ProblemDescriptor.EMPTY_ARRAY
diff --git a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/environment/CompilationUnitWriteOperation.kt b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/environment/CompilationUnitWriteOperation.kt
index 9d8ffa7..a64c7fd 100644
--- a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/environment/CompilationUnitWriteOperation.kt
+++ b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/environment/CompilationUnitWriteOperation.kt
@@ -14,7 +14,6 @@
 import com.intellij.ide.util.DirectoryUtil
 import com.intellij.openapi.fileTypes.StdFileTypes
 import com.intellij.openapi.project.Project
-import com.intellij.openapi.vfs.LocalFileSystem
 import com.intellij.psi.PsiFile
 import com.intellij.psi.PsiFileFactory
 import com.intellij.psi.PsiManager
@@ -48,7 +47,8 @@
         progress.init(3, "Write {}", cuPath.fileName())
 
         // create in memory file
-        val newPsi = PsiFileFactory.getInstance(project).createFileFromText(cuPath.fileName(), StdFileTypes.JAVA, source, LocalTimeCounter.currentTime(), false, false)
+        val newPsi = PsiFileFactory.getInstance(project)
+                .createFileFromText(cuPath.fileName(), StdFileTypes.JAVA, source, LocalTimeCounter.currentTime(), false, false)
         progress.worked(1)
 
         formatSource(newPsi)
@@ -57,7 +57,7 @@
         optimizeImports(newPsi)
         progress.worked(1)
 
-        registerCompilationUnit(newPsi)
+        TransactionManager.current().register(CompilationUnitWriter(cuPath.targetFile(), newPsi))
 
         createdPsi = newPsi
     }
@@ -78,40 +78,26 @@
         }
     }
 
-    protected fun registerCompilationUnit(psi: PsiFile) {
-        val existingFile = LocalFileSystem.getInstance().findFileByIoFile(cuPath.targetFile().toFile())
-        if (existingFile?.exists() == true) {
-            // update existing file
-            TransactionManager
-                    .current()
-                    .register(FileWriter(cuPath.targetFile(), psi.text, project, existingFile))
-        } else {
-            // new file
-            TransactionManager
-                    .current()
-                    .register(NewCompilationUnitWriter(psi, cuPath.targetFile()))
-        }
-    }
 
     companion object {
-        private class NewCompilationUnitWriter(val psi: PsiFile, val targetFile: Path) : TransactionMember {
+        private class CompilationUnitWriter(val targetFile: Path, val psi: PsiFile) : TransactionMember {
 
             override fun file() = targetFile
 
             override fun commit(progress: IdeaProgress): Boolean {
-                progress.init(2, "Write {}", psi.name)
+                progress.init(2, "Write compilation unit {}", psi.name)
+
                 val targetDirectory = targetFile.parent
                 val dir = DirectoryUtil.mkdirs(PsiManager.getInstance(psi.project), targetDirectory.toString().replace(File.separatorChar, '/'))
                         ?: throw SdkException("Cannot write '$targetFile' because the directory could not be created.")
                 progress.worked(1)
 
-                // check again in case it changed in the meantime (must be idempotent)
                 val existingFile = dir.findFile(targetFile.fileName.toString())
                 if (existingFile == null) {
+                    SdkLog.debug("Add new compilation unit '{}'.", psi.name)
                     dir.add(psi)
                 } else {
-                    // it has been created in the meantime -> perform an update instead
-                    FileWriter(targetFile, psi.text, psi.project, existingFile.virtualFile).commit(toIdeaProgress(null))
+                    FileWriter(targetFile, psi.text, psi.project).commit(toIdeaProgress(null))
                 }
                 progress.worked(1)
                 return true
diff --git a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/environment/FileWriter.kt b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/environment/FileWriter.kt
index cd6e89c..4b95f30 100644
--- a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/environment/FileWriter.kt
+++ b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/environment/FileWriter.kt
@@ -10,24 +10,23 @@
  */
 package org.eclipse.scout.sdk.s2i.environment
 
+import com.intellij.openapi.fileEditor.FileDocumentManager
 import com.intellij.openapi.project.Project
-import com.intellij.openapi.vfs.LocalFileSystem
 import com.intellij.openapi.vfs.VfsUtil
-import com.intellij.openapi.vfs.VirtualFile
 import org.eclipse.scout.sdk.core.log.SdkLog
+import org.eclipse.scout.sdk.s2i.toVirtualFile
 import java.nio.file.Path
 
-open class FileWriter(val targetFile: Path, private val content: CharSequence, val project: Project, private val vFile: VirtualFile?) : TransactionMember {
-
-    constructor(file: Path, content: CharSequence, project: Project) : this(file, content, project, null)
+open class FileWriter(val targetFile: Path, private val content: CharSequence, val project: Project) : TransactionMember {
 
     override fun file(): Path = targetFile
 
     override fun commit(progress: IdeaProgress): Boolean {
-        progress.init(4, "Write file {}", targetFile.fileName)
+        progress.init(4, "Write file {}", targetFile)
 
-        var existingFile = vFile ?: LocalFileSystem.getInstance().findFileByIoFile(targetFile.toFile())
-        if (existingFile?.exists() != true) {
+        var existingFile = targetFile.toVirtualFile()
+        progress.worked(1)
+        if (existingFile == null || !existingFile.exists() || !existingFile.isValid) {
             // new file
             val dir = VfsUtil.createDirectoryIfMissing(targetFile.parent.toString())
             if (dir == null) {
@@ -35,12 +34,20 @@
                 return false
             }
             progress.worked(1)
+            SdkLog.debug("Adding new file '{}'.", targetFile)
             existingFile = dir.createChildData(this, targetFile.fileName.toString())
             progress.worked(1)
         }
         progress.setWorkRemaining(1)
 
-        existingFile.setBinaryContent(content.toString().toByteArray(existingFile.charset))
+
+        val documentManager = FileDocumentManager.getInstance()
+        val document = documentManager.getDocument(existingFile)
+        if (document == null) {
+            SdkLog.warning("Cannot load document for file '{}' to change its content.", targetFile)
+            return false
+        }
+        document.setText(content)
         progress.worked(1)
         return true
     }
diff --git a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/environment/IdeaEnvironment.kt b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/environment/IdeaEnvironment.kt
index c6da933..dc5c58e 100644
--- a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/environment/IdeaEnvironment.kt
+++ b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/environment/IdeaEnvironment.kt
@@ -11,7 +11,6 @@
 package org.eclipse.scout.sdk.s2i.environment
 
 import com.intellij.openapi.module.Module
-import com.intellij.openapi.project.DumbService
 import com.intellij.openapi.project.Project
 import com.intellij.openapi.roots.ProjectRootManager
 import com.intellij.openapi.vfs.VirtualFile
@@ -35,6 +34,7 @@
 import org.eclipse.scout.sdk.core.util.CoreUtils.toStringIfOverwritten
 import org.eclipse.scout.sdk.core.util.Ensure.newFail
 import org.eclipse.scout.sdk.s2i.*
+import org.eclipse.scout.sdk.s2i.environment.TransactionManager.Companion.repeatUntilPassesWithIndex
 import org.eclipse.scout.sdk.s2i.environment.model.JavaEnvironmentWithIdea
 import org.jetbrains.jps.model.serialization.PathMacroUtil
 import java.nio.file.Path
@@ -69,8 +69,7 @@
             return job.schedule({ result.get() })
         }
 
-        fun <T> computeInReadAction(project: Project, callable: () -> T): T =
-                DumbService.getInstance(project).runReadActionInSmartMode(callable)
+        fun <T> computeInReadAction(project: Project, callable: () -> T): T = repeatUntilPassesWithIndex(project, true, callable)
 
         fun toIdeaProgress(progress: IProgress?): IdeaProgress = progress?.toIdea() ?: IdeaProgress(null)
     }
diff --git a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/environment/OperationTask.kt b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/environment/OperationTask.kt
index 5afa48b..023d473 100644
--- a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/environment/OperationTask.kt
+++ b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/environment/OperationTask.kt
@@ -18,7 +18,6 @@
 import com.intellij.util.concurrency.AppExecutorUtil
 import org.eclipse.scout.sdk.core.log.SdkLog
 import org.eclipse.scout.sdk.core.s.environment.IFuture
-import org.eclipse.scout.sdk.core.util.CoreUtils
 import org.eclipse.scout.sdk.core.util.EventListenerList
 import org.eclipse.scout.sdk.core.util.FinalValue
 import org.eclipse.scout.sdk.s2i.environment.TransactionManager.Companion.callInExistingTransaction
@@ -31,7 +30,6 @@
 
     private val m_progress = FinalValue<ProgressIndicator>()
     private val m_listeners = EventListenerList()
-    private val m_name = CoreUtils.toStringIfOverwritten(task).orElse(null)
 
     override fun run(indicator: ProgressIndicator) {
         m_progress.setIfAbsent(indicator)
@@ -39,10 +37,10 @@
         val scoutProgress = indicator.toScoutProgress()
         val workForCommit = 10
         val workForTask = 1000
-        scoutProgress.init(workForTask + workForCommit, m_name)
+        scoutProgress.init(workForTask + workForCommit, title)
         if (transactionManager == null) {
             // new independent top level transaction
-            callInNewTransaction(project, { scoutProgress.newChild(workForCommit) }) {
+            callInNewTransaction(project, title, { scoutProgress.newChild(workForCommit) }) {
                 task.invoke(scoutProgress.newChild(workForTask))
             }
         } else {
@@ -99,32 +97,35 @@
 
     override fun onFinished() {
         super.onFinished()
-        try {
-            m_listeners.get(OperationTaskListener::class.java)
-                    .forEach { it.onFinished(this) }
-        } catch (e: RuntimeException) {
-            SdkLog.error("Error in {} when finishing task {}.", OperationTaskListener::class.java.simpleName, m_name, e)
+        m_listeners.get(OperationTaskListener::class.java).forEach {
+            try {
+                it.onFinished(this)
+            } catch (e: RuntimeException) {
+                SdkLog.error("Error in {} '{}' when finishing task {}.", OperationTaskListener::class.java.simpleName, it, title, e)
+            }
         }
     }
 
     override fun onThrowable(error: Throwable) {
         // no super call here
-        try {
-            m_listeners.get(OperationTaskListener::class.java)
-                    .forEach { it.onThrowable(this, error) }
-        } catch (e: RuntimeException) {
-            SdkLog.error("Unable to complete task. Original error:", error)
-            SdkLog.error("Error in {} when finishing task {} exceptionally. See previous log for details about the original error of the task.", OperationTaskListener::class.java.simpleName, m_name, e)
+        m_listeners.get(OperationTaskListener::class.java).forEach {
+            try {
+                it.onThrowable(this, error)
+            } catch (e: RuntimeException) {
+                SdkLog.error("Unable to complete task. Original error:", error)
+                SdkLog.error("Error in {} '{}' when finishing task {} exceptionally. See previous log for details about the original error of the task.", OperationTaskListener::class.java.simpleName, it, title, e)
+            }
         }
     }
 
     override fun onCancel() {
         super.onCancel()
-        try {
-            m_listeners.get(OperationTaskListener::class.java)
-                    .forEach { it.onCancel(this) }
-        } catch (e: RuntimeException) {
-            SdkLog.error("Error in {} when cancelling task {}.", OperationTaskListener::class.java.simpleName, m_name, e)
+        m_listeners.get(OperationTaskListener::class.java).forEach {
+            try {
+                it.onCancel(this)
+            } catch (e: RuntimeException) {
+                SdkLog.error("Error in {} '{}' when cancelling task {}.", OperationTaskListener::class.java.simpleName, it, title, e)
+            }
         }
     }
 }
diff --git a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/environment/TransactionManager.kt b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/environment/TransactionManager.kt
index d55246a..304fe67 100644
--- a/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/environment/TransactionManager.kt
+++ b/org.eclipse.scout.sdk.s2i/src/main/kotlin/org/eclipse/scout/sdk/s2i/environment/TransactionManager.kt
@@ -13,12 +13,12 @@
 import com.intellij.openapi.application.ApplicationManager
 import com.intellij.openapi.application.WriteAction
 import com.intellij.openapi.command.CommandProcessor
+import com.intellij.openapi.editor.Document
 import com.intellij.openapi.fileEditor.FileDocumentManager
 import com.intellij.openapi.progress.ProcessCanceledException
 import com.intellij.openapi.project.DumbService
 import com.intellij.openapi.project.IndexNotReadyException
 import com.intellij.openapi.project.Project
-import com.intellij.openapi.vfs.LocalFileSystem
 import com.intellij.openapi.vfs.ReadonlyStatusHandler
 import com.intellij.psi.PsiDocumentManager
 import org.eclipse.scout.sdk.core.log.SdkLog
@@ -26,25 +26,28 @@
 import org.eclipse.scout.sdk.core.util.CoreUtils.callInContext
 import org.eclipse.scout.sdk.core.util.Ensure
 import org.eclipse.scout.sdk.core.util.FinalValue
-import org.eclipse.scout.sdk.s2i.toNioPath
+import org.eclipse.scout.sdk.s2i.toVirtualFile
+import java.lang.reflect.Method
 import java.nio.file.Path
 
-class TransactionManager constructor(val project: Project) {
+class TransactionManager constructor(val project: Project, val transactionName: String? = null) {
 
     companion object {
 
         private val CURRENT = ThreadLocal.withInitial<TransactionManager> { null }
+        private val BULK_MODE_METHOD = FinalValue<Method?>()
 
         /**
          * Executes a task within a [TransactionManager] and commits all members on successful completion of the transaction.
          *
          * Successful completion means the given progress monitor is not canceled and no exception is thrown from the [runnable].
          * @param project The [Project] for which the transaction should be started
+         * @param name The name that represents the transaction to the user. E.g. when undoing actions (the confirmation dialog will show this text).
          * @param progressProvider A provider for a progress indicator to use when committing the transaction. This provider is also used to determine if the task has been canceled. Only if not canceled the transaction will be committed.
          * @param runnable The runnable to execute
          */
-        fun runInNewTransaction(project: Project, progressProvider: () -> IdeaProgress = { IdeaEnvironment.toIdeaProgress(null) }, runnable: () -> Unit) {
-            callInNewTransaction(project, progressProvider) {
+        fun runInNewTransaction(project: Project, name: String? = null, progressProvider: () -> IdeaProgress = { IdeaEnvironment.toIdeaProgress(null) }, runnable: () -> Unit) {
+            callInNewTransaction(project, name, progressProvider) {
                 runnable.invoke()
             }
         }
@@ -54,12 +57,13 @@
          *
          * Successful completion means the given progress monitor is not canceled and no exception is thrown from the [callable].
          * @param project The [Project] for which the transaction should be started
+         * @param name The name that represents the transaction to the user. E.g. when undoing actions (the confirmation dialog will show this text).
          * @param progressProvider A provider for a progress indicator to use when committing the transaction. This provider is also used to determine if the task has been canceled. Only if not canceled the transaction will be committed.
          * @param callable The runnable to execute
          */
-        fun <R> callInNewTransaction(project: Project, progressProvider: () -> IdeaProgress = { IdeaEnvironment.toIdeaProgress(null) }, callable: () -> R?): R? {
+        fun <R> callInNewTransaction(project: Project, name: String? = null, progressProvider: () -> IdeaProgress = { IdeaEnvironment.toIdeaProgress(null) }, callable: () -> R?): R? {
             var save = false
-            val transactionManager = TransactionManager(project)
+            val transactionManager = TransactionManager(project, name)
             val result: R?
             try {
                 result = callInExistingTransaction(transactionManager, callable)
@@ -94,26 +98,27 @@
          * The [callable] is executed in the UI thread but the call to this method waits until it is completed.
          *
          * @param project The [Project] for which the [callable] should be executed.
+         * @param name The name that represents the write action to the user. E.g. when undoing actions (the confirmation dialog will show this text).
          * @param callable The task to execute
          * @return The result of the [callable].
          */
-        fun <T> computeInWriteAction(project: Project, callable: () -> T): T {
+        fun <T> computeInWriteAction(project: Project, name: String? = null, callable: () -> T): T {
             val result = FinalValue<T>()
             // repeat outside the write lock to release the UI thread between retries (prevent freezes)
-            repeatUntilPassesWithIndex(project) {
+            repeatUntilPassesWithIndex(project, false/* not allowed because entering a write action afterwards */) {
                 ApplicationManager.getApplication().invokeAndWait {
                     // this is executed in the UI thread! keep short to prevent freezes!
                     // write operations are only allowed in the UI thread
                     // see http://www.jetbrains.org/intellij/sdk/docs/basics/architectural_overview/general_threading_rules.html
                     result.computeIfAbsent {
-                        WriteAction.compute<T, RuntimeException> { computeInSmartModeAndCommandProcessor(project, callable) }
+                        WriteAction.compute<T, RuntimeException> { computeInCommandProcessor(project, name, callable) }
                     }
                 }
             }
             return result.get()
         }
 
-        private fun <T> computeInSmartModeAndCommandProcessor(project: Project, callable: () -> T): T {
+        private fun <T> computeInCommandProcessor(project: Project, name: String? = null, callable: () -> T): T {
             val result = FinalValue<T>()
             if (!project.isInitialized) {
                 return result.get()
@@ -122,15 +127,19 @@
                 DumbService.getInstance(project).runReadActionInSmartMode {
                     result.computeIfAbsent(callable)
                 }
-            }, null, null)
+            }, name, null)
             return result.get()
         }
 
-        fun <T> repeatUntilPassesWithIndex(project: Project, callable: () -> T): T {
+        internal fun <T> repeatUntilPassesWithIndex(project: Project, executeInReadAction: Boolean, callable: () -> T): T {
             while (true) {
                 try {
                     if (project.isInitialized) { // includes !disposed & open
-                        return callable.invoke()
+                        return if (executeInReadAction) {
+                            DumbService.getInstance(project).runReadActionInSmartMode(callable)
+                        } else {
+                            callable.invoke()
+                        }
                     } else {
                         throw ProcessCanceledException()
                     }
@@ -152,6 +161,15 @@
             }
             return t
         }
+
+        private fun setInBulkUpdateMethod() = BULK_MODE_METHOD.computeIfAbsentAndGet {
+            try {
+                return@computeIfAbsentAndGet Document::class.java.getMethod("setInBulkUpdate", Boolean::class.java)
+            } catch (e: NoSuchMethodException) {
+                SdkLog.debug("Not using bulk mode for large document modifications because not supported by the platform.", onTrace(e))
+                return@computeIfAbsentAndGet null
+            }
+        }
     }
 
     private val m_members = HashMap<Path, MutableList<TransactionMember>>()
@@ -209,7 +227,7 @@
                 return false
             }
             // the boolean result might be null in case the callable was not executed because the project is closing
-            val result: Boolean? = computeInWriteAction(project) { commitAllInUiThread(progress) }
+            val result: Boolean? = computeInWriteAction(project, transactionName) { commitAllInUiThread(progress) }
             return result ?: return false
         } finally {
             m_members.clear()
@@ -219,13 +237,10 @@
 
     private fun commitAllInUiThread(progress: IdeaProgress): Boolean {
         val workForEnsureWritable = 1
-        progress.init(size() + workForEnsureWritable, "Flush file content")
+        progress.init(size() + workForEnsureWritable, "Starting to commit transaction. Number of members: {}", size())
 
-        val fileSystem = LocalFileSystem.getInstance()
-        val files = m_members.keys
-                .map(Path::toFile)
-                .mapNotNull(fileSystem::findFileByIoFile)
-
+        // make file writable
+        val files = m_members.keys.mapNotNull(Path::toVirtualFile)
         val status = ReadonlyStatusHandler.getInstance(project).ensureFilesWritable(files)
         if (status.hasReadonlyFiles()) {
             SdkLog.info("Unable to make all resources writable. Transaction will be discarded. Message: ${status.readonlyFilesMessage}")
@@ -233,29 +248,63 @@
         }
         progress.worked(workForEnsureWritable)
 
-        val success = m_members.values
-                .flatten()
-                .map { commitMember(it, progress.newChild(1)) }
-                .all { committed -> committed }
-        if (success) {
-            commitPsiDocuments(m_members.keys)
+        // validate documents
+        val documentManager = FileDocumentManager.getInstance()
+        val documents = files.map { file -> documentManager.getDocument(file) }
+        val documentsReady = documents.all { documentReady(it, documentManager) }
+        if (!documentsReady) {
+            SdkLog.warning("Cannot commit all transaction members because at least one document cannot be written.")
+            return false
         }
-        return success
+
+        // commit
+        return commitDocuments(documents.filterNotNull(), documentManager, progress)
     }
 
-    private fun commitPsiDocuments(files: Set<Path>) {
-        val psiDocManager = PsiDocumentManager.getInstance(project)
-        if (!psiDocManager.hasUncommitedDocuments()) {
-            return
+    @Suppress("MissingRecentApi")
+    private fun commitDocuments(documents: List<Document>, documentManager: FileDocumentManager, progress: IdeaProgress): Boolean {
+        val setInBulkUpdate = setInBulkUpdateMethod() // Can be removed if the supported min. IJ version is 2019.3
+        val useBulkMode = setInBulkUpdate != null && documents.size >= 100
+        try {
+            if (useBulkMode) {
+                documents.forEach { doc -> setInBulkUpdate!!.invoke(doc, true) }
+            }
+            val success = commitMembers(documents, progress)
+            if (success) {
+                documents.forEach { documentManager.saveDocument(it) }
+            }
+            return success
+        } finally {
+            if (useBulkMode) {
+                documents.forEach { doc -> setInBulkUpdate!!.invoke(doc, false) }
+            }
         }
+    }
 
-        val fileDocManager = FileDocumentManager.getInstance()
-        val uncommittedDocuments = psiDocManager.uncommittedDocuments
-        uncommittedDocuments
-                .associate { it to fileDocManager.getFile(it) }
-                .filter { files.contains(it.value?.toNioPath()) }
-                .map { it.key }
-                .forEach { psiDocManager.commitDocument(it) }
+    private fun commitMembers(documents: List<Document>, progress: IdeaProgress): Boolean {
+        val psiDocumentManager = PsiDocumentManager.getInstance(project)
+        try {
+            return m_members.values
+                    .flatten()
+                    .map { commitMember(it, progress.newChild(1)) }
+                    .all { committed -> committed }
+        } finally {
+            documents.forEach { psiDocumentManager.commitDocument(it) }
+        }
+    }
+
+    private fun documentReady(document: Document?, documentManager: FileDocumentManager): Boolean {
+        if (document == null) {
+            return false
+        }
+        if (!document.isWritable) {
+            return false
+        }
+        if (documentManager.isDocumentUnsaved(document)) {
+            // save before overwriting to ensure there are no conflicts afterwards
+            documentManager.saveDocument(document)
+        }
+        return true
     }
 
     private fun commitMember(member: TransactionMember, progress: IdeaProgress): Boolean {