diff --git a/.idea/modules.xml b/.idea/modules.xml
index 086a6a9cb4ad..8d131a60d490 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -24,6 +24,7 @@
+
diff --git a/build/bazel-generated-file-list.txt b/build/bazel-generated-file-list.txt
index 57842044c818..41b2ce271e64 100644
--- a/build/bazel-generated-file-list.txt
+++ b/build/bazel-generated-file-list.txt
@@ -240,6 +240,7 @@ fleet/ktor/client/core
fleet/ktor/network/tls
fleet/ktor/server/cio
fleet/lsp.protocol
+fleet/lsp.protocol/test
fleet/modules/api
fleet/modules/jvm
fleet/multiplatform.shims
diff --git a/fleet/lsp.protocol/test/BUILD.bazel b/fleet/lsp.protocol/test/BUILD.bazel
new file mode 100644
index 000000000000..4f08dcf384e3
--- /dev/null
+++ b/fleet/lsp.protocol/test/BUILD.bazel
@@ -0,0 +1,55 @@
+### auto-generated section `build fleet.lsp.protocol.test` start
+load("//build:compiler-options.bzl", "create_kotlinc_options")
+load("@rules_jvm//:jvm.bzl", "jvm_library")
+
+create_kotlinc_options(
+ name = "custom_test",
+ opt_in = [
+ "kotlinx.coroutines.ExperimentalCoroutinesApi",
+ "kotlinx.serialization.ExperimentalSerializationApi",
+ "kotlin.concurrent.atomics.ExperimentalAtomicApi",
+ ],
+ x_consistent_data_class_copy_visibility = True,
+ x_context_parameters = True,
+ x_lambdas = "class"
+)
+
+jvm_library(
+ name = "test",
+ visibility = ["//visibility:public"],
+ srcs = glob([], allow_empty = True, exclude = ["**/module-info.java"]),
+ runtime_deps = [
+ "@lib//:kotlin-stdlib",
+ "@lib//:kotlinx-coroutines-core",
+ "@lib//:kotlinx-io-core",
+ ]
+)
+
+jvm_library(
+ name = "test_test_lib",
+ module_name = "fleet.lsp.protocol.test",
+ visibility = ["//visibility:public"],
+ srcs = glob(["src/**/*.kt", "src/**/*.java", "src/**/*.form"], allow_empty = True, exclude = ["**/module-info.java"]),
+ kotlinc_opts = ":custom_test",
+ deps = [
+ "@lib//:kotlin-stdlib",
+ "@lib//:kotlinx-coroutines-core",
+ "@lib//:kotlinx-io-core",
+ "//fleet/ktor/client/cio",
+ "//fleet/ktor/client/core",
+ "//fleet/ktor/network/tls",
+ "//fleet/lsp.protocol",
+ "//fleet/test-runtime",
+ ],
+ runtime_deps = [":test"]
+)
+### auto-generated section `build fleet.lsp.protocol.test` end
+
+### auto-generated section `test fleet.lsp.protocol.test` start
+load("@community//build:tests-options.bzl", "jps_test")
+
+jps_test(
+ name = "test_test",
+ runtime_deps = [":test_test_lib"]
+)
+### auto-generated section `test fleet.lsp.protocol.test` end
\ No newline at end of file
diff --git a/fleet/lsp.protocol/test/fleet.lsp.protocol.test.iml b/fleet/lsp.protocol/test/fleet.lsp.protocol.test.iml
new file mode 100644
index 000000000000..c54e2f4e2645
--- /dev/null
+++ b/fleet/lsp.protocol/test/fleet.lsp.protocol.test.iml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/fleet/lsp.protocol/test/gradlebuild/build.gradle.kts b/fleet/lsp.protocol/test/gradlebuild/build.gradle.kts
new file mode 100644
index 000000000000..aca461499cf1
--- /dev/null
+++ b/fleet/lsp.protocol/test/gradlebuild/build.gradle.kts
@@ -0,0 +1,72 @@
+// IMPORT__MARKER_START
+import fleet.buildtool.conventions.configureAtMostOneJvmTargetOrThrow
+import fleet.buildtool.conventions.withJavaSourceSet
+// IMPORT__MARKER_END
+plugins {
+ alias(libs.plugins.kotlin.multiplatform)
+ id("fleet.project-module-conventions")
+ id("fleet.toolchain-conventions")
+ id("fleet.module-publishing-conventions")
+ id("fleet.sdk-repositories-publishing-conventions")
+ id("fleet.open-source-module-conventions")
+ alias(libs.plugins.dokka)
+ // GRADLE_PLUGINS__MARKER_START
+ id("fleet-module")
+ // GRADLE_PLUGINS__MARKER_END
+}
+
+fleetModule {
+ module {
+ name = "fleet.lsp.protocol.test"
+ importedFromJps {}
+ test {}
+ }
+}
+
+@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
+kotlin {
+ // KOTLIN__MARKER_START
+ compilerOptions.freeCompilerArgs = listOf(
+ "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
+ "-Xlambdas=class",
+ "-Xconsistent-data-class-copy-visibility",
+ "-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
+ "-Xcontext-parameters",
+ "-Xjvm-default=all",
+ "-XXLanguage:+AllowEagerSupertypeAccessibilityChecks",
+ "-opt-in=kotlin.concurrent.atomics.ExperimentalAtomicApi",
+ "-progressive",
+ )
+ jvm {}
+ sourceSets.jvmTest.configure { kotlin.srcDir(layout.projectDirectory.dir("../src")) }
+ configureAtMostOneJvmTargetOrThrow { compilations.named("test") { withJavaSourceSet { javaSourceSet -> javaSourceSet.java.srcDir(layout.projectDirectory.dir("../src")) } } }
+ sourceSets.commonMain.configure { kotlin.srcDir(layout.projectDirectory.dir("../srcCommonMain")) }
+ sourceSets.commonMain.configure { resources.srcDir(layout.projectDirectory.dir("../resourcesCommonMain")) }
+ sourceSets.commonTest.configure { kotlin.srcDir(layout.projectDirectory.dir("../srcCommonTest")) }
+ sourceSets.commonTest.configure { resources.srcDir(layout.projectDirectory.dir("../resourcesCommonTest")) }
+ sourceSets.jvmMain.configure { kotlin.srcDir(layout.projectDirectory.dir("../srcJvmMain")) }
+ configureAtMostOneJvmTargetOrThrow { compilations.named("main") { withJavaSourceSet { javaSourceSet -> javaSourceSet.java.srcDir(layout.projectDirectory.dir("../srcJvmMain")) } } }
+ sourceSets.jvmMain.configure { resources.srcDir(layout.projectDirectory.dir("../resourcesJvmMain")) }
+ sourceSets.jvmTest.configure { kotlin.srcDir(layout.projectDirectory.dir("../srcJvmTest")) }
+ configureAtMostOneJvmTargetOrThrow { compilations.named("test") { withJavaSourceSet { javaSourceSet -> javaSourceSet.java.srcDir(layout.projectDirectory.dir("../srcJvmTest")) } } }
+ sourceSets.jvmTest.configure { resources.srcDir(layout.projectDirectory.dir("../resourcesJvmTest")) }
+ sourceSets.commonMain.dependencies {
+ implementation(jps.org.jetbrains.kotlin.kotlin.stdlib1993400674.get().let { "${it.group}:${it.name}:${it.version}" }) {
+ exclude(group = "org.jetbrains", module = "annotations")
+ }
+ implementation(jps.org.jetbrains.intellij.deps.kotlinx.kotlinx.coroutines.core.jvm930800474.get().let { "${it.group}:kotlinx-coroutines-core:${it.version}" }) {
+ isTransitive = false
+ }
+ implementation(jps.org.jetbrains.kotlinx.kotlinx.io.core.jvm479158162.get().let { "${it.group}:kotlinx-io-core:${it.version}" }) {
+ exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib")
+ }
+ }
+ sourceSets.commonTest.dependencies {
+ implementation(project(":fleet.ktor.client.cio"))
+ implementation(project(":fleet.ktor.client.core"))
+ implementation(project(":fleet.ktor.network.tls"))
+ implementation(project(":fleet.lsp.protocol"))
+ implementation(project(":fleet.test.runtime"))
+ }
+ // KOTLIN__MARKER_END
+}
\ No newline at end of file
diff --git a/fleet/lsp.protocol/test/src/com/jetbrains/lsp/test/ByteReaderTest.kt b/fleet/lsp.protocol/test/src/com/jetbrains/lsp/test/ByteReaderTest.kt
new file mode 100644
index 000000000000..187387c461c7
--- /dev/null
+++ b/fleet/lsp.protocol/test/src/com/jetbrains/lsp/test/ByteReaderTest.kt
@@ -0,0 +1,129 @@
+package com.jetbrains.lsp.test
+
+import com.jetbrains.lsp.implementation.ByteReader
+import com.jetbrains.lsp.implementation.readUTF8Line
+import io.ktor.utils.io.*
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.withIndex
+import kotlinx.coroutines.runBlocking
+import kotlinx.io.Source
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+private const val LF = 0x0A.toByte()
+private const val CR = 0x0D.toByte()
+
+class ByteReaderTest {
+
+ @Test
+ fun `readUTF8Line reads an LF-terminated line`() {
+ runBlocking {
+ val message = "Hello, world!"
+ val channel = ByteChannel()
+ val reader = ByteChannelReader(channel)
+
+ val line = async { reader.readUTF8Line() }
+ channel.writeLine(message, LF)
+ channel.flushAndClose()
+
+ assertEquals(message, line.await())
+ }
+ }
+
+ @Test
+ fun `readUTF8Line reads an LF-terminated line sent byte by byte`() {
+ runBlocking {
+ val message = "Hello, world!"
+ val channel = ByteChannel()
+ val reader = ByteChannelReader(channel)
+
+ val line = async { reader.readUTF8Line() }
+ message.toByteArray(Charsets.UTF_8).forEach { byte ->
+ channel.writeByte(byte)
+ channel.flush()
+ }
+ channel.writeByte(LF)
+ channel.flushAndClose()
+
+ assertEquals(message, line.await())
+ }
+ }
+
+ @Test
+ fun `readUTF8Line reads a CRLF-terminated line sent byte by byte`() {
+ runBlocking {
+ val message = "Hello, world!"
+ val channel = ByteChannel()
+ val reader = ByteChannelReader(channel)
+
+ val line = async { reader.readUTF8Line() }
+ message.toByteArray(Charsets.UTF_8).forEach { byte ->
+ channel.writeByte(byte)
+ channel.flush()
+ }
+ channel.writeBytes(CR, LF)
+ channel.flushAndClose()
+
+ assertEquals(message, line.await())
+ }
+ }
+
+ @Test
+ fun `readUTF8Line reads several LF-terminated lines`() {
+ runBlocking {
+ val messages = listOf(
+ "Hello", "world", "how", "are", "you", "doing"
+ )
+ val channel = ByteChannel()
+ val reader = ByteChannelReader(channel)
+
+ val lines = flow {
+ while (!reader.isClosedForRead) {
+ emit(reader.readUTF8Line())
+ }
+ }
+
+ for (message in messages) {
+ channel.writeLine(message, LF)
+ channel.flush()
+ }
+ channel.close()
+
+ lines.withIndex().collect { (i, line) ->
+ assertEquals(messages[i], line)
+ }
+ }
+ }
+
+ private suspend fun ByteWriteChannel.writeBytes(vararg bytes: Byte) {
+ for (byte in bytes) {
+ writeByte(byte)
+ }
+ }
+
+ private suspend fun ByteWriteChannel.writeLine(s: String, vararg terminators: Byte = byteArrayOf(LF)) {
+ writeStringUtf8(s)
+ for (terminator in terminators) {
+ writeByte(terminator)
+ }
+ }
+}
+
+@OptIn(InternalAPI::class)
+internal class ByteChannelReader(val underlying: ByteChannel) : ByteReader {
+ override val closedCause: Throwable?
+ get() = underlying.closedCause
+ override val isClosedForRead: Boolean
+ get() = underlying.isClosedForRead
+ override val readBuffer: Source
+ get() = underlying.readBuffer
+
+ override suspend fun awaitContent(min: Int): Boolean {
+ return underlying.awaitContent()
+ }
+
+ override fun cancel(cause: Throwable?) {
+ underlying.cancel(cause)
+ }
+}
\ No newline at end of file