diff --git a/build.gradle.kts b/build.gradle.kts index b3080f0..9054d19 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.intellij.platform.gradle.TestFrameworkType + plugins { id("java") // Do not upgrade until following has been fixed: @@ -21,10 +23,17 @@ dependencies { intellijPlatform { intellijIdeaCommunity("2024.3.1.1") bundledPlugin("org.jetbrains.plugins.terminal") + + testFramework(TestFrameworkType.Platform) } implementation("org.eclipse.lsp4j:org.eclipse.lsp4j:0.23.1") implementation("org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:0.23.1") + + testImplementation("junit:junit:4.13.2") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.14.1") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.14.1") + testRuntimeOnly("org.junit.vintage:junit-vintage-engine") } kotlin { @@ -38,13 +47,9 @@ tasks { sinceBuild.set("243") } - signPlugin { - certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) - privateKey.set(System.getenv("PRIVATE_KEY")) - password.set(System.getenv("PRIVATE_KEY_PASSWORD")) - } - - publishPlugin { - token.set(System.getenv("PUBLISH_TOKEN")) + test { + useJUnitPlatform { + includeEngines("junit-vintage") + } } } diff --git a/src/main/kotlin/io/github/ethersync/EthersyncServiceImpl.kt b/src/main/kotlin/io/github/ethersync/EthersyncServiceImpl.kt index 659e0d3..5a3c30c 100644 --- a/src/main/kotlin/io/github/ethersync/EthersyncServiceImpl.kt +++ b/src/main/kotlin/io/github/ethersync/EthersyncServiceImpl.kt @@ -4,11 +4,11 @@ import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.process.ColoredProcessHandler import com.intellij.execution.process.ProcessEvent import com.intellij.execution.process.ProcessListener -import com.intellij.openapi.application.EDT import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.editor.EditorFactory -import com.intellij.openapi.editor.event.* +import com.intellij.openapi.editor.event.EditorFactoryEvent +import com.intellij.openapi.editor.event.EditorFactoryListener import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.fileEditor.TextEditor @@ -17,24 +17,21 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.project.ProjectManagerListener import com.intellij.openapi.vfs.VirtualFile -import com.intellij.openapi.wm.ToolWindowManager +import com.intellij.util.io.BaseOutputReader import com.intellij.util.io.await import com.intellij.util.io.awaitExit import com.intellij.util.io.readLineAsync -import com.intellij.util.io.BaseOutputReader import io.github.ethersync.protocol.* import io.github.ethersync.settings.AppSettings import io.github.ethersync.sync.Changetracker import io.github.ethersync.sync.Cursortracker -import io.github.ethersync.ui.ToolWindow import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.eclipse.lsp4j.jsonrpc.Launcher import org.eclipse.lsp4j.jsonrpc.ResponseErrorException import java.io.BufferedReader import java.io.File +import java.io.IOException import java.io.InputStreamReader import java.nio.file.Files import java.nio.file.attribute.PosixFilePermissions @@ -46,7 +43,7 @@ private val LOG = logger() class EthersyncServiceImpl( private val project: Project, private val cs: CoroutineScope, -) : EthersyncService { +) : EthersyncService { private var launcher: Launcher? = null private var daemonProcess: ColoredProcessHandler? = null @@ -83,10 +80,10 @@ class EthersyncServiceImpl( EditorFactory.getInstance().addEditorFactoryListener(object : EditorFactoryListener { override fun editorCreated(event: EditorFactoryEvent) { - val file = event.editor.virtualFile ?: return - if (!file.exists()) { - return - } +// val file = event.editor.virtualFile ?: return +// if (!file.exists()) { +// return +// } event.editor.caretModel.addCaretListener(cursortracker) event.editor.document.addDocumentListener(changetracker) @@ -103,7 +100,7 @@ class EthersyncServiceImpl( } }, project) - ProjectManager.getInstance().addProjectManagerListener(project, object: ProjectManagerListener { + ProjectManager.getInstance().addProjectManagerListener(project, object : ProjectManagerListener { override fun projectClosingBeforeSave(project: Project) { shutdown() } @@ -137,8 +134,7 @@ class EthersyncServiceImpl( if (joinCode == null || joinCode.trim().isEmpty()) { cmd.addParameter("share") - } - else { + } else { cmd.addParameter("join") cmd.addParameter(joinCode.trim()) } @@ -155,41 +151,42 @@ class EthersyncServiceImpl( private fun launchDaemon(cmd: GeneralCommandLine) { val projectDirectory = File(project.basePath!!) - val ethersyncDirectory = File(projectDirectory, ".ethersync") + val ethersyncDirectory = File(projectDirectory, ".teamtype") cmd.workDirectory = projectDirectory cs.launch { shutdownImpl() if (!ethersyncDirectory.exists()) { - LOG.debug("Creating ethersync directory") + LOG.debug("Creating teamtype directory") val permissions = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")); Files.createDirectory(ethersyncDirectory.toPath(), permissions); } - withContext(Dispatchers.EDT) { - daemonProcess = object : ColoredProcessHandler(cmd) { - override fun readerOptions(): BaseOutputReader.Options { - return BaseOutputReader.Options.forMostlySilentProcess() - } + daemonProcess = object : ColoredProcessHandler(cmd) { + override fun readerOptions(): BaseOutputReader.Options { + return BaseOutputReader.Options.forMostlySilentProcess() } + } - daemonProcess!!.addProcessListener(object : ProcessListener { - override fun startNotified(event: ProcessEvent) { - cs.launch { - val ethersyncSocket = File(ethersyncDirectory, "socket").toPath() - while (!Files.exists(ethersyncSocket)) { - Thread.sleep(100) - } - launchEthersyncClient(projectDirectory) + daemonProcess!!.addProcessListener(object : ProcessListener { + override fun startNotified(event: ProcessEvent) { + cs.launch { + val ethersyncSocket = File(ethersyncDirectory, "socket").toPath() + while (!Files.exists(ethersyncSocket)) { + Thread.sleep(100) } + launchEthersyncClient(projectDirectory) } + } - override fun processTerminated(event: ProcessEvent) { - shutdown() - } - }) + override fun processTerminated(event: ProcessEvent) { + shutdown() + } + }) + /* + withContext(Dispatchers.EDT) { val tw = ToolWindowManager.getInstance(project).getToolWindow("ethersync")!! val toolWindow = tw.contentManager.findContent("Daemon")!!.component if (toolWindow is ToolWindow) { @@ -197,10 +194,10 @@ class EthersyncServiceImpl( } tw.show() - - daemonProcess!!.startNotify() } + */ + daemonProcess!!.startNotify() } } @@ -224,22 +221,22 @@ class EthersyncServiceImpl( } cs.launch { - LOG.info("Starting ethersync client") + LOG.info("Starting teamtype client") // TODO: try catch not existing binary val clientProcessBuilder = ProcessBuilder(AppSettings.getInstance().state.ethersyncBinaryPath, "client") - .directory(projectDirectory) + .directory(projectDirectory) clientProcess = clientProcessBuilder.start() val clientProcess = clientProcess!! val ethersyncEditorProtocol = createProtocolHandler() launcher = Launcher.createIoLauncher( - ethersyncEditorProtocol, - RemoteEthersyncClientProtocol::class.java, - clientProcess.inputStream, - clientProcess.outputStream, - Executors.newCachedThreadPool(), - { c -> c }, - { _ -> run {} } + ethersyncEditorProtocol, + RemoteEthersyncClientProtocol::class.java, + clientProcess.inputStream, + clientProcess.outputStream, + Executors.newCachedThreadPool(), + { c -> c }, + { _ -> run {} } ) val listening = launcher!!.startListening() @@ -261,8 +258,13 @@ class EthersyncServiceImpl( val stderr = BufferedReader(InputStreamReader(clientProcess.errorStream)) stderr.use { while (true) { - val line = stderr.readLineAsync() ?: break; - LOG.trace(line) + try { + val line = stderr.readLineAsync() ?: break; + LOG.trace(line) + } catch (e: IOException) { + LOG.trace(e) + break + } } } } diff --git a/src/main/kotlin/io/github/ethersync/settings/AppSettings.kt b/src/main/kotlin/io/github/ethersync/settings/AppSettings.kt index da4c4f1..6fc2f9d 100644 --- a/src/main/kotlin/io/github/ethersync/settings/AppSettings.kt +++ b/src/main/kotlin/io/github/ethersync/settings/AppSettings.kt @@ -14,7 +14,7 @@ class AppSettings : PersistentStateComponent { data class State( @NonNls - var ethersyncBinaryPath: String = "ethersync" + var ethersyncBinaryPath: String = "teamtype" ) private var state: State = State() diff --git a/src/main/kotlin/io/github/ethersync/sync/Cursortracker.kt b/src/main/kotlin/io/github/ethersync/sync/Cursortracker.kt index 6ddfbdd..e8891d5 100644 --- a/src/main/kotlin/io/github/ethersync/sync/Cursortracker.kt +++ b/src/main/kotlin/io/github/ethersync/sync/Cursortracker.kt @@ -1,5 +1,6 @@ package io.github.ethersync.sync +import com.intellij.openapi.application.EDT import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.LogicalPosition import com.intellij.openapi.editor.colors.EditorFontType @@ -16,7 +17,9 @@ import io.github.ethersync.protocol.CursorEvent import io.github.ethersync.protocol.CursorRequest import io.github.ethersync.protocol.RemoteEthersyncClientProtocol import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.eclipse.lsp4j.Position import org.eclipse.lsp4j.Range import org.eclipse.lsp4j.jsonrpc.ResponseErrorException @@ -132,7 +135,12 @@ class Cursortracker( suspend fun clear() { remoteProxy = null - withUiContext { + synchronized(highlighter) { + if (highlighter.isEmpty()) { + return + } + } + withContext(Dispatchers.EDT) { synchronized(highlighter) { for (entry in highlighter) { val fileEditor = FileEditorManager.getInstance(project) diff --git a/src/test/kotlin/io/github/ethersync/EthersyncServiceImplTest.kt b/src/test/kotlin/io/github/ethersync/EthersyncServiceImplTest.kt new file mode 100644 index 0000000..aaac2c2 --- /dev/null +++ b/src/test/kotlin/io/github/ethersync/EthersyncServiceImplTest.kt @@ -0,0 +1,104 @@ +package io.github.ethersync + +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.components.service +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.TextEditor +import com.intellij.openapi.vfs.findOrCreateFile +import com.intellij.testFramework.HeavyPlatformTestCase +import com.intellij.testFramework.runInEdtAndWait +import com.intellij.testFramework.utils.vfs.createFile +import com.intellij.util.application +import java.io.BufferedReader +import java.io.InputStreamReader +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.attribute.PosixFilePermissions +import java.util.Optional +import kotlin.io.path.Path + +class EthersyncServiceImplTest : HeavyPlatformTestCase() { + + var daemon: Process? = null + var joinCode: String? = null + var daemonDir: Path? = null + + override fun setUp() { + super.setUp() + + daemonDir = Files.createTempDirectory("remote-project") + val permissions = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")); + Files.createDirectory(Path(daemonDir!!.toString(), ".teamtype"), permissions); + + daemon = ProcessBuilder() + .command("teamtype", "share") + .directory(daemonDir!!.toFile()) + .start() + + val reader = BufferedReader(InputStreamReader(daemon!!.inputStream)) + reader.use { + var line: String? = null + do { + line = reader.readLine() + + if(line != null) { + line = line.trim() + if (line.startsWith("teamtype join ")) { + joinCode = line.substring(14) + break + } + } + } while (line != null) + } + } + + fun testIntention() { + val dir = orCreateProjectBaseDir + + val service = project.service() + runInEdtAndWait { + service.start(joinCode) + } + + Thread.sleep(5_000) + + + application.runWriteAction { + dir.createFile("file.txt") + } + + runInEdtAndWait { + val file = dir.findOrCreateFile("file.txt") + FileEditorManager.getInstance(project).openFile(file) + val editor = FileEditorManager.getInstance(project) + .allEditors + .filterIsInstance() + .firstOrNull { editor -> editor.file == file }!! + + Thread.sleep(5_000) + + val document = editor.editor.document + WriteCommandAction.runWriteCommandAction(project, { + document.insertString(0, "Hello") + }) + + Thread.sleep(5_000) + } + + val firstLine = Files.lines(Path(daemonDir!!.toString(), "file.txt")) + .findFirst() + assertEquals(Optional.of("Hello"), firstLine) + + runInEdtAndWait { + service.shutdown() + } + } + + override fun tearDown() { + daemon!!.destroy() + daemon!!.waitFor() + daemon = null + daemonDir = null + joinCode = null + } +}