Skip to content

Commit 480f30c

Browse files
Basic Sync of Cursors
This commit provides some further implementation of the editor protocol: a Neovim client and a Intellij client can sync the cursor positions. However, the implementation is still rudimentary and buggy.
1 parent c468f0b commit 480f30c

File tree

6 files changed

+155
-72
lines changed

6 files changed

+155
-72
lines changed

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

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,44 @@
11
package io.github.ethersync
22

3+
import com.google.gson.JsonObject
4+
import com.intellij.openapi.application.ApplicationManager
5+
import com.intellij.openapi.application.ModalityState
36
import com.intellij.openapi.components.Service
4-
import com.intellij.openapi.components.service
57
import com.intellij.openapi.diagnostic.logger
8+
import com.intellij.openapi.editor.EditorFactory
9+
import com.intellij.openapi.editor.LogicalPosition
10+
import com.intellij.openapi.editor.event.CaretEvent
11+
import com.intellij.openapi.editor.event.CaretListener
12+
import com.intellij.openapi.editor.event.EditorFactoryEvent
13+
import com.intellij.openapi.editor.event.EditorFactoryListener
14+
import com.intellij.openapi.editor.markup.HighlighterLayer
15+
import com.intellij.openapi.editor.markup.HighlighterTargetArea
16+
import com.intellij.openapi.editor.markup.RangeHighlighter
17+
import com.intellij.openapi.editor.markup.TextAttributes
18+
import com.intellij.openapi.fileEditor.FileEditorManager
19+
import com.intellij.openapi.fileEditor.FileEditorManagerListener
20+
import com.intellij.openapi.fileEditor.TextEditor
621
import com.intellij.openapi.project.Project
22+
import com.intellij.openapi.vfs.VirtualFile
23+
import com.intellij.testFramework.utils.editor.getVirtualFile
24+
import com.intellij.ui.JBColor
725
import com.intellij.util.io.awaitExit
826
import com.intellij.util.io.readLineAsync
27+
import io.github.ethersync.protocol.CursorEvent
28+
import io.github.ethersync.protocol.CursorRequest
29+
import io.github.ethersync.protocol.DocumentRequest
930
import io.github.ethersync.protocol.EthersyncEditorProtocol
1031
import kotlinx.coroutines.CoroutineScope
1132
import kotlinx.coroutines.launch
33+
import org.eclipse.lsp4j.Position
34+
import org.eclipse.lsp4j.Range
1235
import org.eclipse.lsp4j.jsonrpc.Launcher
1336
import java.io.BufferedReader
1437
import java.io.File
1538
import java.io.InputStreamReader
39+
import java.util.Collections
40+
import java.util.LinkedList
41+
import java.util.concurrent.CompletableFuture
1642
import java.util.concurrent.Executors
1743

1844
private val LOG = logger<EthersyncServiceImpl>()
@@ -70,15 +96,74 @@ class EthersyncServiceImpl(
7096
}
7197
}
7298

