[fleet, lsp] multiplatform protocol module

GitOrigin-RevId: d5e04548ba175ff0d1486ef9179394ff05eb16a9
This commit is contained in:
Alexander Zolotov
2025-09-18 13:05:51 +00:00
committed by intellij-monorepo-bot
parent bcad681773
commit b181f5ecc3
56 changed files with 263 additions and 241 deletions
+2 -1
View File
@@ -7,6 +7,7 @@ create_kotlinc_options(
opt_in = [
"kotlinx.coroutines.ExperimentalCoroutinesApi",
"kotlinx.serialization.ExperimentalSerializationApi",
"kotlin.concurrent.atomics.ExperimentalAtomicApi",
],
x_consistent_data_class_copy_visibility = True,
x_context_parameters = True,
@@ -17,7 +18,7 @@ jvm_library(
name = "lsp.protocol",
module_name = "fleet.lsp.protocol",
visibility = ["//visibility:public"],
srcs = glob(["src/main/**/*.kt", "src/main/**/*.java", "src/main/**/*.form"], allow_empty = True, exclude = ["**/module-info.java"]),
srcs = glob(["srcCommonMain/**/*.kt", "srcCommonMain/**/*.java", "srcCommonMain/**/*.form", "srcJvmMain/**/*.kt", "srcJvmMain/**/*.java", "srcJvmMain/**/*.form"], allow_empty = True, exclude = ["**/module-info.java"]),
kotlinc_opts = ":custom_lsp.protocol",
deps = [
"@lib//:kotlin-stdlib",
+3 -2
View File
@@ -4,7 +4,7 @@
<facet type="kotlin-language" name="Kotlin">
<configuration version="5" platform="JVM 21" allPlatforms="JVM [21]" useProjectSettings="false">
<compilerSettings>
<option name="additionalArguments" value="-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi -Xlambdas=class -Xconsistent-data-class-copy-visibility -opt-in=kotlinx.serialization.ExperimentalSerializationApi -Xcontext-parameters -Xjvm-default=all -XXLanguage:+AllowEagerSupertypeAccessibilityChecks" />
<option name="additionalArguments" value="-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" />
</compilerSettings>
<compilerArguments>
<stringArguments>
@@ -24,7 +24,8 @@
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/srcCommonMain" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/srcJvmMain" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/gradlebuild/build" />
</content>
<orderEntry type="inheritedJdk" />
@@ -33,11 +33,13 @@ kotlin {
"-Xcontext-parameters",
"-Xjvm-default=all",
"-XXLanguage:+AllowEagerSupertypeAccessibilityChecks",
"-opt-in=kotlin.concurrent.atomics.ExperimentalAtomicApi",
)
jvm {}
wasmJs {
browser {}
}
pluginManager.withPlugin("fleet-build-jps-module-plugin") {
sourceSets.jvmMain.configure { kotlin.srcDir(layout.projectDirectory.dir("../src/main")) }
configureAtMostOneJvmTargetOrThrow { compilations.named("main") { withJavaSourceSet { javaSourceSet -> javaSourceSet.java.srcDir(layout.projectDirectory.dir("../src/main")) } } }
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("../testCommonTest")) }
@@ -48,6 +50,10 @@ kotlin {
sourceSets.jvmTest.configure { kotlin.srcDir(layout.projectDirectory.dir("../testJvmTest")) }
configureAtMostOneJvmTargetOrThrow { compilations.named("test") { withJavaSourceSet { javaSourceSet -> javaSourceSet.java.srcDir(layout.projectDirectory.dir("../testJvmTest")) } } }
sourceSets.jvmTest.configure { resources.srcDir(layout.projectDirectory.dir("../testResourcesJvmTest")) }
sourceSets.wasmJsMain.configure { kotlin.srcDir(layout.projectDirectory.dir("../srcWasmJsMain")) }
sourceSets.wasmJsMain.configure { resources.srcDir(layout.projectDirectory.dir("../resourcesWasmJsMain")) }
sourceSets.wasmJsTest.configure { kotlin.srcDir(layout.projectDirectory.dir("../testWasmJsTest")) }
sourceSets.wasmJsTest.configure { resources.srcDir(layout.projectDirectory.dir("../testResourcesWasmJsTest")) }
}
sourceSets.commonMain.dependencies {
implementation(jps.org.jetbrains.kotlin.kotlin.stdlib1993400674.get().let { "${it.group}:${it.name}:${it.version}" }) {
@@ -1,157 +0,0 @@
package com.jetbrains.lsp.implementation
import com.jetbrains.lsp.protocol.*
import io.ktor.utils.io.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.io.IOException
import kotlinx.serialization.json.JsonElement
import java.io.ByteArrayOutputStream
private suspend fun ByteReadChannel.readLine(): String {
val buffer = ByteArrayOutputStream()
var prevChar: Int? = null
while (true) {
val currentChar = this.readByte().toInt()
if (currentChar == -1 || (prevChar == '\r'.code && currentChar == '\n'.code)) {
break
}
if (prevChar != null) {
buffer.write(prevChar)
}
prevChar = currentChar
}
return buffer.toString(Charsets.UTF_8).trim()
}
private suspend fun ByteReadChannel.readFrame(): JsonElement? {
var contentLength = -1
var readSomething = false
val buf = try {
while (!isClosedForRead) {
val line = this.readLine()
if (line.isEmpty()) break
readSomething = true
val (key, value) = line.split(':').map { it.trim() }
if (key == "Content-Length") {
contentLength = value.toInt()
}
}
if (!readSomething) return null
if (contentLength == -1) throw IllegalStateException("Content-Length header not found")
readByteArray(contentLength)
} catch (e: Exception) {
when (e) {
is IOException -> return null
else -> throw e
}
}
return LSP.json.decodeFromString(JsonElement.serializer(), String(buf, Charsets.UTF_8))
}
/**
* @return Boolean indicating whether the frame was successfully written (`true`) or the channel was closed (`false`).
*/
private suspend fun ByteWriteChannel.writeFrame(jsonElement: JsonElement): Boolean {
val str = LSP.json.encodeToString(JsonElement.serializer(), jsonElement)
val frameStr = buildString {
// protocol requires string length in bytes
val contentLengthInBytes = str.toByteArray(Charsets.UTF_8).size
append("Content-Length: $contentLengthInBytes\r\n")
append("Content-Type: application/json-rpc; charset=utf-8\r\n")
append("\r\n")
append(str)
}
try {
if (isClosedForWrite) return false
writeByteArray(frameStr.toByteArray(Charsets.UTF_8))
flush()
return true
}
catch (e: Exception) {
when (e) {
is IOException -> return false
else -> throw e
}
}
}
suspend fun withBaseProtocolFraming(
connection: LspConnection,
exitSignal: CompletableDeferred<Unit>?,
body: suspend CoroutineScope.(
incoming: ReceiveChannel<JsonElement>,
outgoing: SendChannel<JsonElement>,
) -> Unit,
) {
val reader = connection.input
val writer = connection.output
coroutineScope {
val (incomingSender, incomingReceiver) = channels<JsonElement>()
val (outgoingSender, outgoingReceiver) = channels<JsonElement>(Channel.UNLIMITED)
val readJob = launch {
incomingSender.use {
while (true) {
val frame = reader.readFrame()
if (frame == null) {
exitSignal?.complete(Unit)
break
}
incomingSender.send(frame)
}
}
}
val writeJob = launch {
outgoingReceiver.consumeEach { frame ->
val success = writer.writeFrame(frame)
if (!success) {
exitSignal?.complete(Unit)
}
}
}
try {
body(incomingReceiver, outgoingSender)
}
finally {
readJob.cancel()
writeJob.cancel()
connection.close()
}
}
}
fun main() {
val handler = lspHandlers {
request(Initialize) { initParams ->
InitializeResult(
capabilities = ServerCapabilities(
textDocumentSync = TextDocumentSyncKind.Incremental,
),
serverInfo = InitializeResult.ServerInfo(
name = "IntelliJ Analyzer",
version = "1.0"
),
)
}
notification(DocumentSync.DidOpen) { didOpen ->
println("didOpen: $didOpen")
}
notification(DocumentSync.DidChange) { didChange ->
println("didChange: $didChange")
}
}
runBlocking(Dispatchers.Default) {
tcpServer(TcpConnectionConfig.Server("127.0.0.1", 9999, isMultiClient = true)) { connection ->
withBaseProtocolFraming(connection, exitSignal = null) { incoming, outgoing ->
withLsp(incoming, outgoing, handler) { lsp ->
awaitCancellation()
}
}
}
}
}
@@ -0,0 +1,114 @@
package com.jetbrains.lsp.implementation
import com.jetbrains.lsp.protocol.LSP
import fleet.util.decodeToStringUtf8
import fleet.util.encodeToByteArrayUtf8
import io.ktor.utils.io.*
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.io.IOException
import kotlinx.serialization.json.JsonElement
suspend fun withBaseProtocolFraming(
connection: LspConnection,
exitSignal: CompletableDeferred<Unit>?,
body: suspend CoroutineScope.(
incoming: ReceiveChannel<JsonElement>,
outgoing: SendChannel<JsonElement>,
) -> Unit,
) {
val reader = connection.input
val writer = connection.output
coroutineScope {
val (incomingSender, incomingReceiver) = channels<JsonElement>()
val (outgoingSender, outgoingReceiver) = channels<JsonElement>(Channel.UNLIMITED)
val readJob = launch {
incomingSender.use {
while (true) {
val frame = reader.readFrame()
if (frame == null) {
exitSignal?.complete(Unit)
break
}
incomingSender.send(frame)
}
}
}
val writeJob = launch {
outgoingReceiver.consumeEach { frame ->
val success = writer.writeFrame(frame)
if (!success) {
exitSignal?.complete(Unit)
}
}
}
try {
body(incomingReceiver, outgoingSender)
}
finally {
readJob.cancel()
writeJob.cancel()
connection.close()
}
}
}
private suspend fun ByteReadChannel.readFrame(): JsonElement? {
var contentLength = -1
var readSomething = false
val buf = try {
while (!isClosedForRead) {
val line = readUTF8Line()
if (line.isNullOrEmpty()) break
readSomething = true
val (key, value) = line.split(':').map { it.trim() }
if (key == "Content-Length") {
contentLength = value.toInt()
}
}
if (!readSomething) return null
if (contentLength == -1) throw IllegalStateException("Content-Length header not found")
readByteArray(contentLength)
} catch (e: Exception) {
when (e) {
is IOException -> return null
else -> throw e
}
}
return LSP.json.decodeFromString(JsonElement.serializer(), buf.decodeToStringUtf8())
}
/**
* @return Boolean indicating whether the frame was successfully written (`true`) or the channel was closed (`false`).
*/
private suspend fun ByteWriteChannel.writeFrame(jsonElement: JsonElement): Boolean {
val str = LSP.json.encodeToString(JsonElement.serializer(), jsonElement)
val frameStr = buildString {
// protocol requires string length in bytes
val contentLengthInBytes = str.encodeToByteArrayUtf8().size
append("Content-Length: $contentLengthInBytes\r\n")
append("Content-Type: application/json-rpc; charset=utf-8\r\n")
append("\r\n")
append(str)
}
try {
if (isClosedForWrite) return false
writeByteArray(frameStr.encodeToByteArrayUtf8())
flush()
return true
}
catch (e: Exception) {
when (e) {
is IOException -> return false
else -> throw e
}
}
}
@@ -1,19 +1,18 @@
package com.jetbrains.lsp.implementation
import com.jetbrains.lsp.protocol.*
import fleet.multiplatform.shims.ConcurrentHashMap
import fleet.util.logging.logger
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.atomics.AtomicInt
import kotlin.concurrent.atomics.incrementAndFetch
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@@ -43,13 +42,13 @@ suspend fun withLsp(
) {
coroutineScope {
val outgoingRequests = ConcurrentHashMap<StringOrInt, OutgoingRequest>()
val idGen = AtomicInteger(0)
val idGen = AtomicInt(0)
val lspClient = object : LspClient {
override suspend fun <Params, Result, Error> request(
requestType: RequestType<Params, Result, Error>,
params: Params,
): Result {
val id = StringOrInt(JsonPrimitive(idGen.incrementAndGet()))
val id = StringOrInt(JsonPrimitive(idGen.incrementAndFetch()))
val deferred = CompletableDeferred<Any?>()
outgoingRequests[id] = OutgoingRequest(deferred, requestType)
val request = RequestMessage(
@@ -273,57 +272,4 @@ suspend fun withLsp(
}
}
fun main() {
runBlocking(Dispatchers.Default) {
val clientToServer = Channel<JsonElement>(Channel.UNLIMITED)
val serverToClient = Channel<JsonElement>(Channel.UNLIMITED)
val HelloRequestType = RequestType("hello", String.serializer(), String.serializer(), Unit.serializer())
val HangRequestType = RequestType("hand", Unit.serializer(), Unit.serializer(), Unit.serializer())
val PrintHelloNotification = NotificationType("printHello", String.serializer())
withLsp(
incoming = clientToServer,
outgoing = serverToClient,
handlers = lspHandlers {
request(HelloRequestType) { str ->
"Hello, $str"
}
notification(PrintHelloNotification) { str ->
println("server: $str")
}
request(HangRequestType) {
try {
awaitCancellation()
} catch (c: CancellationException) {
println("cancelled by client")
throw c
}
}
},
) { server ->
withLsp(
incoming = serverToClient,
outgoing = clientToServer,
handlers = lspHandlers {
notification(PrintHelloNotification) { str ->
println("client: $str")
}
},
) { client ->
println(client.request(HelloRequestType, "World"))
client.notify(PrintHelloNotification, "Hello World")
client.notify(PrintHelloNotification, "Hello World")
val hangingRequestJob = launch {
client.request(HangRequestType, Unit)
}
delay(100)
hangingRequestJob.cancel()
println("request cancelled")
delay(100)
println("quitting")
}
}
}
}
private val LOG = logger<LspClient>()
@@ -10,6 +10,7 @@ import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlin.jvm.JvmInline
@Serializable
data class CodeActionOptions(
@@ -8,6 +8,7 @@ import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlin.jvm.JvmInline
@Serializable
data class CompletionClientCapabilities(
@@ -8,6 +8,7 @@ import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlin.jvm.JvmInline
@Serializable
data class DeclarationClientCapabilities(
@@ -9,6 +9,7 @@ import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlin.jvm.JvmInline
@Serializable
data class HoverParams(
@@ -1,12 +1,10 @@
package com.jetbrains.lsp.protocol
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.KSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonObject
import kotlin.jvm.JvmInline
data class Header(
val contentLenght: Int,
@@ -1,5 +1,7 @@
package com.jetbrains.lsp.protocol
import fleet.util.isValidUriString
import io.ktor.http.Url
import kotlinx.serialization.*
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.PolymorphicKind
@@ -8,6 +10,7 @@ import kotlinx.serialization.descriptors.buildSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
import kotlin.jvm.JvmInline
/**
* URI following the URI specification, so special spaces (like spaces) are encoded.
@@ -18,18 +21,21 @@ import kotlinx.serialization.json.*
@JvmInline
value class URI(val uri: String) {
init {
require(isValidUriString(uri)) { "Invalid URI: $uri" }
/**
* We need to have consistent URIs as they are used as keys in the analyzer
*/
require(uri.isValidUriString()) { "Invalid URI: $uri" }
}
/**
* Returns the URI's schema without schema delimiter (`://`)
*/
val scheme: String get() = asJavaUri().scheme
val scheme: String get() = Url(uri).protocol.name
/**
* Returns the file name
*/
val fileName: String get() = asJavaUri().path.substringAfterLast('/')
val fileName: String get() = Url(uri).segments.last()
/**
* Returns the file extension (without dot) if present
@@ -41,25 +47,12 @@ value class URI(val uri: String) {
return if (dotIndex > 0) name.substring(dotIndex + 1) else null
}
fun asJavaUri(): java.net.URI = java.net.URI(uri)
object Schemas {
const val FILE: String = "file"
const val JRT: String = "jrt"
const val JAR: String = "jar"
const val ZIP: String = "zip"
}
companion object {
/**
* We need to have consistent URIs as they are used as keys in the analyzer
*/
fun isValidUriString(uriString: String): Boolean {
val javaUri = runCatching { java.net.URI(uriString) }.getOrNull() ?: return false
return javaUri.scheme != null
}
}
}
@@ -3,6 +3,7 @@ package com.jetbrains.lsp.protocol
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline
@Serializable
data class NotebookDocumentClientCapabilities(
@@ -8,6 +8,7 @@ import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlin.jvm.JvmInline
@Serializable
data class SemanticTokensLegend(
@@ -7,6 +7,7 @@ import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.decodeFromJsonElement
import kotlin.jvm.JvmInline
@Serializable
@JvmInline
@@ -15,6 +15,7 @@ import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlin.jvm.JvmInline
@Serializable
data class SignatureHelpOptions(
@@ -7,6 +7,7 @@ import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlin.jvm.JvmInline
@Serializable(with = JsonObjectOrArraySerializer::class)
sealed interface JsonObjectOrArray {
@@ -10,6 +10,7 @@ import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlin.jvm.JvmInline
@Serializable
data class DocumentHighlightParams(
@@ -10,6 +10,7 @@ import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlin.jvm.JvmInline
@Serializable
data class DidChangeWorkspaceFoldersParams(
@@ -8,6 +8,7 @@ import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlin.jvm.JvmInline
@Serializable
data class PrepareRenameParams(
@@ -7,6 +7,7 @@ import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlin.jvm.JvmInline
@Serializable
data class WorkspaceSymbolOptions(
@@ -0,0 +1,66 @@
package com.jetbrains.lsp.implementation
import com.jetbrains.lsp.protocol.NotificationType
import com.jetbrains.lsp.protocol.RequestType
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.JsonElement
fun main() {
runBlocking(Dispatchers.Default) {
val clientToServer = Channel<JsonElement>(Channel.UNLIMITED)
val serverToClient = Channel<JsonElement>(Channel.UNLIMITED)
val HelloRequestType = RequestType("hello", String.serializer(), String.serializer(), Unit.serializer())
val HangRequestType = RequestType("hand", Unit.serializer(), Unit.serializer(), Unit.serializer())
val PrintHelloNotification = NotificationType("printHello", String.serializer())
withLsp(
incoming = clientToServer,
outgoing = serverToClient,
handlers = lspHandlers {
request(HelloRequestType) { str ->
"Hello, $str"
}
notification(PrintHelloNotification) { str ->
println("server: $str")
}
request(HangRequestType) {
try {
awaitCancellation()
} catch (c: CancellationException) {
println("cancelled by client")
throw c
}
}
},
) { server ->
withLsp(
incoming = serverToClient,
outgoing = clientToServer,
handlers = lspHandlers {
notification(PrintHelloNotification) { str ->
println("client: $str")
}
},
) { client ->
println(client.request(HelloRequestType, "World"))
client.notify(PrintHelloNotification, "Hello World")
client.notify(PrintHelloNotification, "Hello World")
val hangingRequestJob = launch {
client.request(HangRequestType, Unit)
}
delay(100)
hangingRequestJob.cancel()
println("request cancelled")
delay(100)
println("quitting")
}
}
}
}
@@ -0,0 +1,37 @@
package com.jetbrains.lsp.implementation
import com.jetbrains.lsp.protocol.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.runBlocking
fun main() {
val handler = lspHandlers {
request(Initialize) { initParams ->
InitializeResult(
capabilities = ServerCapabilities(
textDocumentSync = TextDocumentSyncKind.Incremental,
),
serverInfo = InitializeResult.ServerInfo(
name = "IntelliJ Analyzer",
version = "1.0"
),
)
}
notification(DocumentSync.DidOpen) { didOpen ->
println("didOpen: $didOpen")
}
notification(DocumentSync.DidChange) { didChange ->
println("didChange: $didChange")
}
}
runBlocking(Dispatchers.Default) {
tcpServer(TcpConnectionConfig.Server("127.0.0.1", 9999, isMultiClient = true)) { connection ->
withBaseProtocolFraming(connection, exitSignal = null) { incoming, outgoing ->
withLsp(incoming, outgoing, handler) { lsp ->
awaitCancellation()
}
}
}
}
}
@@ -0,0 +1,4 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.lsp.protocol
fun URI.asJavaUri(): java.net.URI = java.net.URI(uri)