Skip to content

Commit e2eadf7

Browse files
Teamtype v0.9.x Support
This change ports the code to support Teamtype 0.9.x (fixes #12). Additionally, this change provides the first and rudimentary test infrastructure to make sure that the plugin works as intended.
1 parent 8dec1e2 commit e2eadf7

9 files changed

Lines changed: 365 additions & 121 deletions

File tree

DEVELOPMENT.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ Make sure you have at least a JDK 21 installed on your machine and that the
88
`./gradlew runIde --no-daemon` directly builds & starts a sandboxed IntelliJ
99
IDEA with the plugin enabled.
1010

11+
Testing is currently in the beginning and there is exists some issue in the
12+
`HeavyPlatfromTestCase` so one has to manually run tests in isolation:
13+
14+
```bash
15+
./gradlew test --info --rerun --tests EthersyncServiceImplTest.test01SyncChangesFromRemoteProjectToJetbrains
16+
./gradlew test --info --rerun --tests EthersyncServiceImplTest.test02SyncChangesFromJetbrainsToRemoteProject
17+
```
18+
1119
## Install into existing IDE
1220

1321
`./gradlew buildPlugin` creates in `build/distributions/` a ZIP archive that

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Currently, the project is in development but the pre-release version of the
1111
plugin can be installed by downloading the latest ZIP from [here][nightly
1212
download] and this ZIP can be installed into the Jetbrains product from disk
1313
(see [install from disk] instructions for IntelliJ). Please note, that this
14-
version currently only supports Ethersync 0.8.
14+
version currently only supports Teamtype 0.9.x.
1515

1616
# Development
1717

build.gradle.kts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
2+
13
plugins {
24
id("java")
35
// Do not upgrade until following has been fixed:
@@ -21,10 +23,17 @@ dependencies {
2123
intellijPlatform {
2224
intellijIdeaCommunity("2024.3.1.1")
2325
bundledPlugin("org.jetbrains.plugins.terminal")
26+
27+
testFramework(TestFrameworkType.Platform)
2428
}
2529

2630
implementation("org.eclipse.lsp4j:org.eclipse.lsp4j:0.23.1")
2731
implementation("org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:0.23.1")
32+
33+
testImplementation("junit:junit:4.13.2")
34+
testImplementation("org.junit.jupiter:junit-jupiter-api:5.14.1")
35+
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.14.1")
36+
testRuntimeOnly("org.junit.vintage:junit-vintage-engine")
2837
}
2938

3039
kotlin {
@@ -38,13 +47,9 @@ tasks {
3847
sinceBuild.set("243")
3948
}
4049

41-
signPlugin {
42-
certificateChain.set(System.getenv("CERTIFICATE_CHAIN"))
43-
privateKey.set(System.getenv("PRIVATE_KEY"))
44-
password.set(System.getenv("PRIVATE_KEY_PASSWORD"))
45-
}
46-
47-
publishPlugin {
48-
token.set(System.getenv("PUBLISH_TOKEN"))
50+
test {
51+
useJUnitPlatform {
52+
includeEngines("junit-vintage")
53+
}
4954
}
5055
}

src/main/kotlin/io/github/ethersync/EthersyncService.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package io.github.ethersync
22

3+
import kotlinx.coroutines.Job
4+
35
interface EthersyncService {
46

5-
fun start(joinCode: String?)
7+
fun start(joinCode: String?): Job
68

79
fun shutdown()
810

src/main/kotlin/io/github/ethersync/EthersyncServiceImpl.kt

Lines changed: 117 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import com.intellij.openapi.application.EDT
88
import com.intellij.openapi.components.Service
99
import com.intellij.openapi.diagnostic.logger
1010
import com.intellij.openapi.editor.EditorFactory
11-
import com.intellij.openapi.editor.event.*
11+
import com.intellij.openapi.editor.event.EditorFactoryEvent
12+
import com.intellij.openapi.editor.event.EditorFactoryListener
1213
import com.intellij.openapi.fileEditor.FileEditorManager
1314
import com.intellij.openapi.fileEditor.FileEditorManagerListener
1415
import com.intellij.openapi.fileEditor.TextEditor
@@ -18,23 +19,24 @@ import com.intellij.openapi.project.ProjectManager
1819
import com.intellij.openapi.project.ProjectManagerListener
1920
import com.intellij.openapi.vfs.VirtualFile
2021
import com.intellij.openapi.wm.ToolWindowManager
22+
import com.intellij.psi.PsiDocumentManager
23+
import com.intellij.util.io.BaseOutputReader
2124
import com.intellij.util.io.await
2225
import com.intellij.util.io.awaitExit
2326
import com.intellij.util.io.readLineAsync
24-
import com.intellij.util.io.BaseOutputReader
2527
import io.github.ethersync.protocol.*
2628
import io.github.ethersync.settings.AppSettings
2729
import io.github.ethersync.sync.Changetracker
2830
import io.github.ethersync.sync.Cursortracker
2931
import io.github.ethersync.ui.ToolWindow
30-
import kotlinx.coroutines.CoroutineScope
31-
import kotlinx.coroutines.Dispatchers
32-
import kotlinx.coroutines.launch
33-
import kotlinx.coroutines.withContext
32+
import kotlinx.coroutines.*
33+
import kotlinx.coroutines.channels.Channel
34+
import kotlinx.coroutines.channels.consumeEach
3435
import org.eclipse.lsp4j.jsonrpc.Launcher
3536
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException
3637
import java.io.BufferedReader
3738
import java.io.File
39+
import java.io.IOException
3840
import java.io.InputStreamReader
3941
import java.nio.file.Files
4042
import java.nio.file.attribute.PosixFilePermissions
@@ -46,7 +48,7 @@ private val LOG = logger<EthersyncServiceImpl>()
4648
class EthersyncServiceImpl(
4749
private val project: Project,
4850
private val cs: CoroutineScope,
49-
) : EthersyncService {
51+
) : EthersyncService {
5052

5153
private var launcher: Launcher<RemoteEthersyncClientProtocol>? = null
5254
private var daemonProcess: ColoredProcessHandler? = null
@@ -55,6 +57,9 @@ class EthersyncServiceImpl(
5557
private val changetracker: Changetracker = Changetracker(project, cs)
5658
private val cursortracker: Cursortracker = Cursortracker(project, cs)
5759

60+
/** test-only! */
61+
var attachDaemonOutputToUi: Boolean = true
62+
5863
init {
5964
val bus = project.messageBus.connect()
6065
bus.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener {
@@ -83,10 +88,9 @@ class EthersyncServiceImpl(
8388

8489
EditorFactory.getInstance().addEditorFactoryListener(object : EditorFactoryListener {
8590
override fun editorCreated(event: EditorFactoryEvent) {
86-
val file = event.editor.virtualFile ?: return
87-
if (!file.exists()) {
88-
return
89-
}
91+
val doc = event.editor.document
92+
val file = PsiDocumentManager.getInstance(project).getPsiFile(doc) ?: return
93+
LOG.debug("Starting to watch changes of ${file}")
9094

9195
event.editor.caretModel.addCaretListener(cursortracker)
9296
event.editor.document.addDocumentListener(changetracker)
@@ -103,7 +107,7 @@ class EthersyncServiceImpl(
103107
}
104108
}, project)
105109

106-
ProjectManager.getInstance().addProjectManagerListener(project, object: ProjectManagerListener {
110+
ProjectManager.getInstance().addProjectManagerListener(project, object : ProjectManagerListener {
107111
override fun projectClosingBeforeSave(project: Project) {
108112
shutdown()
109113
}
@@ -132,75 +136,97 @@ class EthersyncServiceImpl(
132136
cursortracker.clear()
133137
}
134138

135-
override fun start(joinCode: String?) {
139+
override fun start(joinCode: String?): Job {
136140
val cmd = GeneralCommandLine(AppSettings.getInstance().state.ethersyncBinaryPath)
137141

138142
if (joinCode == null || joinCode.trim().isEmpty()) {
139143
cmd.addParameter("share")
140-
}
141-
else {
144+
} else {
142145
cmd.addParameter("join")
143146
cmd.addParameter(joinCode.trim())
144147
}
145148

146-
launchDaemon(cmd)
149+
return cs.launch {
150+
val channel = Channel<Unit>()
151+
launchDaemon(cmd, channel)
152+
channel.consumeEach { msg ->
153+
LOG.debug("Started: $msg")
154+
}
155+
}
147156
}
148157

149158
override fun startWithCustomCommandLine(commandLine: String) {
150159
// TODO: splitting by " " is probably insufficient if there is an argument with spaces in it…
151160
val cmd = GeneralCommandLine(commandLine.split(" "))
152161

153-
launchDaemon(cmd)
162+
cs.launch {
163+
val channel = Channel<Unit>()
164+
launchDaemon(cmd, channel)
165+
channel.consumeEach { msg ->
166+
LOG.debug("Started: $msg")
167+
}
168+
}
154169
}
155170

156-
private fun launchDaemon(cmd: GeneralCommandLine) {
171+
private suspend fun launchDaemon(cmd: GeneralCommandLine, clientStarted: Channel<Unit>) {
157172
val projectDirectory = File(project.basePath!!)
158-
val ethersyncDirectory = File(projectDirectory, ".ethersync")
173+
val ethersyncDirectory = File(projectDirectory, ".teamtype")
159174
cmd.workDirectory = projectDirectory
160175

161-
cs.launch {
162-
shutdownImpl()
176+
shutdownImpl()
177+
178+
if (!ethersyncDirectory.exists()) {
179+
LOG.debug("Creating teamtype directory")
180+
val permissions = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"));
181+
withContext(Dispatchers.IO) {
182+
Files.createDirectory(ethersyncDirectory.toPath(), permissions)
183+
};
184+
}
163185

164-
if (!ethersyncDirectory.exists()) {
165-
LOG.debug("Creating ethersync directory")
166-
val permissions = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"));
167-
Files.createDirectory(ethersyncDirectory.toPath(), permissions);
186+
daemonProcess = object : ColoredProcessHandler(cmd) {
187+
override fun readerOptions(): BaseOutputReader.Options {
188+
return BaseOutputReader.Options.forMostlySilentProcess()
168189
}
190+
}
169191

170-
withContext(Dispatchers.EDT) {
171-
daemonProcess = object : ColoredProcessHandler(cmd) {
172-
override fun readerOptions(): BaseOutputReader.Options {
173-
return BaseOutputReader.Options.forMostlySilentProcess()
192+
daemonProcess!!.addProcessListener(object : ProcessListener {
193+
override fun startNotified(event: ProcessEvent) {
194+
cs.launch {
195+
val ethersyncSocket = File(ethersyncDirectory, "socket").toPath()
196+
while (!Files.exists(ethersyncSocket)) {
197+
Thread.sleep(100)
174198
}
199+
launchEthersyncClient(projectDirectory, clientStarted)
175200
}
201+
}
176202

177-
daemonProcess!!.addProcessListener(object : ProcessListener {
178-
override fun startNotified(event: ProcessEvent) {
179-
cs.launch {
180-
val ethersyncSocket = File(ethersyncDirectory, "socket").toPath()
181-
while (!Files.exists(ethersyncSocket)) {
182-
Thread.sleep(100)
183-
}
184-
launchEthersyncClient(projectDirectory)
185-
}
186-
}
203+
override fun processTerminated(event: ProcessEvent) {
204+
shutdown()
205+
}
206+
})
187207

188-
override fun processTerminated(event: ProcessEvent) {
189-
shutdown()
190-
}
191-
})
192208

193-
val tw = ToolWindowManager.getInstance(project).getToolWindow("ethersync")!!
194-
val toolWindow = tw.contentManager.findContent("Daemon")!!.component
195-
if (toolWindow is ToolWindow) {
196-
toolWindow.attachToProcess(daemonProcess!!)
197-
}
209+
attachDaemonToToolWindow()
198210

199-
tw.show()
211+
daemonProcess!!.startNotify()
212+
}
200213

201-
daemonProcess!!.startNotify()
214+
private suspend fun attachDaemonToToolWindow() {
215+
if (!attachDaemonOutputToUi) {
216+
return
217+
}
218+
val process = daemonProcess ?: return
219+
220+
withContext(Dispatchers.EDT) {
221+
val tw = ToolWindowManager.getInstance(project).getToolWindow("ethersync") ?: return@withContext
222+
223+
val daemon = tw.contentManager.findContent("Daemon") ?: return@withContext
224+
val toolWindow = daemon.component
225+
if (toolWindow is ToolWindow) {
226+
toolWindow.attachToProcess(process)
202227
}
203228

229+
tw.show()
204230
}
205231
}
206232

@@ -218,51 +244,56 @@ class EthersyncServiceImpl(
218244
}
219245
}
220246

221-
private fun launchEthersyncClient(projectDirectory: File) {
247+
private suspend fun launchEthersyncClient(projectDirectory: File, clientStarted: Channel<Unit>) {
222248
if (clientProcess != null) {
223249
return
224250
}
225251

226-
cs.launch {
227-
LOG.info("Starting ethersync client")
228-
// TODO: try catch not existing binary
229-
val clientProcessBuilder = ProcessBuilder(AppSettings.getInstance().state.ethersyncBinaryPath, "client")
230-
.directory(projectDirectory)
231-
clientProcess = clientProcessBuilder.start()
232-
val clientProcess = clientProcess!!
233-
234-
val ethersyncEditorProtocol = createProtocolHandler()
235-
launcher = Launcher.createIoLauncher(
236-
ethersyncEditorProtocol,
237-
RemoteEthersyncClientProtocol::class.java,
238-
clientProcess.inputStream,
239-
clientProcess.outputStream,
240-
Executors.newCachedThreadPool(),
241-
{ c -> c },
242-
{ _ -> run {} }
243-
)
244-
245-
val listening = launcher!!.startListening()
246-
cursortracker.remoteProxy = launcher!!.remoteProxy
247-
changetracker.remoteProxy = launcher!!.remoteProxy
248-
249-
val fileEditorManager = FileEditorManager.getInstance(project)
250-
for (file in fileEditorManager.openFiles) {
251-
val content = LoadTextUtil.loadText(file).toString()
252-
launchDocumentOpenRequest(file.canonicalFile!!.url, content)
253-
}
252+
LOG.info("Starting teamtype client")
253+
// TODO: try catch not existing binary
254+
val clientProcessBuilder = ProcessBuilder(AppSettings.getInstance().state.ethersyncBinaryPath, "client")
255+
.directory(projectDirectory)
256+
clientProcess = clientProcessBuilder.start()
257+
val clientProcess = clientProcess!!
258+
259+
val ethersyncEditorProtocol = createProtocolHandler()
260+
launcher = Launcher.createIoLauncher(
261+
ethersyncEditorProtocol,
262+
RemoteEthersyncClientProtocol::class.java,
263+
clientProcess.inputStream,
264+
clientProcess.outputStream,
265+
Executors.newCachedThreadPool(),
266+
{ c -> c },
267+
{ _ -> run {} }
268+
)
269+
270+
val listening = launcher!!.startListening()
271+
cursortracker.remoteProxy = launcher!!.remoteProxy
272+
changetracker.remoteProxy = launcher!!.remoteProxy
273+
274+
val fileEditorManager = FileEditorManager.getInstance(project)
275+
for (file in fileEditorManager.openFiles) {
276+
val content = LoadTextUtil.loadText(file).toString()
277+
launchDocumentOpenRequest(file.canonicalFile!!.url, content)
278+
}
254279

255-
clientProcess.awaitExit()
280+
clientStarted.send(Unit)
281+
clientStarted.close()
282+
clientProcess.awaitExit()
256283

257-
listening.cancel(true)
258-
listening.await()
284+
listening.cancel(true)
285+
listening.await()
259286

260-
if (clientProcess.exitValue() != 0) {
261-
val stderr = BufferedReader(InputStreamReader(clientProcess.errorStream))
262-
stderr.use {
263-
while (true) {
287+
if (clientProcess.exitValue() != 0) {
288+
val stderr = BufferedReader(InputStreamReader(clientProcess.errorStream))
289+
stderr.use {
290+
while (true) {
291+
try {
264292
val line = stderr.readLineAsync() ?: break;
265293
LOG.trace(line)
294+
} catch (e: IOException) {
295+
LOG.trace(e)
296+
break
266297
}
267298
}
268299
}

src/main/kotlin/io/github/ethersync/settings/AppSettings.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class AppSettings : PersistentStateComponent<AppSettings.State> {
1414

1515
data class State(
1616
@NonNls
17-
var ethersyncBinaryPath: String = "ethersync"
17+
var ethersyncBinaryPath: String = "teamtype"
1818
)
1919

2020
private var state: State = State()

0 commit comments

Comments
 (0)