diff --git a/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/misc/ParamsProvider.kt b/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/misc/ParamsProvider.kt index 76bccd548..59843aa84 100644 --- a/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/misc/ParamsProvider.kt +++ b/projector-client-common/src/jsMain/kotlin/org/jetbrains/projector/client/common/misc/ParamsProvider.kt @@ -65,6 +65,8 @@ actual object ParamsProvider { private const val DEFAULT_REPAINT_INTERVAL_MS = 333 private const val DEFAULT_IMAGE_CACHE_SIZE_CHARS = 5_000_000 private const val DEFAULT_BLOCK_CLOSING = true + private val DEFAULT_TYPING_CLEAR_STRATEGY = TypingClearStrategy.SERVER_VALIDATION + private const val DEFAULT_HIDE_MAIN_CANVAS_ON_SPECULATIVE_TYPING = false val SYSTEM_SCALING_RATIO get() = window.devicePixelRatio // get every time because it can be changed @@ -100,6 +102,8 @@ actual object ParamsProvider { actual val IMAGE_CACHE_SIZE_CHARS: Int val BLOCK_CLOSING: Boolean val LAYOUT_TYPE: LayoutType + val TYPING_CLEAR_STRATEGY: TypingClearStrategy + val HIDE_MAIN_CANVAS_ON_SPECULATIVE_TYPING: Boolean val SCALING_RATIO: Double get() = SYSTEM_SCALING_RATIO * USER_SCALING_RATIO @@ -161,6 +165,14 @@ actual object ParamsProvider { "frAzerty" -> LayoutType.FR_AZERTY else -> LayoutType.JS_DEFAULT } + TYPING_CLEAR_STRATEGY = when (searchParams.get("typingClearStrategy")) { + "server" -> TypingClearStrategy.SERVER_VALIDATION + "position" -> TypingClearStrategy.BY_POSITION + "naive" -> TypingClearStrategy.NAIVE + else -> DEFAULT_TYPING_CLEAR_STRATEGY + } + HIDE_MAIN_CANVAS_ON_SPECULATIVE_TYPING = searchParams.get("hideOnSpeculative")?.toBoolean() + ?: DEFAULT_HIDE_MAIN_CANVAS_ON_SPECULATIVE_TYPING } } @@ -181,4 +193,10 @@ actual object ParamsProvider { JS_DEFAULT, FR_AZERTY, } + + enum class TypingClearStrategy { + NAIVE, + BY_POSITION, + SERVER_VALIDATION, + } } diff --git a/projector-client-web/README.md b/projector-client-web/README.md index 5b6cc6e08..bc808e15f 100644 --- a/projector-client-web/README.md +++ b/projector-client-web/README.md @@ -68,6 +68,8 @@ Name | Type | Default value | Description `cacheSize` | Int | `5M` | Set size of cache for images in Chars. `blockClosing` | Boolean | `true` | Enable blocking of accidental closing of the web page `relayServerId` | String? | Not present | Identifier of Projector server to connect to for relay connection. Warning: Static files must be accessed via https when relay is used. +`typingClearStrategy` | String | `server` | Sets strategy of removing speculative symbols:
`server` - symbol is removed when server sends back validation of its insertion in the text;
`position` - symbols are removed when a new string is painted at the position of those symbols (faster, but may be inaccurate);
`naive` - ALL symbols are removed when any new string is painted (legacy variant). +`hideOnSpeculative` | Boolean | `false` | Hide main window canvas when speculative symbols are typed and not removed yet. ## Shortcuts - `Ctrl + F10` prints statistics to the browser console. Example: diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/ServerEventsProcessor.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/ServerEventsProcessor.kt index b231060df..3f6577a76 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/ServerEventsProcessor.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/ServerEventsProcessor.kt @@ -26,6 +26,7 @@ package org.jetbrains.projector.client.web import kotlinx.browser.window import org.jetbrains.projector.client.common.SingleRenderingSurfaceProcessor.Companion.shrinkByPaintEvents import org.jetbrains.projector.client.common.misc.ImageCacher +import org.jetbrains.projector.client.common.misc.ParamsProvider import org.jetbrains.projector.client.web.component.MarkdownPanelManager import org.jetbrains.projector.client.web.input.InputController import org.jetbrains.projector.client.web.misc.PingStatistics @@ -34,6 +35,7 @@ import org.jetbrains.projector.client.web.state.ProjectorUI import org.jetbrains.projector.client.web.window.OnScreenMessenger import org.jetbrains.projector.client.web.window.WindowDataEventsProcessor import org.jetbrains.projector.common.misc.Do +import org.jetbrains.projector.common.protocol.data.Point import org.jetbrains.projector.common.protocol.toClient.* import org.jetbrains.projector.util.logging.Logger import org.w3c.dom.url.URL @@ -85,17 +87,41 @@ class ServerEventsProcessor(private val windowDataEventsProcessor: WindowDataEve // todo: should WindowManager.lookAndFeelChanged() be called here? OnScreenMessenger.lookAndFeelChanged() } + is SpeculativeEvent -> when (command) { + is SpeculativeEvent.SpeculativeStringDrawnEvent -> { + if (ParamsProvider.TYPING_CLEAR_STRATEGY == ParamsProvider.TypingClearStrategy.SERVER_VALIDATION) { + typing.onSymbolValidated(command.operationId) + } + + Unit + } + } } } - // todo: determine the moment better - if (drawCommandsEvents.any { it.drawEvents.any { drawEvent -> drawEvent is ServerDrawStringEvent } }) { - typing.removeSpeculativeImage() + if (ParamsProvider.TYPING_CLEAR_STRATEGY == ParamsProvider.TypingClearStrategy.NAIVE) { + if (drawCommandsEvents.any { it.drawEvents.any { drawEvent -> drawEvent is ServerDrawStringEvent } }) { + typing.removeSpeculativeImage() + } } drawCommandsEvents.sortWith(drawingOrderComparator) drawCommandsEvents.forEach { event -> + + if (ParamsProvider.TYPING_CLEAR_STRATEGY == ParamsProvider.TypingClearStrategy.BY_POSITION) { + + var verticalOffset = 0.0 + + event.drawEvents.forEach { drawEvent -> + when (drawEvent) { + is ServerSetTransformEvent -> verticalOffset = drawEvent.tx[5] + is ServerDrawStringEvent -> typing.onDrawString(drawEvent, Point(0.0, verticalOffset)) + else -> {} + } + } + } + Do exhaustive when (val target = event.target) { is ServerDrawCommandsEvent.Target.Onscreen -> windowDataEventsProcessor.draw(target.windowId, event.drawEvents) diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/protocol/ManualJsonToClientMessageDecoder.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/protocol/ManualJsonToClientMessageDecoder.kt index 3c6c34a6d..0796448c3 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/protocol/ManualJsonToClientMessageDecoder.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/protocol/ManualJsonToClientMessageDecoder.kt @@ -27,6 +27,7 @@ import org.jetbrains.projector.common.protocol.data.* import org.jetbrains.projector.common.protocol.handshake.ProtocolType import org.jetbrains.projector.common.protocol.toClient.* import org.jetbrains.projector.common.protocol.toClient.data.idea.CaretInfo +import org.jetbrains.projector.common.protocol.toClient.data.idea.SelectionInfo import kotlin.js.Json import kotlin.math.roundToLong @@ -67,6 +68,7 @@ object ManualJsonToClientMessageDecoder : ToClientMessageDecoder { "n" -> ServerMarkdownEvent.ServerMarkdownScrollEvent(content["a"] as Int, content["b"] as Int) "o" -> ServerMarkdownEvent.ServerMarkdownBrowseUriEvent(content["a"] as String) "p" -> ServerWindowColorsEvent(content["a"].unsafeCast().toColorsStorage()) + "q" -> SpeculativeEvent.SpeculativeStringDrawnEvent(content["a"] as Int) else -> throw IllegalArgumentException("Unsupported event type: ${JSON.stringify(this)}") } } @@ -98,13 +100,29 @@ object ManualJsonToClientMessageDecoder : ToClientMessageDecoder { content["g"] as Int, content["h"] as Int, content["i"] as Int, + content["j"] as Int, + content["k"].unsafeCast().toPoint(), + content["l"] as Int, ) else -> throw IllegalArgumentException("Unsupported caret info type: ${JSON.stringify(this)}") } } private fun Json.toCaretInfo(): CaretInfo { - return CaretInfo(this["a"].unsafeCast().toPoint()) + return CaretInfo( + this["a"].unsafeCast().toPoint(), + this["b"] as Int, + this["c"]?.let { it.unsafeCast().toSelectionInfo() }, + ) + } + + private fun Json.toSelectionInfo(): SelectionInfo { + return SelectionInfo( + this["a"].unsafeCast().toPoint(), + this["b"] as Int, + this["c"].unsafeCast().toPoint(), + this["d"] as Int, + ) } private fun Array.toTarget(): ServerDrawCommandsEvent.Target { diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/speculative/Typing.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/speculative/Typing.kt index 0ab95462d..98c5890f2 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/speculative/Typing.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/speculative/Typing.kt @@ -26,8 +26,13 @@ package org.jetbrains.projector.client.web.speculative import kotlinx.browser.document import org.jetbrains.projector.client.common.canvas.Extensions.argbIntToRgbaString import org.jetbrains.projector.client.common.canvas.Extensions.toFontFaceName +import org.jetbrains.projector.client.common.misc.ParamsProvider import org.jetbrains.projector.client.common.misc.ParamsProvider.SCALING_RATIO +import org.jetbrains.projector.common.protocol.data.Point import org.jetbrains.projector.common.protocol.toClient.ServerCaretInfoChangedEvent +import org.jetbrains.projector.common.protocol.toClient.ServerDrawStringEvent +import org.jetbrains.projector.common.protocol.toClient.data.idea.CaretInfo +import org.jetbrains.projector.common.protocol.toClient.data.idea.SelectionInfo import org.jetbrains.projector.common.protocol.toServer.ClientKeyPressEvent import org.jetbrains.projector.common.protocol.toServer.KeyModifier import org.w3c.dom.CanvasRenderingContext2D @@ -39,10 +44,22 @@ sealed class Typing { abstract fun removeSpeculativeImage() - abstract fun addEventChar(event: ClientKeyPressEvent) + abstract fun onDrawString(stringEvent: ServerDrawStringEvent, offset: Point) + + abstract fun onSymbolValidated(requestId: Int) + + abstract fun addEventChar(event: ClientKeyPressEvent) : DrawingResult abstract fun dispose() + sealed class DrawingResult { + + object Skip : DrawingResult() + + data class Drawn(val operationId: Int, val editorId: Int, val virtualOffset: Int, val selection: SelectionInfo?): DrawingResult() + + } + object NotSpeculativeTyping : Typing() { override fun changeCaretInfo(caretInfoChange: ServerCaretInfoChangedEvent.CaretInfoChange) { @@ -53,10 +70,16 @@ sealed class Typing { // do nothing } - override fun addEventChar(event: ClientKeyPressEvent) { + override fun onDrawString(stringEvent: ServerDrawStringEvent, offset: Point) { // do nothing } + override fun onSymbolValidated(requestId: Int) { + // do nothing + } + + override fun addEventChar(event: ClientKeyPressEvent) = DrawingResult.Skip + override fun dispose() { // do nothing } @@ -65,6 +88,7 @@ sealed class Typing { class SpeculativeTyping(private val canvasByIdGetter: (Int) -> HTMLCanvasElement?) : Typing() { private val speculativeCanvasImage = (document.createElement("canvas") as HTMLCanvasElement).apply { + id = "speculativeCanvas" style.apply { position = "fixed" display = "none" @@ -79,37 +103,122 @@ sealed class Typing { save() } + private val CaretInfo.isVisibleInEditor: Boolean + get() = locationInWindow.x >= 0 && locationInWindow.y >= 0 + private var carets: ServerCaretInfoChangedEvent.CaretInfoChange = ServerCaretInfoChangedEvent.CaretInfoChange.NoCarets + private var drawnSpeculativeSymbols = mutableMapOf() + + private var lastId = 0 + private fun updateCanvas() { val caret = carets as? ServerCaretInfoChangedEvent.CaretInfoChange.Carets val canvas = caret?.editorWindowId?.let { canvasByIdGetter(it) } - if (canvas != null) { + if (canvas != null && ParamsProvider.HIDE_MAIN_CANVAS_ON_SPECULATIVE_TYPING) { canvas.style.display = "block" - - ensureSpeculativeCanvasSize(canvas) - - speculativeCanvasContext.apply { - restore() - save() - - drawImage(canvas, 0.0, 0.0) - } } speculativeCanvasImage.style.display = "none" } override fun changeCaretInfo(caretInfoChange: ServerCaretInfoChangedEvent.CaretInfoChange) { + val oldCarets = carets + if (caretInfoChange is ServerCaretInfoChangedEvent.CaretInfoChange.Carets + && oldCarets is ServerCaretInfoChangedEvent.CaretInfoChange.Carets + && caretInfoChange.editorScrolled != oldCarets.editorScrolled + ) { + removeSpeculativeImage() + } carets = caretInfoChange } override fun removeSpeculativeImage() { + if (drawnSpeculativeSymbols.isNotEmpty()) { + drawnSpeculativeSymbols.clear() + updateCanvas() + } + } + + /** + * Checks that symbol is located at event right bound + */ + private fun isSymbolAtEventEnd(symbol: DrawnSymbol, stringEvent: ServerDrawStringEvent, eventOffset: Point): Boolean { + val currentCarets = (carets as? ServerCaretInfoChangedEvent.CaretInfoChange.Carets) ?: return false + + val shiftToLineTop = currentCarets.lineAscent + val totalVerticalPaintOffset = stringEvent.y + eventOffset.y - shiftToLineTop + + val offsetInEditor = totalVerticalPaintOffset - currentCarets.editorMetrics.y + + val symbolShiftX = symbol.carets.run { editorMetrics.x - editorScrolled.x } + val symbolShiftY = symbol.carets.run { editorMetrics.y - editorScrolled.y } + + // event rectangle + val topLeftX1 = stringEvent.x + val topLeftY1 = offsetInEditor + currentCarets.editorScrolled.y + val bottomRightX1 = topLeftX1 + stringEvent.desiredWidth + val bottomRightY1 = topLeftY1 + currentCarets.lineHeight + + // speculative symbol rectangle + val topLeftX2 = symbol.x - symbolShiftX + val topLeftY2 = symbol.y - symbolShiftY + val bottomRightX2 = topLeftX2 + symbol.width + val bottomRightY2 = topLeftY2 + symbol.height + + return bottomRightX1 == bottomRightX2 && topLeftY1 == topLeftY2 && bottomRightY1 == bottomRightY2 + } + + private fun removeSymbolIfNeeded(symbol: DrawnSymbol, stringEvent: ServerDrawStringEvent, eventOffset: Point): Boolean { + if (!isSymbolAtEventEnd(symbol, stringEvent, eventOffset)) return false + symbol.removeFromCanvas() + return true + } + + private fun clearSpeculativeCanvas() { + speculativeCanvasContext.clearRect(0.0, 0.0, speculativeCanvasImage.width.toDouble(), speculativeCanvasImage.height.toDouble()) updateCanvas() } + private fun clearSpeculativeCanvasIfNeeded() { + if (drawnSpeculativeSymbols.isEmpty()) { + clearSpeculativeCanvas() + } + } + + override fun onDrawString(stringEvent: ServerDrawStringEvent, offset: Point) { + if (drawnSpeculativeSymbols.isNotEmpty()) { + val notRemoved = mutableSetOf() // skipped ids + val toRemove = mutableSetOf() // ids' of chars that should be removed + + drawnSpeculativeSymbols.entries.forEach { (id, symbol) -> + val removed = removeSymbolIfNeeded(symbol, stringEvent, offset) + if (removed) { + // remove all previously skipped chars + notRemoved.removeAll { notRemovedId -> + drawnSpeculativeSymbols[notRemovedId]?.removeFromCanvas() + toRemove.add(notRemovedId) + } + toRemove += id + } else { + notRemoved += id + } + } + + toRemove.forEach { drawnSpeculativeSymbols.remove(it) } + + clearSpeculativeCanvasIfNeeded() + } + } + + override fun onSymbolValidated(requestId: Int) { + val symbol = drawnSpeculativeSymbols.remove(requestId) ?: return + symbol.removeFromCanvas() + clearSpeculativeCanvasIfNeeded() + } + /** * Skip drawing speculative symbol when we cannot be sure char will be actually drawn on the server. * Potentially invisible inputted chars: @@ -124,17 +233,32 @@ sealed class Typing { || event.char.category.fromOtherUnicodeGroup } - override fun addEventChar(event: ClientKeyPressEvent) { - if (shouldSkipEvent(event)) return + override fun addEventChar(event: ClientKeyPressEvent): DrawingResult { + if (shouldSkipEvent(event)) return DrawingResult.Skip - val currentCarets = carets as? ServerCaretInfoChangedEvent.CaretInfoChange.Carets ?: return + val currentCarets = carets as? ServerCaretInfoChangedEvent.CaretInfoChange.Carets ?: return DrawingResult.Skip - val canvas = canvasByIdGetter(currentCarets.editorWindowId) ?: return + val canvas = canvasByIdGetter(currentCarets.editorWindowId) ?: return DrawingResult.Skip - val firstCaretLocation = currentCarets.caretInfoList.firstOrNull()?.locationInWindow ?: return // todo: support multiple carets + val serverFirstCaretInfo = currentCarets.caretInfoList.firstOrNull() ?: return DrawingResult.Skip // todo: support multiple carets + + val paintOffset = drawnSpeculativeSymbols.values.sumOf { it.width } + val firstCaretInfo = serverFirstCaretInfo.copy( + locationInWindow = serverFirstCaretInfo.locationInWindow.copy( + x = serverFirstCaretInfo.locationInWindow.x + paintOffset + ) + ) + + if (!firstCaretInfo.isVisibleInEditor) return DrawingResult.Skip + + val firstCaretLocation = firstCaretInfo.locationInWindow ensureSpeculativeCanvasSize(canvas) + val newId = lastId + 1 + val virtualOffset = firstCaretInfo.offset + drawnSpeculativeSymbols.size + val virtualSelection = if (drawnSpeculativeSymbols.isEmpty()) firstCaretInfo.selection else null + speculativeCanvasContext.apply { restore() save() @@ -155,7 +279,6 @@ sealed class Typing { val fontSize = "${currentCarets.fontSize}px" font = "$fontSize $fontFace" - fillStyle = currentCarets.textColor.argbIntToRgbaString() val speculativeCharWidth = measureText(event.char.toString()).width @@ -174,11 +297,25 @@ sealed class Typing { val stringYPos = firstCaretLocation.y + currentCarets.lineAscent + fillStyle = currentCarets.backgroundColor.argbIntToRgbaString() + fillRect(firstCaretLocation.x, firstCaretLocation.y, speculativeCharWidth, currentCarets.lineHeight.toDouble()) + + fillStyle = currentCarets.textColor.argbIntToRgbaString() fillText(event.char.toString(), firstCaretLocation.x, stringYPos) + + drawnSpeculativeSymbols[newId] = DrawnSymbol(firstCaretLocation.x, firstCaretLocation.y, + speculativeCharWidth, currentCarets.lineHeight.toDouble(), + currentCarets) } speculativeCanvasImage.style.display = "block" - canvas.style.display = "none" + if (ParamsProvider.HIDE_MAIN_CANVAS_ON_SPECULATIVE_TYPING) { + canvas.style.display = "none" + } + + lastId = newId + + return DrawingResult.Drawn(newId, currentCarets.editorId, virtualOffset, virtualSelection) } private fun ensureSpeculativeCanvasSize(canvas: HTMLCanvasElement) { @@ -188,7 +325,10 @@ sealed class Typing { speculativeStyle.top = canvasStyle.top speculativeStyle.width = canvasStyle.width speculativeStyle.height = canvasStyle.height - speculativeStyle.zIndex = canvasStyle.zIndex + speculativeStyle.zIndex = canvasStyle.zIndex.let zTransform@ { + if (ParamsProvider.HIDE_MAIN_CANVAS_ON_SPECULATIVE_TYPING) return@zTransform it + (it.toInt() + 1).toString() + } } } @@ -201,5 +341,15 @@ sealed class Typing { override fun dispose() { speculativeCanvasImage.remove() } + + private data class DrawnSymbol( + val x: Double, val y: Double, + val width: Double, val height: Double, + val carets: ServerCaretInfoChangedEvent.CaretInfoChange.Carets, + ) + + private fun DrawnSymbol.removeFromCanvas() { + speculativeCanvasContext.clearRect(x, y, width, height) + } } } diff --git a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ClientState.kt b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ClientState.kt index cd4c3646f..d5a2c83bf 100644 --- a/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ClientState.kt +++ b/projector-client-web/src/main/kotlin/org/jetbrains/projector/client/web/state/ClientState.kt @@ -500,10 +500,15 @@ sealed class ClientState { } is ClientAction.AddEvent -> { - val event = action.event + var event = action.event if (event is ClientKeyPressEvent) { - typing.addEventChar(event) + val drawingResult = typing.addEventChar(event) + if (drawingResult is Typing.DrawingResult.Drawn) { + event = ClientSpeculativeKeyPressEvent( + event, drawingResult.operationId, drawingResult.editorId, drawingResult.virtualOffset, drawingResult.selection) + + } } eventsToSend.add(event) diff --git a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/handshake/Constant.kt b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/handshake/Constant.kt index afaa44653..913327acc 100644 --- a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/handshake/Constant.kt +++ b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/handshake/Constant.kt @@ -57,4 +57,6 @@ val commonVersionList = listOf( 1670488062, -733798733, -1255984693, + -1671626343, + 1626821130, ) diff --git a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toClient/ServerEvent.kt b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toClient/ServerEvent.kt index ca7212531..1ea8c8367 100644 --- a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toClient/ServerEvent.kt +++ b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toClient/ServerEvent.kt @@ -132,6 +132,12 @@ data class ServerCaretInfoChangedEvent( val verticalScrollBarWidth: Int, @SerialName("i") val textColor: Int, + @SerialName("j") + val backgroundColor: Int, + @SerialName("k") + val editorScrolled: Point, + @SerialName("l") + val editorId: Int, ) : CaretInfoChange() } } @@ -241,3 +247,16 @@ data class ServerWindowColorsEvent( val windowHeaderInactiveText: PaintValue.Color, ) } + +@Serializable +sealed class SpeculativeEvent : ServerEvent() { + + @Suppress("unused") // it is actually used in org.jetbrains.projector.client.web.ServerEventsProcessor, but Qodana doesn't see it... + @Serializable + @SerialName("q") + data class SpeculativeStringDrawnEvent( + @SerialName("a") + val operationId: Int, + ) : SpeculativeEvent() + +} diff --git a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toClient/data/idea/CaretInfo.kt b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toClient/data/idea/CaretInfo.kt index e20d9573b..34fffb3ed 100644 --- a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toClient/data/idea/CaretInfo.kt +++ b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toClient/data/idea/CaretInfo.kt @@ -31,4 +31,20 @@ import org.jetbrains.projector.common.protocol.data.Point data class CaretInfo( @SerialName("a") val locationInWindow: Point, + @SerialName("b") + val offset: Int, + @SerialName("c") + val selection: SelectionInfo?, +) + +@Serializable +data class SelectionInfo( + @SerialName("a") + val startLocation: Point, + @SerialName("b") + val startOffset: Int, + @SerialName("c") + val endLocation: Point, + @SerialName("d") + val endOffset: Int, ) diff --git a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toServer/ClientEvent.kt b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toServer/ClientEvent.kt index fb450f405..11ec16f29 100644 --- a/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toServer/ClientEvent.kt +++ b/projector-common/src/commonMain/kotlin/org/jetbrains/projector/common/protocol/toServer/ClientEvent.kt @@ -26,6 +26,7 @@ package org.jetbrains.projector.common.protocol.toServer import kotlinx.serialization.Serializable import org.jetbrains.projector.common.protocol.data.* import org.jetbrains.projector.common.protocol.handshake.DisplayDescription +import org.jetbrains.projector.common.protocol.toClient.data.idea.SelectionInfo enum class ResizeDirection { NW, @@ -132,6 +133,16 @@ data class ClientKeyPressEvent( val modifiers: Set, ) : ClientEvent() +@Suppress("unused") // it is actually used in org.jetbrains.projector.client.web.state.ClientState, but Qodana doesn't see it... +@Serializable +data class ClientSpeculativeKeyPressEvent( + val originalEvent: ClientKeyPressEvent, + val requestId: Int, + val editorId: Int, + val offset: Int, + val selectionInfo: SelectionInfo?, +) : ClientEvent() + @Serializable data class ClientRawKeyEvent( /** From connection opening. */