diff --git a/gradle.properties b/gradle.properties index 60956f80..e606893b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,7 +26,7 @@ intellijPluginVersion=1.1.2 javassistVersion=3.27.0-GA kotlinVersion=1.5.20 mockitoKotlinVersion=3.2.0 -projectorClientVersion=042060cb +projectorClientVersion=6ceeb326 projectorClientGroup=com.github.JetBrains.projector-client targetJvm=11 # Give JitPack some time to build projector-client: diff --git a/projector-server/src/main/kotlin/org/jetbrains/projector/server/ProjectorServer.kt b/projector-server/src/main/kotlin/org/jetbrains/projector/server/ProjectorServer.kt index fb140504..3082d093 100644 --- a/projector-server/src/main/kotlin/org/jetbrains/projector/server/ProjectorServer.kt +++ b/projector-server/src/main/kotlin/org/jetbrains/projector/server/ProjectorServer.kt @@ -25,6 +25,10 @@ package org.jetbrains.projector.server +import com.intellij.openapi.application.invokeAndWaitIfNeeded +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.command.executeCommand +import com.intellij.openapi.editor.EditorFactory import org.jetbrains.projector.awt.PClipboard import org.jetbrains.projector.awt.PToolkit import org.jetbrains.projector.awt.PWindow @@ -111,6 +115,8 @@ class ProjectorServer private constructor( private val markdownQueue = ConcurrentLinkedQueue() + private val speculativeQueue = ConcurrentLinkedQueue>() + private var windowColorsEvent: ServerWindowColorsEvent? = null private val ideaColors = IdeColors { colors -> @@ -255,7 +261,7 @@ class ProjectorServer private constructor( private fun isClipboardChanged(current: ServerClipboardEvent?) = lastClipboardEvent != current @OptIn(ExperimentalStdlibApi::class) - private fun createDataToSend(): List { + private fun createDataToSend(): List> { val clipboardEvent = when (isAgent) { false -> PClipboard.extractLastContents()?.toServerClipboardEvent().let(::listOfNotNull) true -> { @@ -313,17 +319,26 @@ class ProjectorServer private constructor( val markdownEvents = extractData(markdownQueue) - val commandsCount = caretInfoEvents.size + - newImagesCopy.size + clipboardEvent.size + drawCommands.size + windowSetChangedEvent.size + markdownEvents.size + 1 + val speculativeEvents = extractData(speculativeQueue).map { + FilterableEvent(it.first) { _, settings -> it.second == settings.address } + } + + val commandsCount = caretInfoEvents.size + newImagesCopy.size + clipboardEvent.size + drawCommands.size + + windowSetChangedEvent.size + markdownEvents.size + speculativeEvents.size + 1 + + fun toFilterableEvent(event: ServerEvent): FilterableEvent<*> { + return FilterableEvent(event) { _, _ -> true } + } val allEvents = buildList(commandsCount) { - addAll(caretInfoEvents) - addAll(newImagesCopy) - addAll(clipboardEvent) - addAll(drawCommands) - addAll(windowSetChangedEvent) - addAll(markdownEvents) - windowColorsEvent?.let { add(it); windowColorsEvent = null } + addAll(caretInfoEvents.map(::toFilterableEvent)) + addAll(newImagesCopy.map(::toFilterableEvent)) + addAll(clipboardEvent.map(::toFilterableEvent)) + addAll(drawCommands.map(::toFilterableEvent)) + addAll(windowSetChangedEvent.map(::toFilterableEvent)) + addAll(markdownEvents.map(::toFilterableEvent)) + addAll(speculativeEvents) + windowColorsEvent?.let { add(toFilterableEvent(it)); windowColorsEvent = null } } ProjectorImageCacher.collectGarbage() @@ -484,6 +499,46 @@ class ProjectorServer private constructor( is ClientWindowCloseEvent -> SwingUtilities.invokeLater { PWindow.getWindow(message.windowId)?.close() } is ClientWindowInterestEvent -> SwingUtilities.invokeLater { clientSettings.interestManager.processClientEvent(message) } + + is ClientSpeculativeKeyPressEvent -> { + + val editor = EditorFactory.getInstance().allEditors.find { + System.identityHashCode(it) == message.editorId + } + + if (editor == null) { + processMessage(clientSettings, message.originalEvent) // fallback + } else { + + invokeAndWaitIfNeeded { + runWriteAction { + executeCommand { + val insertedString = message.originalEvent.char.toString() + + val selectionInfo = message.selectionInfo + + editor.document.apply { + if (selectionInfo != null) { + replaceString(selectionInfo.startOffset, selectionInfo.endOffset, insertedString) + } + else { + insertString(message.offset, insertedString) + } + } + + val newOffset = (selectionInfo?.startOffset ?: message.offset) + insertedString.length + + editor.caretModel.primaryCaret.apply { + removeSelection() + moveToOffset(newOffset) + } + } + } + } + } + + speculativeQueue.add(SpeculativeEvent.SpeculativeStringDrawnEvent(message.requestId) to clientSettings.address!!) + } } } @@ -628,14 +683,19 @@ class ProjectorServer private constructor( clientEventHandler.updateClientsCount() } - private fun sendPictures(dataToSend: List) { + private fun sendPictures(dataToSend: List>) { transports.forEach { transport -> transport.forEachOpenedConnection { client -> val readyClientSettings = client.settings as? ReadyClientSettings ?: return@forEachOpenedConnection val compressed = with(readyClientSettings.setUpClientData) { val requestedData = extractData(readyClientSettings.requestedData) - val message = readyClientSettings.interestManager.filterEvents(requestedData.asSequence() + dataToSend.asSequence()).toList() + val message = readyClientSettings.interestManager.filterEvents(requestedData.asSequence() + dataToSend.mapNotNull { + when (it.isValidForClient(readyClientSettings)) { + true -> it.originalEvent + false -> null + } + }.asSequence()).toList() if (message.isEmpty()) { return@forEachOpenedConnection @@ -896,4 +956,8 @@ class ProjectorServer private constructor( fun getEnvPort() = (getProperty(PORT_PROPERTY_NAME) ?: getProperty(PORT_PROPERTY_NAME_OLD))?.toIntOrNull() ?: DEFAULT_PORT } + + class FilterableEvent(val originalEvent: T, private val filter: (T, ReadyClientSettings) -> Boolean) { + fun isValidForClient(clientSettings: ReadyClientSettings) = filter(originalEvent, clientSettings) + } } diff --git a/projector-server/src/main/kotlin/org/jetbrains/projector/server/idea/CaretInfoUpdater.kt b/projector-server/src/main/kotlin/org/jetbrains/projector/server/idea/CaretInfoUpdater.kt index 7a20ad20..cf755ec3 100644 --- a/projector-server/src/main/kotlin/org/jetbrains/projector/server/idea/CaretInfoUpdater.kt +++ b/projector-server/src/main/kotlin/org/jetbrains/projector/server/idea/CaretInfoUpdater.kt @@ -28,6 +28,7 @@ package org.jetbrains.projector.server.idea import com.intellij.ide.DataManager import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.application.invokeAndWaitIfNeeded +import com.intellij.openapi.editor.colors.EditorColors import com.intellij.openapi.editor.colors.EditorFontType import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.editor.impl.EditorImpl @@ -38,6 +39,7 @@ import org.jetbrains.projector.common.protocol.data.CommonRectangle import org.jetbrains.projector.common.protocol.data.Point import org.jetbrains.projector.common.protocol.toClient.ServerCaretInfoChangedEvent import org.jetbrains.projector.common.protocol.toClient.data.idea.CaretInfo +import org.jetbrains.projector.common.protocol.toClient.data.idea.SelectionInfo import org.jetbrains.projector.server.core.ij.invokeWhenIdeaIsInitialized import org.jetbrains.projector.server.platform.getTextAttributesCompat import org.jetbrains.projector.server.platform.readAction @@ -122,12 +124,41 @@ class CaretInfoUpdater(private val onCaretInfoChanged: (ServerCaretInfoChangedEv val points = focusedEditor.caretModel.allCarets.map { val caretLocationInEditor = invokeAndWaitIfNeeded { it.editor.visualPositionToXY(it.visualPosition) } + val caretOffset = readAction { it.offset } + val selectionStart = readAction { it.selectionStart } + val selectionEnd = readAction { it.selectionEnd } + + val selectionInfo = if (selectionStart == selectionEnd) { + null + } else { + val selectionStartPointVisual = invokeAndWaitIfNeeded { it.editor.visualPositionToXY(it.selectionStartPosition) } + + val selectionStartPoint = Point( + x = (editorLocationInWindowX + selectionStartPointVisual.x).toDouble(), + y = (editorLocationInWindowY + selectionStartPointVisual.y).toDouble(), + ) + + val selectionEndPointVisual = invokeAndWaitIfNeeded { it.editor.visualPositionToXY(it.selectionEndPosition) } + + val selectionEndPoint = Point( + x = (editorLocationInWindowX + selectionEndPointVisual.x).toDouble(), + y = (editorLocationInWindowY + selectionEndPointVisual.y).toDouble(), + ) + + SelectionInfo( + selectionStartPoint, + selectionStart, + selectionEndPoint, + selectionEnd, + ) + } + val point = Point( x = (editorLocationInWindowX + caretLocationInEditor.x).toDouble(), y = (editorLocationInWindowY + caretLocationInEditor.y).toDouble(), ) - CaretInfo(point) + CaretInfo(point, caretOffset, selectionInfo) } val isVerticalScrollBarVisible = visibleEditorRect.height < focusedEditorComponent.height @@ -135,6 +166,7 @@ class CaretInfoUpdater(private val onCaretInfoChanged: (ServerCaretInfoChangedEv val textColor = getTextColorBeforeCaret(focusedEditor) val editorFont = getFontBeforeCaret(focusedEditor) + val backgroundColor = getBackgroundBeforeCaret(focusedEditor) ServerCaretInfoChangedEvent.CaretInfoChange.Carets( points, @@ -151,6 +183,9 @@ class CaretInfoUpdater(private val onCaretInfoChanged: (ServerCaretInfoChangedEv lineAscent = lineAscent, verticalScrollBarWidth = verticalScrollBarWidth, textColor = textColor, + backgroundColor = backgroundColor, + editorScrolled = Point(visibleEditorRect.x.toDouble(), visibleEditorRect.y.toDouble()), + editorId = System.identityHashCode(focusedEditor), ) } } @@ -175,7 +210,23 @@ class CaretInfoUpdater(private val onCaretInfoChanged: (ServerCaretInfoChangedEv return editor.colorsScheme.getFont(editorFontType) } - private fun getTextAttributesBeforeCaret(editor: EditorEx, filter: (TextAttributes) -> Boolean): TextAttributes? { + private fun getBackgroundBeforeCaret(editor: EditorEx): Int { + val attrs = getTextAttributesBeforeCaret(editor, { + if (it.priority >= 0) it.attrs else null + }) { it.backgroundColor != null } + + val color = attrs?.backgroundColor + ?: editor.colorsScheme.getColor(EditorColors.CARET_ROW_COLOR) + ?: editor.colorsScheme.defaultBackground + + return color.rgb + } + + private fun getTextAttributesBeforeCaret( + editor: EditorEx, + mapper: (ExtendedTextAttributes) -> TextAttributes? = { it.attrs }, + filter: (TextAttributes) -> Boolean + ): TextAttributes? { val caretOffset = readAction { editor.caretModel.offset } @@ -194,7 +245,7 @@ class CaretInfoUpdater(private val onCaretInfoChanged: (ServerCaretInfoChangedEv it(editor, caretOffset, compareAndUpdate) } - return bestFitAttributes?.attrs + return bestFitAttributes?.let(mapper) } private fun getAttrsFromRangeHighlighters( @@ -203,7 +254,9 @@ class CaretInfoUpdater(private val onCaretInfoChanged: (ServerCaretInfoChangedEv compareAndUpdate: (ExtendedTextAttributes) -> Unit, ) { - val rangeHighlighters = invokeAndWaitIfNeeded { editor.filteredDocumentMarkupModel.allHighlighters } + val rangeHighlighters = invokeAndWaitIfNeeded { + editor.filteredDocumentMarkupModel.allHighlighters + editor.markupModel.allHighlighters + } val startPos = caretOffset - 1