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. */