From 8a0d998020817aaa56736fa1b5ea650c962e5de3 Mon Sep 17 00:00:00 2001 From: Dzianis Lisiankou Date: Tue, 5 Mar 2024 11:28:36 +0100 Subject: [PATCH] IJMP-1533: close files in editor when deleting related file --- .../synchronizer/ContentSynchronizer.kt | 6 + .../RemoteAttributedContentSynchronizer.kt | 7 ++ .../explorer/ui/ExplorerTreeView.kt | 27 ++++- .../explorer/ui/ExplorerTreeViewTestSpec.kt | 104 ++++++++++++++++++ 4 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 src/test/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerTreeViewTestSpec.kt diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/ContentSynchronizer.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/ContentSynchronizer.kt index 8488c99f..c94e392f 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/ContentSynchronizer.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/ContentSynchronizer.kt @@ -69,4 +69,10 @@ interface ContentSynchronizer { */ fun isFileUploadNeeded(syncProvider: SyncProvider): Boolean + /** + * Marks file as not needed for synchronisation until the next time file is modified. + * @param syncProvider instance of [SyncProvider] class that contains file to mark. + */ + fun markAsNotNeededForSync(syncProvider: SyncProvider) + } diff --git a/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/RemoteAttributedContentSynchronizer.kt b/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/RemoteAttributedContentSynchronizer.kt index c0f8bd86..11ee81b7 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/RemoteAttributedContentSynchronizer.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/dataops/content/synchronizer/RemoteAttributedContentSynchronizer.kt @@ -204,4 +204,11 @@ abstract class RemoteAttributedContentSynchronizer override fun isFileUploadNeeded(syncProvider: SyncProvider): Boolean { return needToUpload.firstOrNull { syncProvider == it } != null } + + /** + * Base implementation of [ContentSynchronizer.markAsNotNeededForSync] method for each content synchronizer. + */ + override fun markAsNotNeededForSync(syncProvider: SyncProvider) { + needToUpload.remove(syncProvider) + } } diff --git a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerTreeView.kt b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerTreeView.kt index 8463699b..d481d4b1 100644 --- a/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerTreeView.kt +++ b/src/main/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerTreeView.kt @@ -4,11 +4,13 @@ import com.intellij.ide.dnd.aware.DnDAwareTree import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.* import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service import com.intellij.openapi.editor.ex.util.EditorUtil import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.impl.text.EditorHighlighterUpdater import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer +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 @@ -71,7 +73,7 @@ inline fun > AnActionEvent.getEx abstract class ExplorerTreeView, UnitConfig : EntityWithUuid> ( val explorer: Explorer, - project: Project, + private val project: Project, parentDisposable: Disposable, private val contextMenu: ActionGroup, rootNodeProvider: (explorer: Explorer, project: Project, treeStructure: ExplorerTreeStructureBase) -> ExplorerTreeNode, @@ -227,6 +229,14 @@ abstract class ExplorerTreeView) { + // listens for virtual file delete events and + // closes files opened in editor if file to be deleted is an ancestor of these files + events.filterIsInstance().forEach { + closeChildrenInEditor(it.file) + } + } + override fun after(events: List) { events .mapNotNull { @@ -374,5 +384,20 @@ abstract class ExplorerTreeView + if (VfsUtilCore.isAncestor(selectedFile, openFile, false)) { + val contentSynchronizer = service().getContentSynchronizer(openFile) + val syncProvider = DocumentedSyncProvider(openFile) + contentSynchronizer?.markAsNotNeededForSync(syncProvider) + runWriteActionInEdtAndWait { + fileEditorManager.closeFile(openFile) + } + } + } + } } diff --git a/src/test/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerTreeViewTestSpec.kt b/src/test/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerTreeViewTestSpec.kt new file mode 100644 index 00000000..207b3fca --- /dev/null +++ b/src/test/kotlin/eu/ibagroup/formainframe/explorer/ui/ExplorerTreeViewTestSpec.kt @@ -0,0 +1,104 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright IBA Group 2020 + */ + +package eu.ibagroup.formainframe.explorer.ui + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.vfs.VfsUtilCore +import com.intellij.openapi.vfs.VirtualFile +import eu.ibagroup.formainframe.config.connect.ConnectionConfig +import eu.ibagroup.formainframe.dataops.DataOpsManager +import eu.ibagroup.formainframe.dataops.content.synchronizer.ContentSynchronizer +import eu.ibagroup.formainframe.explorer.* +import eu.ibagroup.formainframe.testutils.WithApplicationShouldSpec +import eu.ibagroup.formainframe.testutils.testServiceImpl.TestDataOpsManagerImpl +import eu.ibagroup.formainframe.utils.service +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.shouldBe +import io.mockk.* +import kotlin.reflect.KFunction + +class ExplorerTreeViewTestSpec: WithApplicationShouldSpec({ + afterSpec { + clearAllMocks() + } + context("Explorer module: ui/ExplorerTreeView") { + + lateinit var fileExplorerView: ExplorerTreeView<*, *, *> + + val explorerMock = mockk>() + every { explorerMock.componentManager } returns ApplicationManager.getApplication() + + val openFilesMock = arrayOf(mockk(), mockk()) + var closedFileSize = 0 + + val dataOpsManagerService = + ApplicationManager.getApplication().service() as TestDataOpsManagerImpl + + val contentSynchronizerMock = mockk() + every { contentSynchronizerMock.markAsNotNeededForSync(any()) } returns Unit + + beforeEach { + mockkConstructor(CommonExplorerTreeStructure::class) + every { anyConstructed>().rootElement } returns Unit + + fileExplorerView = spyk( + FileExplorerView( + explorerMock, + mockk(), + mockk(), + mockk(), + { _, _, _ -> mockk() } + ) { } + ) + + closedFileSize = 0 + mockkStatic(FileEditorManager::getInstance) + every { FileEditorManager.getInstance(any()) } returns object : TestFileEditorManager() { + override fun getOpenFiles(): Array { + return openFilesMock + } + + override fun closeFile(file: VirtualFile) { + closedFileSize++ + } + } + + val isAncestorRef: (VirtualFile, VirtualFile, Boolean) -> Boolean = VfsUtilCore::isAncestor + mockkStatic(isAncestorRef as KFunction<*>) + every { VfsUtilCore.isAncestor(any(), any(), any()) } returns true + + dataOpsManagerService.testInstance = object : TestDataOpsManagerImpl(explorerMock.componentManager) { + override fun getContentSynchronizer(file: VirtualFile): ContentSynchronizer { + return contentSynchronizerMock + } + } + } + + afterEach { + unmockkAll() + } + + // closeChildrenInEditor + should("close files in editor if selected file is their ancestor") { + fileExplorerView.closeChildrenInEditor(mockk()) + + assertSoftly { closedFileSize shouldBe openFilesMock.size } + } + should("don't close files in editor if selected file is not their ancestor") { + every { VfsUtilCore.isAncestor(any(), any(), any()) } returns false + + fileExplorerView.closeChildrenInEditor(mockk()) + + assertSoftly { closedFileSize shouldBe 0 } + } + } +}) \ No newline at end of file