diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 0172910..4969a4c 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -19,10 +19,10 @@ jobs: java-version: '21' distribution: 'temurin' cache: gradle - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 - name: Check Spotless run: ./gradlew spotlessCheck + - name: Run Unit Tests + run: ./gradlew test buildPlugin: runs-on: ubuntu-latest @@ -35,8 +35,6 @@ jobs: java-version: '21' distribution: 'temurin' cache: gradle - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 - name: Build plugin run: ./gradlew --no-configuration-cache buildPlugin - uses: actions/upload-artifact@v4.3.3 @@ -62,7 +60,5 @@ jobs: java-version: '21' distribution: 'temurin' cache: gradle - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 - name: Verify plugin run: ./gradlew --no-configuration-cache verifyPlugin diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 1bec35e..3ad4dc5 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,6 +1,11 @@ + + diff --git a/build.gradle.kts b/build.gradle.kts index e6a46c7..57161bd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,12 @@ +import org.jetbrains.intellij.platform.gradle.TestFrameworkType + plugins { id("java") id("org.jetbrains.kotlin.jvm") version "1.9.21" id("org.jetbrains.intellij.platform") version "2.0.0" id("com.diffplug.spotless") version "7.0.0.BETA2" + + id("com.adarshr.test-logger") version "4.0.0" } group = "com.vaadin" @@ -48,7 +52,12 @@ dependencies { pluginVerifier() zipSigner() instrumentationTools() + + testFramework(TestFrameworkType.Platform) } + + testImplementation(kotlin("test")) + testImplementation("junit:junit:4.13.2") } configure { @@ -84,4 +93,6 @@ tasks { channels.set(listOf(publishChannel)) token.set(System.getenv("PUBLISH_TOKEN")) } + + test { useJUnitPlatform() } } diff --git a/src/main/kotlin/com/vaadin/plugin/copilot/handler/AbstractHandler.kt b/src/main/kotlin/com/vaadin/plugin/copilot/handler/AbstractHandler.kt index a20c0bd..3c2f467 100644 --- a/src/main/kotlin/com/vaadin/plugin/copilot/handler/AbstractHandler.kt +++ b/src/main/kotlin/com/vaadin/plugin/copilot/handler/AbstractHandler.kt @@ -5,10 +5,13 @@ import com.intellij.openapi.editor.Document import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.fileEditor.FileEditor import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.OpenFileDescriptor import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.findDocument import com.intellij.psi.PsiDocumentManager import com.vaadin.plugin.copilot.CopilotPluginUtil +import com.vaadin.plugin.copilot.service.CopilotUndoManager import io.netty.handler.codec.http.HttpResponseStatus import java.io.File @@ -58,10 +61,30 @@ abstract class AbstractHandler(val project: Project) : Handler { return FileEditorWrapper(editors.first(), project, false) } + open fun postSave(vfsFile: VirtualFile) { + LOG.info("File $vfsFile contents saved") + notifyUndoManager(vfsFile) + commitAndFlush(vfsFile.findDocument()) + openFileInEditor(vfsFile) + } + + private fun openFileInEditor(vfsFile: VirtualFile) { + val openFileDescriptor = OpenFileDescriptor(project, vfsFile) + FileEditorManager.getInstance(project).openTextEditor(openFileDescriptor, false) + } + + fun notifyUndoManager(vfsFile: VirtualFile) { + getCopilotUndoManager().fileWritten(vfsFile) + } + fun commitAndFlush(vfsDoc: Document?) { if (vfsDoc != null) { PsiDocumentManager.getInstance(project).commitDocument(vfsDoc) FileDocumentManager.getInstance().saveDocuments(vfsDoc::equals) } } + + fun getCopilotUndoManager(): CopilotUndoManager { + return project.getService(CopilotUndoManager::class.java) + } } diff --git a/src/main/kotlin/com/vaadin/plugin/copilot/handler/RedoHandler.kt b/src/main/kotlin/com/vaadin/plugin/copilot/handler/RedoHandler.kt index 28d2dda..bb82bbf 100644 --- a/src/main/kotlin/com/vaadin/plugin/copilot/handler/RedoHandler.kt +++ b/src/main/kotlin/com/vaadin/plugin/copilot/handler/RedoHandler.kt @@ -1,36 +1,26 @@ package com.vaadin.plugin.copilot.handler -import com.intellij.openapi.application.runInEdt -import com.intellij.openapi.application.runWriteAction -import com.intellij.openapi.command.impl.UndoManagerImpl +import com.intellij.openapi.command.undo.UndoManager +import com.intellij.openapi.fileEditor.FileEditor import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.findDocument -import com.vaadin.plugin.actions.VaadinCompileOnSaveAction +import com.intellij.openapi.vfs.VirtualFile class RedoHandler(project: Project, data: Map) : UndoHandler(project, data) { - private val copilotActionPrefix = "_Redo Vaadin Copilot" + override fun getOpsCount(vfsFile: VirtualFile): Int { + return getCopilotUndoManager().getRedoCount(vfsFile) + } + + override fun before(vfsFile: VirtualFile) { + getCopilotUndoManager().redoStart(vfsFile) + } + + override fun runManagerAction(undoManager: UndoManager, editor: FileEditor) { + undoManager.redo(editor) + } - override fun run(): HandlerResponse { - for (vfsFile in vfsFiles) { - runInEdt { - getEditorWrapper(vfsFile).use { wrapper -> - val editor = wrapper.getFileEditor() - val undoManager = UndoManagerImpl.getInstance(project) - runWriteAction { - if (undoManager.isRedoAvailable(editor)) { - val redo = undoManager.getRedoActionNameAndDescription(editor).first - if (redo.startsWith(copilotActionPrefix)) { - undoManager.redo(editor) - commitAndFlush(vfsFile.findDocument()) - LOG.info("$redo performed on ${vfsFile.path}") - VaadinCompileOnSaveAction().compile(project, vfsFile) - } - } - } - } - } - } - return RESPONSE_OK + override fun after(vfsFile: VirtualFile) { + getCopilotUndoManager().redoDone(vfsFile) + LOG.info("$vfsFile redo performed") } } diff --git a/src/main/kotlin/com/vaadin/plugin/copilot/handler/UndoHandler.kt b/src/main/kotlin/com/vaadin/plugin/copilot/handler/UndoHandler.kt index dde3e9e..2ad3050 100644 --- a/src/main/kotlin/com/vaadin/plugin/copilot/handler/UndoHandler.kt +++ b/src/main/kotlin/com/vaadin/plugin/copilot/handler/UndoHandler.kt @@ -3,46 +3,62 @@ package com.vaadin.plugin.copilot.handler import com.intellij.openapi.application.runInEdt import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.command.impl.UndoManagerImpl +import com.intellij.openapi.command.undo.UndoManager +import com.intellij.openapi.fileEditor.FileEditor import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.findDocument -import com.vaadin.plugin.actions.VaadinCompileOnSaveAction import java.io.File open class UndoHandler(project: Project, data: Map) : AbstractHandler(project) { - private val copilotActionPrefix = "_Undo Vaadin Copilot" - - protected val vfsFiles: ArrayList = ArrayList() + private val vfsFiles: ArrayList = ArrayList() init { val paths = data["files"] as Collection for (path in paths) { val file = File(path) if (isFileInsideProject(project, file)) { - VfsUtil.findFileByIoFile(file, true)?.let { vfsFiles.add(it) } + var vfsFile = VfsUtil.findFileByIoFile(file, true) + if (vfsFile != null) { + vfsFiles.add(vfsFile) + } else { + // if we want to undo file removal we need to create empty virtual file to write + // content + runWriteAction { + val parent = VfsUtil.createDirectories(file.parent) + vfsFile = parent.createChildData(this, file.name) + vfsFiles.add(vfsFile!!) + } + } } else { - LOG.warn("File $file is not a part of a project") + LOG.warn("File $path is not a part of a project") } } } override fun run(): HandlerResponse { for (vfsFile in vfsFiles) { + val count = getOpsCount(vfsFile) + if (count == 0) { + continue + } + runInEdt { getEditorWrapper(vfsFile).use { wrapper -> val undoManager = UndoManagerImpl.getInstance(project) val editor = wrapper.getFileEditor() runWriteAction { - if (undoManager.isUndoAvailable(editor)) { - val undo = undoManager.getUndoActionNameAndDescription(editor).first - if (undo.startsWith(copilotActionPrefix)) { - undoManager.undo(editor) - commitAndFlush(vfsFile.findDocument()) - LOG.info("$undo performed on ${vfsFile.path}") - VaadinCompileOnSaveAction().compile(project, vfsFile) + try { + before(vfsFile) + var i = 0 + while (i++ < count) { + runManagerAction(undoManager, editor) } + commitAndFlush(vfsFile.findDocument()) + } finally { + after(vfsFile) } } } @@ -50,4 +66,21 @@ open class UndoHandler(project: Project, data: Map) : AbstractHandl } return RESPONSE_OK } + + open fun getOpsCount(vfsFile: VirtualFile): Int { + return getCopilotUndoManager().getUndoCount(vfsFile) + } + + open fun before(vfsFile: VirtualFile) { + getCopilotUndoManager().undoStart(vfsFile) + } + + open fun runManagerAction(undoManager: UndoManager, editor: FileEditor) { + undoManager.undo(editor) + } + + open fun after(vfsFile: VirtualFile) { + getCopilotUndoManager().undoDone(vfsFile) + LOG.info("$vfsFile undo performed") + } } diff --git a/src/main/kotlin/com/vaadin/plugin/copilot/handler/WriteBase64FileHandler.kt b/src/main/kotlin/com/vaadin/plugin/copilot/handler/WriteBase64FileHandler.kt index edeccc8..b8783cc 100644 --- a/src/main/kotlin/com/vaadin/plugin/copilot/handler/WriteBase64FileHandler.kt +++ b/src/main/kotlin/com/vaadin/plugin/copilot/handler/WriteBase64FileHandler.kt @@ -2,12 +2,7 @@ package com.vaadin.plugin.copilot.handler import com.intellij.openapi.editor.Document import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiFile -import com.intellij.psi.PsiManager -import java.io.File -import java.nio.file.Files import java.util.Base64 class WriteBase64FileHandler(project: Project, data: Map) : WriteFileHandler(project, data) { @@ -15,11 +10,4 @@ class WriteBase64FileHandler(project: Project, data: Map) : WriteFi override fun doWrite(vfsFile: VirtualFile?, doc: Document?, content: String) { vfsFile?.setBinaryContent(Base64.getDecoder().decode(content)) } - - override fun doCreate(ioFile: File, content: String): PsiFile { - Files.createFile(ioFile.toPath()) - val vfsFile = VfsUtil.findFileByIoFile(ioFile, true) - doWrite(vfsFile!!, null, content) - return PsiManager.getInstance(project).findFile(vfsFile)!! - } } diff --git a/src/main/kotlin/com/vaadin/plugin/copilot/handler/WriteFileHandler.kt b/src/main/kotlin/com/vaadin/plugin/copilot/handler/WriteFileHandler.kt index 80cec42..a95f1bd 100644 --- a/src/main/kotlin/com/vaadin/plugin/copilot/handler/WriteFileHandler.kt +++ b/src/main/kotlin/com/vaadin/plugin/copilot/handler/WriteFileHandler.kt @@ -1,24 +1,17 @@ package com.vaadin.plugin.copilot.handler import com.intellij.openapi.application.runInEdt -import com.intellij.openapi.application.runReadAction import com.intellij.openapi.command.CommandProcessor import com.intellij.openapi.command.UndoConfirmationPolicy import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.actionSystem.DocCommandGroupId -import com.intellij.openapi.fileEditor.FileEditorManager -import com.intellij.openapi.fileEditor.OpenFileDescriptor -import com.intellij.openapi.fileTypes.FileTypeManager import com.intellij.openapi.project.Project import com.intellij.openapi.util.text.Strings import com.intellij.openapi.vfs.ReadonlyStatusHandler import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.findDocument -import com.intellij.psi.PsiFile -import com.intellij.psi.PsiFileFactory -import com.intellij.psi.PsiManager import com.vaadin.plugin.utils.IdeUtil import io.netty.handler.codec.http.HttpResponseStatus import java.io.File @@ -67,9 +60,7 @@ open class WriteFileHandler(project: Project, data: Map) : Abstract { WriteCommandAction.runWriteCommandAction(project) { doWrite(vfsFile, it, content) - commitAndFlush(it) - LOG.info("File $ioFile contents saved") - openFileInEditor(vfsFile) + postSave(vfsFile) } }, undoLabel ?: "Vaadin Copilot Write File", @@ -80,54 +71,28 @@ open class WriteFileHandler(project: Project, data: Map) : Abstract } private fun create() { - val psiDir = runReadAction { - val parentDir = getOrCreateParentDir() - if (parentDir != null) { - return@runReadAction PsiManager.getInstance(project).findDirectory(parentDir) - } - return@runReadAction null - } - - if (psiDir != null) { - runInEdt { - CommandProcessor.getInstance() - .executeCommand( - project, - { - WriteCommandAction.runWriteCommandAction(project) { - val psiFile = doCreate(ioFile, content) - if (psiFile.containingDirectory == null) { - psiDir.add(psiFile) - } - - LOG.info("File $ioFile contents saved") - VfsUtil.findFileByIoFile(ioFile, true)?.let { vfsFile -> openFileInEditor(vfsFile) } - } - }, - undoLabel ?: "Vaadin Copilot Write File", - null, - UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION, - ) - } - } - } - - private fun openFileInEditor(vfsFile: VirtualFile) { - val openFileDescriptor = OpenFileDescriptor(project, vfsFile) - FileEditorManager.getInstance(project).openTextEditor(openFileDescriptor, false) - } - - private fun getOrCreateParentDir(): VirtualFile? { - if (!ioFile.parentFile.exists() && !ioFile.parentFile.mkdirs()) { - LOG.warn("Cannot create parent directories for ${ioFile.parent}") - return null + runInEdt { + CommandProcessor.getInstance() + .executeCommand( + project, + { + WriteCommandAction.runWriteCommandAction(project) { + val vfsFile = doCreate(ioFile, content) + postSave(vfsFile) + } + }, + undoLabel ?: "Vaadin Copilot Write File", + null, + UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION, + ) } - return VfsUtil.findFileByIoFile(ioFile.parentFile, true) } - open fun doCreate(ioFile: File, content: String): PsiFile { - val fileType = FileTypeManager.getInstance().getFileTypeByFileName(ioFile.name) - return PsiFileFactory.getInstance(project).createFileFromText(ioFile.name, fileType, content) + open fun doCreate(file: File, content: String): VirtualFile { + val parent = VfsUtil.createDirectories(file.parent) + val vfsFile = parent.createChildData(this, file.name) + doWrite(vfsFile, vfsFile.findDocument(), content) + return vfsFile } open fun doWrite(vfsFile: VirtualFile?, doc: Document?, content: String) { diff --git a/src/main/kotlin/com/vaadin/plugin/copilot/service/CopilotUndoManager.kt b/src/main/kotlin/com/vaadin/plugin/copilot/service/CopilotUndoManager.kt new file mode 100644 index 0000000..08ea756 --- /dev/null +++ b/src/main/kotlin/com/vaadin/plugin/copilot/service/CopilotUndoManager.kt @@ -0,0 +1,20 @@ +package com.vaadin.plugin.copilot.service + +import com.intellij.openapi.vfs.VirtualFile + +interface CopilotUndoManager { + + fun fileWritten(file: VirtualFile) + + fun getUndoCount(file: VirtualFile): Int + + fun getRedoCount(file: VirtualFile): Int + + fun undoStart(file: VirtualFile) + + fun undoDone(file: VirtualFile) + + fun redoStart(file: VirtualFile) + + fun redoDone(file: VirtualFile) +} diff --git a/src/main/kotlin/com/vaadin/plugin/copilot/service/CopilotUndoManagerImpl.kt b/src/main/kotlin/com/vaadin/plugin/copilot/service/CopilotUndoManagerImpl.kt new file mode 100644 index 0000000..3d696f5 --- /dev/null +++ b/src/main/kotlin/com/vaadin/plugin/copilot/service/CopilotUndoManagerImpl.kt @@ -0,0 +1,112 @@ +package com.vaadin.plugin.copilot.service + +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service +import com.intellij.openapi.project.Project +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.VFileEvent +import java.util.Stack +import java.util.concurrent.locks.ReentrantLock + +@Service(Service.Level.PROJECT) +class CopilotUndoManagerImpl(val project: Project) : CopilotUndoManager, Disposable { + + companion object { + const val ACTIONS_ON_SAVE_WINDOW = 1000 // ms window for actions on save + } + + class Batch() { + private val time = System.currentTimeMillis() + private var count = 0 + + fun getCount(): Int { + return count + } + + fun increment() { + count += 1 + } + + fun isInProgress(): Boolean { + return System.currentTimeMillis() - time <= ACTIONS_ON_SAVE_WINDOW + } + } + + private val undoStack: MutableMap> = mutableMapOf() + private val redoStack: MutableMap> = mutableMapOf() + private val locks: MutableMap = mutableMapOf() + + // increments latest batch for file if is current batch + // locking prevents modifying stack during undo / redo + private val bulkFileListener = + object : BulkFileListener { + override fun after(events: MutableList) { + events + .filter { ev -> ev.isFromSave } + .filter { ev -> locks[ev.path] == null || !locks[ev.path]!!.isLocked } + .forEach { + val stack = undoStack[it.path] + if (stack != null) { + if (stack.peek().isInProgress()) { + stack.peek().increment() + } else { + undoStack.remove(it.path) + } + } + } + } + } + + init { + project.messageBus.connect(this).subscribe(VirtualFileManager.VFS_CHANGES, bulkFileListener) + } + + override fun fileWritten(file: VirtualFile) { + undoStack.getOrPut(file.path) { Stack() }.push(Batch()) + } + + override fun getUndoCount(file: VirtualFile): Int { + return undoStack[file.path]?.peek()?.getCount() ?: 0 + } + + override fun getRedoCount(file: VirtualFile): Int { + return redoStack[file.path]?.peek()?.getCount() ?: 0 + } + + override fun undoStart(file: VirtualFile) { + locks.getOrPut(file.path) { ReentrantLock() }.lock() + } + + override fun redoStart(file: VirtualFile) { + locks.getOrPut(file.path) { ReentrantLock() }.lock() + } + + override fun undoDone(file: VirtualFile) { + popAndPush(file, undoStack, redoStack) + locks[file.path]?.unlock() + } + + override fun redoDone(file: VirtualFile) { + popAndPush(file, redoStack, undoStack) + locks[file.path]?.unlock() + } + + private fun popAndPush( + file: VirtualFile, + fromStacksMap: MutableMap>, + targetStacksMap: MutableMap> + ) { + val batch = fromStacksMap[file.path]?.pop() + targetStacksMap.getOrPut(file.path) { Stack() }.push(batch) + if (fromStacksMap[file.path]?.isEmpty() == true) { + fromStacksMap.remove(file.path) + } + } + + override fun dispose() { + undoStack.clear() + redoStack.clear() + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 37f7efc..c007ace 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -47,6 +47,9 @@ + diff --git a/src/test/kotlin/com/vaadin/plugin/copilot/service/CopilotUndoManagerTest.kt b/src/test/kotlin/com/vaadin/plugin/copilot/service/CopilotUndoManagerTest.kt new file mode 100644 index 0000000..32cc45e --- /dev/null +++ b/src/test/kotlin/com/vaadin/plugin/copilot/service/CopilotUndoManagerTest.kt @@ -0,0 +1,195 @@ +package com.vaadin.plugin.copilot.service + +import ai.grazie.utils.mpp.UUID +import com.intellij.openapi.application.runWriteActionAndWait +import com.intellij.openapi.command.CommandProcessor +import com.intellij.openapi.command.UndoConfirmationPolicy +import com.intellij.openapi.editor.Document +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.findDocument +import com.intellij.psi.PsiDocumentManager +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.testFramework.runInEdtAndWait +import com.vaadin.plugin.copilot.handler.RedoHandler +import com.vaadin.plugin.copilot.handler.UndoHandler +import com.vaadin.plugin.copilot.handler.WriteFileHandler +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class CopilotUndoManagerTest : BasePlatformTestCase() { + + private lateinit var tempFile: File + + private lateinit var undoManager: CopilotUndoManager + + @BeforeEach + fun setup() { + super.setUp() + tempFile = File("${project.basePath}/${UUID.random().text}.tmp") + tempFile.deleteOnExit() + Files.createDirectories(Path.of(project.basePath)) + undoManager = project.getService(CopilotUndoManager::class.java) + } + + @AfterEach + fun teardown() { + super.tearDown() + } + + @Test + fun test_newFile_createUndoRedo() { + callFileWriteHandler(tempFile.path, "Some changes") + + var vfsFile = VfsUtil.findFileByIoFile(tempFile, false) + assertTrue(vfsFile?.exists() == true) + + assertEquals(1, undoManager.getUndoCount(vfsFile!!)) + + // simulate save action + writeFile(vfsFile, "Updated Content", "Format On Save") + assertEquals(2, undoManager.getUndoCount(vfsFile)) + assertEquals(0, undoManager.getRedoCount(vfsFile)) + + // undo should remove file + callUndoHandler(tempFile.path) + assertEquals(0, undoManager.getUndoCount(vfsFile)) + assertEquals(2, undoManager.getRedoCount(vfsFile)) + vfsFile = VfsUtil.findFileByIoFile(tempFile, false) + assertTrue(vfsFile == null) + + // redo should recreate it + callRedoHandler(tempFile.path) + + vfsFile = VfsUtil.findFileByIoFile(tempFile, false) + assertTrue(vfsFile?.exists() == true) + assertEquals(2, undoManager.getUndoCount(vfsFile!!)) + assertEquals(0, undoManager.getRedoCount(vfsFile)) + } + + @Test + fun test_existingFile_writeUndoRedo() { + tempFile.createNewFile() + Files.writeString(tempFile.toPath(), "Original Content") + + val vfsFile = VfsUtil.findFileByIoFile(tempFile, false)!! + assertTrue(vfsFile.exists()) + + callFileWriteHandler(vfsFile.path, "Some changes") + + assertEquals(1, undoManager.getUndoCount(vfsFile)) + + // simulate save action + writeFile(vfsFile, "Updated Content", "Format On Save") + assertEquals(2, undoManager.getUndoCount(vfsFile)) + assertEquals(0, undoManager.getRedoCount(vfsFile)) + + callUndoHandler(vfsFile.path) + assertEquals(0, undoManager.getUndoCount(vfsFile)) + assertEquals(2, undoManager.getRedoCount(vfsFile)) + + callRedoHandler(vfsFile.path) + assertEquals(2, undoManager.getUndoCount(vfsFile)) + assertEquals(0, undoManager.getRedoCount(vfsFile)) + + runWriteActionAndWait { vfsFile.delete(this) } + } + + @Test + fun testExistingFile_multipleUndo() { + tempFile.createNewFile() + Files.writeString(tempFile.toPath(), "Original Content") + + val vfsFile = VfsUtil.findFileByIoFile(tempFile, false)!! + assertTrue(vfsFile.exists()) + + // write and format first time + callFileWriteHandler(vfsFile.path, "Change one") + writeFile(vfsFile, "Change one", "Format On Save") + + // idle time + Thread.sleep(1000) + + // write and format second time + callFileWriteHandler(vfsFile.path, "Change two") + writeFile(vfsFile, "Change two", "Format On Save") + + // idle time + Thread.sleep(1000) + + // write and format third time + callFileWriteHandler(vfsFile.path, "Change three") + writeFile(vfsFile, "Change three", "Format On Save") + + // 3 Copilot consecutive operations -> all should be possible to undo + + // call undo first time + callUndoHandler(vfsFile.path) + assertEquals(2, undoManager.getUndoCount(vfsFile)) + assertEquals(2, undoManager.getRedoCount(vfsFile)) + + // call undo second time + callUndoHandler(vfsFile.path) + assertEquals(2, undoManager.getUndoCount(vfsFile)) + assertEquals(2, undoManager.getRedoCount(vfsFile)) + + // call undo third time + callUndoHandler(vfsFile.path) + assertEquals(0, undoManager.getUndoCount(vfsFile)) + assertEquals(2, undoManager.getRedoCount(vfsFile)) + } + + private fun callFileWriteHandler(file: String, text: String) { + val data = mapOf("content" to text, "undoLabel" to "Vaadin File Write", "file" to file) + runInEdtAndWait { + val response = WriteFileHandler(project, data).run() + assertEquals(200, response.status.code()) + } + } + + private fun callUndoHandler(file: String) { + val data = mapOf("files" to listOf(file)) + runInEdtAndWait { + val response = UndoHandler(project, data).run() + assertEquals(200, response.status.code()) + } + } + + private fun callRedoHandler(file: String) { + val data = mapOf("files" to listOf(file)) + runInEdtAndWait { + val response = RedoHandler(project, data).run() + assertEquals(200, response.status.code()) + } + } + + private fun commitAndFlush(doc: Document) { + PsiDocumentManager.getInstance(project).commitDocument(doc) + FileDocumentManager.getInstance().saveDocuments(doc::equals) + } + + private fun writeFile(vfsFile: VirtualFile, content: String, undoLabel: String = "Test Write") { + runInEdtAndWait { + CommandProcessor.getInstance() + .executeCommand( + project, + { + runWriteActionAndWait { + vfsFile.findDocument()!!.let { doc -> + doc.setText(content) + commitAndFlush(doc) + } + } + }, + undoLabel, + null, + UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION, + ) + } + } +}