mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[fleet, lsp] multiplatform protocol module
GitOrigin-RevId: d5e04548ba175ff0d1486ef9179394ff05eb16a9
This commit is contained in:
committed by
intellij-monorepo-bot
parent
bcad681773
commit
b181f5ecc3
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-59
@@ -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>()
|
||||
+1
@@ -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(
|
||||
+1
@@ -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(
|
||||
+1
@@ -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(
|
||||
+1
@@ -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(
|
||||
+2
-4
@@ -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,
|
||||
+9
-16
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -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(
|
||||
+1
@@ -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(
|
||||
+1
@@ -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
|
||||
+1
@@ -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(
|
||||
+1
@@ -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 {
|
||||
+1
@@ -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(
|
||||
+1
@@ -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(
|
||||
+1
@@ -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(
|
||||
+1
@@ -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)
|
||||
Reference in New Issue
Block a user