99+
private fun createProtocolHandler(): EthersyncEditorProtocol {
100+
val highlighter = LinkedList<RangeHighlighter>()
101+
102+
return object : EthersyncEditorProtocol {
103+
override fun cursor(cursorEvent: CursorEvent) {
104+
val fileEditorManager = FileEditorManager.getInstance(project)
105+
106+
val fileEditor = fileEditorManager.allEditors
107+
.first { editor -> editor.file.url == cursorEvent.documentUri } ?: return
108+
109+
if (fileEditor is TextEditor) {
110+
val editor = fileEditor.editor
111+
ApplicationManager.getApplication().invokeLater({
112+
synchronized(highlighter) {
113+
val markupModel = editor.markupModel
114+
115+
for (hl in highlighter) {
116+
markupModel.removeHighlighter(hl)
117+
}
118+
highlighter.clear()
119+
120+
for(range in cursorEvent.ranges) {
121+
val startPosition = editor.logicalPositionToOffset(LogicalPosition(range.start.line, range.start.character))
122+
val endPosition = editor.logicalPositionToOffset(LogicalPosition(range.end.line, range.end.character))
123+
124+
val textAttributes = TextAttributes().apply {
125+
backgroundColor = JBColor(JBColor.YELLOW, JBColor.DARK_GRAY)
126+
// TODO: unclear which is the best effect type
127+
// effectType = EffectType.LINE_UNDERSCORE
128+
// effectColor = JBColor(JBColor.YELLOW, JBColor.DARK_GRAY)
129+
}
130+
131+
val hl = markupModel.addRangeHighlighter(
132+
startPosition,
133+
endPosition + 1,
134+
HighlighterLayer.ADDITIONAL_SYNTAX,
135+
textAttributes,
136+
HighlighterTargetArea.EXACT_RANGE
137+
)
138+
139+
highlighter.add(hl)
140+
}
141+
}
142+
}, ModalityState.nonModal())
143+
}
144+
}
145+
146+
override fun open(documentRequest: DocumentRequest): CompletableFuture<JsonObject> {
147+
return CompletableFuture.completedFuture(JsonObject())
148+
}
149+
150+
override fun close(documentRequest: DocumentRequest): CompletableFuture<JsonObject> {
151+
return CompletableFuture.completedFuture(JsonObject())
152+
}
153+
154+
}
155+
}
156+
73157
private fun launchEthersyncClient(socket: String, projectDirectory: File) {
158+
74159
cs.launch {
75160

76161
LOG.info("Starting ethersync client")
77162
val clientProcessBuilder = ProcessBuilder("ethersync", "client", "--socket-name", socket)
78163
.directory(projectDirectory)
79164
val clientProcess = clientProcessBuilder.start()
80165

81-
val ethersyncEditorProtocol = project.service<EthersyncEditorProtocol>()
166+
val ethersyncEditorProtocol = createProtocolHandler()
82167
val launcher = Launcher.createIoLauncher(
83168
ethersyncEditorProtocol,
84169
EthersyncEditorProtocol::class.java,
@@ -89,8 +174,47 @@ class EthersyncServiceImpl(
89174
{ _ -> run {} }
90175
)
91176

177+
val bus = project.messageBus.connect()
178+
bus.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener {
179+
override fun fileOpened(source: FileEditorManager, file: VirtualFile) {
180+
ethersyncEditorProtocol.open(DocumentRequest(file.url))
181+
}
182+
183+
override fun fileClosed(source: FileEditorManager, file: VirtualFile) {
184+
ethersyncEditorProtocol.close(DocumentRequest(file.url))
185+
}
186+
})
187+
188+
val caretListener = object : CaretListener {
189+
override fun caretPositionChanged(event: CaretEvent) {
190+
val uri = event.editor.virtualFile.url
191+
val pos = Position(event.newPosition.line, event.newPosition.column)
192+
val range = Range(pos, pos)
193+
launcher.remoteEndpoint.notify("cursor", CursorRequest(uri, Collections.singletonList(range)))
194+
}
195+
}
196+
197+
EditorFactory.getInstance().addEditorFactoryListener(object : EditorFactoryListener {
198+
override fun editorCreated(event: EditorFactoryEvent) {
199+
event.editor.caretModel.addCaretListener(caretListener)
200+
}
201+
202+
override fun editorReleased(event: EditorFactoryEvent) {
203+
event.editor.caretModel.removeCaretListener(caretListener)
204+
}
205+
}, project)
206+
207+
92208
val listening = launcher.startListening()
93209

210+
cs.launch {
211+
val fileEditorManager = FileEditorManager.getInstance(project)
212+
213+
for (file in fileEditorManager.openFiles) {
214+
ethersyncEditorProtocol.open(DocumentRequest(file.url))
215+
}
216+
}
217+
94218
clientProcess.awaitExit()
95219

96220
listening.cancel(true)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.github.ethersync.protocol
2+
3+
import com.google.gson.annotations.SerializedName
4+
import org.eclipse.lsp4j.Range
5+
6+
data class CursorRequest(
7+
@SerializedName("uri")
8+
val uri: String,
9+
@SerializedName("ranges")
10+
val ranges: List<Range>
11+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package io.github.ethersync.protocol
2+
3+
import com.google.gson.annotations.SerializedName
4+
5+
data class DocumentRequest(
6+
@SerializedName("uri")
7+
val documentUri: String
8+
)
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
package io.github.ethersync.protocol
22

3+
import com.google.gson.JsonObject
34
import org.eclipse.lsp4j.jsonrpc.services.JsonNotification
5+
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest
6+
import java.util.concurrent.CompletableFuture
47

58
interface EthersyncEditorProtocol {
6-
@JsonNotification
9+
@JsonNotification("cursor")
710
fun cursor(cursorEvent: CursorEvent)
11+
12+
@JsonRequest
13+
fun open(documentRequest: DocumentRequest): CompletableFuture<JsonObject>
14+
15+
@JsonRequest
16+
fun close(documentRequest: DocumentRequest): CompletableFuture<JsonObject>
817
}

src/main/kotlin/io/github/ethersync/protocol/EthersyncEditorProtocolImpl.kt

Lines changed: 0 additions & 65 deletions
This file was deleted.

src/main/resources/META-INF/plugin.xml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@
1515
serviceInterface="io.github.ethersync.EthersyncService"
1616
serviceImplementation="io.github.ethersync.EthersyncServiceImpl"/>
1717

18-
19-
<projectService
20-
serviceInterface="io.github.ethersync.protocol.EthersyncEditorProtocol"
21-
serviceImplementation="io.github.ethersync.protocol.EthersyncEditorProtocolImpl"/>
2218
<toolWindow id="Ethersync Daemon Output" factoryClass="io.github.ethersync.DaemonToolWindowFactory" />
2319
</extensions>
2420

0 commit comments

Comments
 (0)