Replace hand-rolled JSON protocol parsing with real JSON
Daemon: parse requests with kotlinx.serialization and read fields from
the params object instead of scanning the whole line for key markers,
which picked up decoy keys ("path", "version", "id") occurring inside
buffer text. Build all responses and events with JsonObject builders,
which also fixes unescaped control characters in output and \uXXXX
escapes in input. Malformed lines now get a BAD_REQUEST reply instead
of corrupting field extraction.
Client: quote outgoing JSON strings with a strict json_quote helper
instead of Odin's %q verb, whose \x/\e escapes are not valid JSON.
Verified end to end: adversarial payloads (decoy protocol keys,
unicode, control chars, malformed lines) against the daemon, and the
full client smoke flow (ids 1-10) against a real Gradle workspace.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4a15350860
commit
8b1723ce31
5 changed files with 245 additions and 256 deletions
|
|
@ -10,6 +10,37 @@ import "core:thread"
|
|||
import "core:time"
|
||||
import json "core:encoding/json"
|
||||
|
||||
// Odin's %q verb emits \x/\e escapes that are not valid JSON, so outgoing
|
||||
// strings are quoted here instead. Control characters become \u00XX.
|
||||
json_quote :: proc(s: string, allocator := context.temp_allocator) -> string {
|
||||
builder: strings.Builder
|
||||
strings.builder_init(&builder, allocator)
|
||||
strings.write_byte(&builder, '"')
|
||||
for i := 0; i < len(s); i += 1 {
|
||||
c := s[i]
|
||||
switch c {
|
||||
case '"':
|
||||
strings.write_string(&builder, "\\\"")
|
||||
case '\\':
|
||||
strings.write_string(&builder, "\\\\")
|
||||
case '\n':
|
||||
strings.write_string(&builder, "\\n")
|
||||
case '\r':
|
||||
strings.write_string(&builder, "\\r")
|
||||
case '\t':
|
||||
strings.write_string(&builder, "\\t")
|
||||
case:
|
||||
if c < 0x20 {
|
||||
fmt.sbprintf(&builder, "\\u%04x", c)
|
||||
} else {
|
||||
strings.write_byte(&builder, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
strings.write_byte(&builder, '"')
|
||||
return strings.to_string(builder)
|
||||
}
|
||||
|
||||
Protocol_Message_Kind :: enum {
|
||||
Invalid,
|
||||
Response,
|
||||
|
|
@ -264,7 +295,7 @@ daemon_send_workspace_open :: proc(client: ^Daemon_Client, workspace: string) {
|
|||
if !client.connected do return
|
||||
id := client.next_id
|
||||
client.next_id += 1
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"workspace/open\",\"params\":{{\"root\":%q}}}}\n", id, workspace)
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"workspace/open\",\"params\":{{\"root\":%s}}}}\n", id, json_quote(workspace))
|
||||
daemon_send(client, request)
|
||||
}
|
||||
|
||||
|
|
@ -272,7 +303,7 @@ daemon_send_text_open :: proc(client: ^Daemon_Client, path: string, version: int
|
|||
if !client.connected do return
|
||||
id := client.next_id
|
||||
client.next_id += 1
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"text/open\",\"params\":{{\"path\":%q,\"version\":%d,\"text\":%q}}}}\n", id, path, version, text)
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"text/open\",\"params\":{{\"path\":%s,\"version\":%d,\"text\":%s}}}}\n", id, json_quote(path), version, json_quote(text))
|
||||
daemon_send(client, request)
|
||||
}
|
||||
|
||||
|
|
@ -280,7 +311,7 @@ daemon_send_text_change :: proc(client: ^Daemon_Client, path: string, version: i
|
|||
if !client.connected do return
|
||||
id := client.next_id
|
||||
client.next_id += 1
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"text/change\",\"params\":{{\"path\":%q,\"version\":%d,\"text\":%q}}}}\n", id, path, version, text)
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"text/change\",\"params\":{{\"path\":%s,\"version\":%d,\"text\":%s}}}}\n", id, json_quote(path), version, json_quote(text))
|
||||
daemon_send(client, request)
|
||||
}
|
||||
|
||||
|
|
@ -288,7 +319,7 @@ daemon_send_completion :: proc(client: ^Daemon_Client, path: string, line, colum
|
|||
if !client.connected do return 0
|
||||
id := client.next_id
|
||||
client.next_id += 1
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"kotlin/completion\",\"params\":{{\"path\":%q,\"line\":%d,\"column\":%d}}}}\n", id, path, line + 1, column + 1)
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"kotlin/completion\",\"params\":{{\"path\":%s,\"line\":%d,\"column\":%d}}}}\n", id, json_quote(path), line + 1, column + 1)
|
||||
daemon_track_response(client, id)
|
||||
daemon_send(client, request)
|
||||
return id
|
||||
|
|
@ -298,7 +329,7 @@ daemon_send_hover :: proc(client: ^Daemon_Client, path: string, line, column: in
|
|||
if !client.connected do return 0
|
||||
id := client.next_id
|
||||
client.next_id += 1
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"kotlin/hover\",\"params\":{{\"path\":%q,\"line\":%d,\"column\":%d}}}}\n", id, path, line + 1, column + 1)
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"kotlin/hover\",\"params\":{{\"path\":%s,\"line\":%d,\"column\":%d}}}}\n", id, json_quote(path), line + 1, column + 1)
|
||||
daemon_track_response(client, id)
|
||||
daemon_send(client, request)
|
||||
return id
|
||||
|
|
@ -308,7 +339,7 @@ daemon_send_definition :: proc(client: ^Daemon_Client, path: string, line, colum
|
|||
if !client.connected do return 0
|
||||
id := client.next_id
|
||||
client.next_id += 1
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"kotlin/definition\",\"params\":{{\"path\":%q,\"line\":%d,\"column\":%d}}}}\n", id, path, line + 1, column + 1)
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"kotlin/definition\",\"params\":{{\"path\":%s,\"line\":%d,\"column\":%d}}}}\n", id, json_quote(path), line + 1, column + 1)
|
||||
daemon_track_response(client, id)
|
||||
daemon_send(client, request)
|
||||
return id
|
||||
|
|
@ -318,7 +349,7 @@ daemon_send_references :: proc(client: ^Daemon_Client, path: string, line, colum
|
|||
if !client.connected do return 0
|
||||
id := client.next_id
|
||||
client.next_id += 1
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"kotlin/references\",\"params\":{{\"path\":%q,\"line\":%d,\"column\":%d}}}}\n", id, path, line + 1, column + 1)
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"kotlin/references\",\"params\":{{\"path\":%s,\"line\":%d,\"column\":%d}}}}\n", id, json_quote(path), line + 1, column + 1)
|
||||
daemon_track_response(client, id)
|
||||
daemon_send(client, request)
|
||||
return id
|
||||
|
|
@ -328,7 +359,7 @@ daemon_send_rename :: proc(client: ^Daemon_Client, path: string, line, column: i
|
|||
if !client.connected do return 0
|
||||
id := client.next_id
|
||||
client.next_id += 1
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"kotlin/rename\",\"params\":{{\"path\":%q,\"line\":%d,\"column\":%d,\"newName\":%q}}}}\n", id, path, line + 1, column + 1, new_name)
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"kotlin/rename\",\"params\":{{\"path\":%s,\"line\":%d,\"column\":%d,\"newName\":%s}}}}\n", id, json_quote(path), line + 1, column + 1, json_quote(new_name))
|
||||
daemon_track_response(client, id)
|
||||
daemon_send(client, request)
|
||||
return id
|
||||
|
|
@ -348,7 +379,7 @@ daemon_send_gradle_run :: proc(client: ^Daemon_Client, task: string) -> int {
|
|||
if !client.connected do return 0
|
||||
id := client.next_id
|
||||
client.next_id += 1
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"gradle/run\",\"params\":{{\"task\":%q}}}}\n", id, task)
|
||||
request := fmt.tprintf("{{\"id\":%d,\"method\":\"gradle/run\",\"params\":{{\"task\":%s}}}}\n", id, json_quote(task))
|
||||
daemon_track_response(client, id)
|
||||
daemon_send(client, request)
|
||||
return id
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ main :: proc() {
|
|||
send_line(socket, "{\"id\":1,\"method\":\"ping\",\"params\":{}}\n")
|
||||
read_messages(socket, 1)
|
||||
|
||||
open_workspace := fmt.tprintf("{{\"id\":2,\"method\":\"workspace/open\",\"params\":{{\"root\":%q}}}}\n", workspace)
|
||||
open_workspace := fmt.tprintf("{{\"id\":2,\"method\":\"workspace/open\",\"params\":{{\"root\":%s}}}}\n", json_quote(workspace))
|
||||
send_line(socket, open_workspace)
|
||||
read_messages(socket, 3)
|
||||
|
||||
|
|
@ -72,31 +72,31 @@ main :: proc() {
|
|||
active_text := editor_active_text(&editor)
|
||||
defer delete(active_text)
|
||||
|
||||
text_change_request := fmt.tprintf("{{\"id\":4,\"method\":\"text/change\",\"params\":{{\"path\":%q,\"version\":1,\"text\":%q}}}}\n", diagnostics_path, string(active_text[:]))
|
||||
text_change_request := fmt.tprintf("{{\"id\":4,\"method\":\"text/change\",\"params\":{{\"path\":%s,\"version\":1,\"text\":%s}}}}\n", json_quote(diagnostics_path), json_quote(string(active_text[:])))
|
||||
send_line(socket, text_change_request)
|
||||
read_messages(socket, 2)
|
||||
|
||||
diagnostics_request := fmt.tprintf("{{\"id\":5,\"method\":\"kotlin/diagnostics\",\"params\":{{\"path\":%q}}}}\n", diagnostics_path)
|
||||
diagnostics_request := fmt.tprintf("{{\"id\":5,\"method\":\"kotlin/diagnostics\",\"params\":{{\"path\":%s}}}}\n", json_quote(diagnostics_path))
|
||||
send_line(socket, diagnostics_request)
|
||||
read_messages(socket, 1)
|
||||
|
||||
completion_request := fmt.tprintf("{{\"id\":6,\"method\":\"kotlin/completion\",\"params\":{{\"path\":%q,\"line\":1,\"column\":1}}}}\n", diagnostics_path)
|
||||
completion_request := fmt.tprintf("{{\"id\":6,\"method\":\"kotlin/completion\",\"params\":{{\"path\":%s,\"line\":1,\"column\":1}}}}\n", json_quote(diagnostics_path))
|
||||
send_line(socket, completion_request)
|
||||
read_messages(socket, 1)
|
||||
|
||||
hover_request := fmt.tprintf("{{\"id\":7,\"method\":\"kotlin/hover\",\"params\":{{\"path\":%q,\"line\":1,\"column\":1}}}}\n", diagnostics_path)
|
||||
hover_request := fmt.tprintf("{{\"id\":7,\"method\":\"kotlin/hover\",\"params\":{{\"path\":%s,\"line\":1,\"column\":1}}}}\n", json_quote(diagnostics_path))
|
||||
send_line(socket, hover_request)
|
||||
read_messages(socket, 1)
|
||||
|
||||
definition_request := fmt.tprintf("{{\"id\":8,\"method\":\"kotlin/definition\",\"params\":{{\"path\":%q,\"line\":1,\"column\":5}}}}\n", diagnostics_path)
|
||||
definition_request := fmt.tprintf("{{\"id\":8,\"method\":\"kotlin/definition\",\"params\":{{\"path\":%s,\"line\":1,\"column\":5}}}}\n", json_quote(diagnostics_path))
|
||||
send_line(socket, definition_request)
|
||||
read_messages(socket, 1)
|
||||
|
||||
references_request := fmt.tprintf("{{\"id\":9,\"method\":\"kotlin/references\",\"params\":{{\"path\":%q,\"line\":1,\"column\":5}}}}\n", diagnostics_path)
|
||||
references_request := fmt.tprintf("{{\"id\":9,\"method\":\"kotlin/references\",\"params\":{{\"path\":%s,\"line\":1,\"column\":5}}}}\n", json_quote(diagnostics_path))
|
||||
send_line(socket, references_request)
|
||||
read_messages(socket, 1)
|
||||
|
||||
rename_request := fmt.tprintf("{{\"id\":10,\"method\":\"kotlin/rename\",\"params\":{{\"path\":%q,\"line\":1,\"column\":5,\"newName\":\"renamedBroken\"}}}}\n", diagnostics_path)
|
||||
rename_request := fmt.tprintf("{{\"id\":10,\"method\":\"kotlin/rename\",\"params\":{{\"path\":%s,\"line\":1,\"column\":5,\"newName\":\"renamedBroken\"}}}}\n", json_quote(diagnostics_path))
|
||||
send_line(socket, rename_request)
|
||||
read_messages(socket, 1)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -298,12 +298,12 @@ ui_state_save :: proc(view: ^SDL_View) {
|
|||
append(&open_files_json, '[')
|
||||
for file, index in view.saved_open_files {
|
||||
if index > 0 do append(&open_files_json, ',')
|
||||
item := fmt.tprintf("{\"path\":%q,\"cursor\":%d}", file.path, file.cursor)
|
||||
item := fmt.tprintf("{\"path\":%s,\"cursor\":%d}", json_quote(file.path), file.cursor)
|
||||
for b in transmute([]u8)item do append(&open_files_json, b)
|
||||
}
|
||||
append(&open_files_json, ']')
|
||||
|
||||
text := fmt.tprintf("{\n \"windowWidth\": %d,\n \"windowHeight\": %d,\n \"sidebarWidth\": %d,\n \"rightSidebarWidth\": %d,\n \"explorerVisible\": %v,\n \"gradleSidebarVisible\": %v,\n \"workspace\": %q,\n \"activeFile\": %d,\n \"openFiles\": %s\n}\n", view.window_width, view.window_height, view.sidebar_width, view.right_sidebar_width, view.explorer_visible, view.gradle_sidebar_visible, view.cached_workspace, view.saved_active_file, string(open_files_json[:]))
|
||||
text := fmt.tprintf("{\n \"windowWidth\": %d,\n \"windowHeight\": %d,\n \"sidebarWidth\": %d,\n \"rightSidebarWidth\": %d,\n \"explorerVisible\": %v,\n \"gradleSidebarVisible\": %v,\n \"workspace\": %s,\n \"activeFile\": %d,\n \"openFiles\": %s\n}\n", view.window_width, view.window_height, view.sidebar_width, view.right_sidebar_width, view.explorer_visible, view.gradle_sidebar_visible, json_quote(view.cached_workspace), view.saved_active_file, string(open_files_json[:]))
|
||||
_ = os.write_entire_file(path, transmute([]byte)text)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ kotlin {
|
|||
dependencies {
|
||||
implementation(gradleApi())
|
||||
implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:2.2.21")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
|
||||
}
|
||||
|
||||
application {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,18 @@ import javax.tools.JavaFileObject
|
|||
import javax.tools.StandardLocation
|
||||
import javax.tools.ToolProvider
|
||||
import kotlin.concurrent.thread
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.add
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonArray
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import org.gradle.tooling.GradleConnector
|
||||
import org.gradle.tooling.model.GradleProject
|
||||
import org.gradle.tooling.model.idea.IdeaProject
|
||||
|
|
@ -107,35 +119,37 @@ private class ClientSession(
|
|||
val line = reader.readLine() ?: break
|
||||
if (line.isBlank()) continue
|
||||
|
||||
val id = extractJsonInt(line, "id")
|
||||
val method = extractJsonString(line, "method")
|
||||
if (id == null || method == null) {
|
||||
val request = runCatching { Json.parseToJsonElement(line).jsonObject }.getOrNull()
|
||||
val id = request?.intField("id")
|
||||
val method = request?.stringField("method")
|
||||
if (request == null || id == null || method == null) {
|
||||
writeLine(writer, errorJson(id ?: 0, "BAD_REQUEST", "Request needs numeric id and string method"))
|
||||
continue
|
||||
}
|
||||
val params = request["params"] as? JsonObject ?: JsonObject(emptyMap())
|
||||
|
||||
when (method) {
|
||||
"ping" -> writeLine(writer, okJson(id, "{\"pong\":true}"))
|
||||
"workspace/open" -> openWorkspace(writer, id, line)
|
||||
"ping" -> writeLine(writer, okJson(id, buildJsonObject { put("pong", true) }))
|
||||
"workspace/open" -> openWorkspace(writer, id, params)
|
||||
"workspace/close" -> closeWorkspace(writer, id)
|
||||
"text/open", "text/change" -> updateText(writer, id, line)
|
||||
"text/close" -> closeText(writer, id, line)
|
||||
"kotlin/diagnostics" -> kotlinDiagnostics(writer, id, line)
|
||||
"kotlin/completion" -> kotlinCompletion(writer, id, line)
|
||||
"kotlin/definition" -> kotlinDefinition(writer, id, line)
|
||||
"kotlin/references" -> kotlinReferences(writer, id, line)
|
||||
"kotlin/rename" -> kotlinRename(writer, id, line)
|
||||
"kotlin/hover" -> kotlinHover(writer, id, line)
|
||||
"text/open", "text/change" -> updateText(writer, id, params)
|
||||
"text/close" -> closeText(writer, id, params)
|
||||
"kotlin/diagnostics" -> kotlinDiagnostics(writer, id, params)
|
||||
"kotlin/completion" -> kotlinCompletion(writer, id, params)
|
||||
"kotlin/definition" -> kotlinDefinition(writer, id, params)
|
||||
"kotlin/references" -> kotlinReferences(writer, id, params)
|
||||
"kotlin/rename" -> kotlinRename(writer, id, params)
|
||||
"kotlin/hover" -> kotlinHover(writer, id, params)
|
||||
"gradle/tasks" -> writeLine(writer, okJson(id, gradleTasksJson()))
|
||||
"gradle/run" -> runGradle(writer, id, line)
|
||||
"gradle/run" -> runGradle(writer, id, params)
|
||||
else -> writeLine(writer, errorJson(id, "UNKNOWN_METHOD", "No handler for $method"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openWorkspace(writer: BufferedWriter, id: Int, line: String) {
|
||||
val rootText = extractJsonString(line, "root")
|
||||
private fun openWorkspace(writer: BufferedWriter, id: Int, params: JsonObject) {
|
||||
val rootText = params.stringField("root")
|
||||
if (rootText == null) {
|
||||
writeLine(writer, errorJson(id, "BAD_REQUEST", "workspace/open needs params.root"))
|
||||
return
|
||||
|
|
@ -143,24 +157,24 @@ private class ClientSession(
|
|||
|
||||
val root = File(rootText).absoluteFile.normalize()
|
||||
if (!root.isDirectory) {
|
||||
writeLine(writer, errorJson(id, "NO_SUCH_WORKSPACE", "Workspace root does not exist: ${escapeJson(root.path)}"))
|
||||
writeLine(writer, errorJson(id, "NO_SUCH_WORKSPACE", "Workspace root does not exist: ${root.path}"))
|
||||
return
|
||||
}
|
||||
|
||||
state.workspaceRoot = root
|
||||
writeLine(writer, eventJson("workspace/indexing", "{\"state\":\"started\"}"))
|
||||
writeLine(writer, eventJson("workspace/indexing", indexingStateJson("started")))
|
||||
|
||||
val gradleWorkspace = try {
|
||||
importGradleWorkspace(root)
|
||||
} catch (t: Throwable) {
|
||||
writeLine(writer, eventJson("workspace/indexing", "{\"state\":\"failed\"}"))
|
||||
writeLine(writer, eventJson("workspace/indexing", indexingStateJson("failed")))
|
||||
writeLine(writer, errorJson(id, "GRADLE_IMPORT_FAILED", t.message ?: t.javaClass.name))
|
||||
return
|
||||
}
|
||||
|
||||
state.gradleWorkspace = gradleWorkspace
|
||||
writeLine(writer, okJson(id, workspaceJson(gradleWorkspace)))
|
||||
writeLine(writer, eventJson("workspace/indexing", "{\"state\":\"idle\"}"))
|
||||
writeLine(writer, eventJson("workspace/indexing", indexingStateJson("idle")))
|
||||
}
|
||||
|
||||
private fun closeWorkspace(writer: BufferedWriter, id: Int) {
|
||||
|
|
@ -169,38 +183,41 @@ private class ClientSession(
|
|||
state.openTexts.clear()
|
||||
state.textVersions.clear()
|
||||
state.diagnosticsVersions.clear()
|
||||
writeLine(writer, okJson(id, "{\"closed\":true}"))
|
||||
writeLine(writer, okJson(id, buildJsonObject { put("closed", true) }))
|
||||
}
|
||||
|
||||
private fun updateText(writer: BufferedWriter, id: Int, line: String) {
|
||||
val path = extractJsonString(line, "path")
|
||||
private fun updateText(writer: BufferedWriter, id: Int, params: JsonObject) {
|
||||
val path = params.stringField("path")
|
||||
if (path == null) {
|
||||
writeLine(writer, errorJson(id, "BAD_REQUEST", "text update needs params.path"))
|
||||
return
|
||||
}
|
||||
val normalizedPath = normalizedPath(path)
|
||||
|
||||
val version = extractJsonInt(line, "version") ?: 0
|
||||
state.openTexts[normalizedPath] = extractJsonString(line, "text") ?: ""
|
||||
val version = params.intField("version") ?: 0
|
||||
state.openTexts[normalizedPath] = params.stringField("text") ?: ""
|
||||
state.textVersions[normalizedPath] = version
|
||||
writeLine(writer, okJson(id, "{\"path\":\"${escapeJson(normalizedPath)}\",\"version\":$version}"))
|
||||
writeLine(writer, okJson(id, buildJsonObject {
|
||||
put("path", normalizedPath)
|
||||
put("version", version)
|
||||
}))
|
||||
scheduleDiagnostics(writer, normalizedPath, version)
|
||||
}
|
||||
|
||||
private fun closeText(writer: BufferedWriter, id: Int, line: String) {
|
||||
val path = extractJsonString(line, "path")?.let(::normalizedPath)
|
||||
private fun closeText(writer: BufferedWriter, id: Int, params: JsonObject) {
|
||||
val path = params.stringField("path")?.let(::normalizedPath)
|
||||
if (path != null) state.openTexts.remove(path)
|
||||
if (path != null) state.textVersions.remove(path)
|
||||
if (path != null) state.diagnosticsVersions.remove(path)
|
||||
writeLine(writer, okJson(id, "{\"closed\":true}"))
|
||||
writeLine(writer, okJson(id, buildJsonObject { put("closed", true) }))
|
||||
}
|
||||
|
||||
private fun gradleTasksJson(): String {
|
||||
return "{\"tasks\":${tasksJson(state.gradleWorkspace?.tasks.orEmpty())}}"
|
||||
private fun gradleTasksJson(): JsonObject {
|
||||
return buildJsonObject { put("tasks", tasksJson(state.gradleWorkspace?.tasks.orEmpty())) }
|
||||
}
|
||||
|
||||
private fun runGradle(writer: BufferedWriter, id: Int, line: String) {
|
||||
val task = extractJsonString(line, "task") ?: extractJsonString(line, "path")
|
||||
private fun runGradle(writer: BufferedWriter, id: Int, params: JsonObject) {
|
||||
val task = params.stringField("task") ?: params.stringField("path")
|
||||
if (task == null) {
|
||||
writeLine(writer, errorJson(id, "BAD_REQUEST", "gradle/run needs params.task"))
|
||||
return
|
||||
|
|
@ -212,7 +229,7 @@ private class ClientSession(
|
|||
return
|
||||
}
|
||||
|
||||
writeLine(writer, eventJson("gradle/run", "{\"task\":\"${escapeJson(task)}\",\"state\":\"started\"}"))
|
||||
writeLine(writer, eventJson("gradle/run", gradleRunStateJson(task, "started")))
|
||||
try {
|
||||
GradleConnector.newConnector().forProjectDirectory(root).connect().use { connection ->
|
||||
connection.newBuild()
|
||||
|
|
@ -222,13 +239,16 @@ private class ClientSession(
|
|||
.run()
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
writeLine(writer, eventJson("gradle/run", "{\"task\":\"${escapeJson(task)}\",\"state\":\"failed\"}"))
|
||||
writeLine(writer, eventJson("gradle/run", gradleRunStateJson(task, "failed")))
|
||||
writeLine(writer, errorJson(id, "GRADLE_RUN_FAILED", t.message ?: t.javaClass.name))
|
||||
return
|
||||
}
|
||||
|
||||
writeLine(writer, eventJson("gradle/run", "{\"task\":\"${escapeJson(task)}\",\"state\":\"finished\"}"))
|
||||
writeLine(writer, okJson(id, "{\"task\":\"${escapeJson(task)}\",\"success\":true}"))
|
||||
writeLine(writer, eventJson("gradle/run", gradleRunStateJson(task, "finished")))
|
||||
writeLine(writer, okJson(id, buildJsonObject {
|
||||
put("task", task)
|
||||
put("success", true)
|
||||
}))
|
||||
}
|
||||
|
||||
private fun gradleOutputStream(writer: BufferedWriter, task: String, stream: String): OutputStream {
|
||||
|
|
@ -256,13 +276,18 @@ private class ClientSession(
|
|||
if (buffer.isEmpty()) return
|
||||
val text = buffer.toString()
|
||||
buffer.clear()
|
||||
writeLine(writer, eventJson("gradle/run", "{\"task\":\"${escapeJson(task)}\",\"state\":\"output\",\"stream\":\"$stream\",\"text\":\"${escapeJson(text)}\"}"))
|
||||
writeLine(writer, eventJson("gradle/run", buildJsonObject {
|
||||
put("task", task)
|
||||
put("state", "output")
|
||||
put("stream", stream)
|
||||
put("text", text)
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun kotlinDiagnostics(writer: BufferedWriter, id: Int, line: String) {
|
||||
val path = extractJsonString(line, "path")
|
||||
private fun kotlinDiagnostics(writer: BufferedWriter, id: Int, params: JsonObject) {
|
||||
val path = params.stringField("path")
|
||||
if (path == null) {
|
||||
writeLine(writer, errorJson(id, "BAD_REQUEST", "kotlin/diagnostics needs params.path"))
|
||||
return
|
||||
|
|
@ -284,49 +309,45 @@ private class ClientSession(
|
|||
}
|
||||
|
||||
val diagnostics = compileForDiagnostics(file, module, state.openTexts[normalizedPath])
|
||||
writeLine(writer, okJson(id, "{\"diagnostics\":${diagnosticsJson(diagnostics)}}"))
|
||||
writeLine(writer, okJson(id, buildJsonObject { put("diagnostics", diagnosticsJson(diagnostics)) }))
|
||||
}
|
||||
|
||||
private fun kotlinCompletion(writer: BufferedWriter, id: Int, line: String) {
|
||||
val path = extractJsonString(line, "path")
|
||||
private fun kotlinCompletion(writer: BufferedWriter, id: Int, params: JsonObject) {
|
||||
val path = params.stringField("path")
|
||||
if (path == null) {
|
||||
writeLine(writer, errorJson(id, "BAD_REQUEST", "kotlin/completion needs params.path"))
|
||||
return
|
||||
}
|
||||
|
||||
val requestLine = extractJsonInt(line, "line") ?: 1
|
||||
val requestColumn = extractJsonInt(line, "column") ?: 1
|
||||
val requestLine = params.intField("line") ?: 1
|
||||
val requestColumn = params.intField("column") ?: 1
|
||||
val normalizedPath = normalizedPath(path)
|
||||
val text = state.openTexts[normalizedPath] ?: runCatching { File(normalizedPath).readText() }.getOrDefault("")
|
||||
val offset = offsetForLineColumn(text, requestLine, requestColumn)
|
||||
val prefix = identifierPrefixAt(text, offset)
|
||||
val items = completionCandidates(prefix, text, state.gradleWorkspace)
|
||||
writeLine(writer, okJson(id, "{\"items\":${completionItemsJson(items)}}"))
|
||||
writeLine(writer, okJson(id, buildJsonObject { put("items", completionItemsJson(items)) }))
|
||||
}
|
||||
|
||||
private fun kotlinHover(writer: BufferedWriter, id: Int, line: String) {
|
||||
val path = extractJsonString(line, "path")
|
||||
private fun kotlinHover(writer: BufferedWriter, id: Int, params: JsonObject) {
|
||||
val path = params.stringField("path")
|
||||
if (path == null) {
|
||||
writeLine(writer, errorJson(id, "BAD_REQUEST", "kotlin/hover needs params.path"))
|
||||
return
|
||||
}
|
||||
|
||||
val requestLine = extractJsonInt(line, "line") ?: 1
|
||||
val requestColumn = extractJsonInt(line, "column") ?: 1
|
||||
val requestLine = params.intField("line") ?: 1
|
||||
val requestColumn = params.intField("column") ?: 1
|
||||
val normalizedPath = normalizedPath(path)
|
||||
val text = state.openTexts[normalizedPath] ?: runCatching { File(normalizedPath).readText() }.getOrDefault("")
|
||||
val offset = offsetForLineColumn(text, requestLine, requestColumn)
|
||||
val identifier = identifierAt(text, offset)
|
||||
val contents = hoverContents(identifier, normalizedPath, text, state.gradleWorkspace)
|
||||
if (contents == null) {
|
||||
writeLine(writer, okJson(id, "{\"contents\":null}"))
|
||||
} else {
|
||||
writeLine(writer, okJson(id, "{\"contents\":\"${escapeJson(contents)}\"}"))
|
||||
}
|
||||
writeLine(writer, okJson(id, buildJsonObject { put("contents", contents) }))
|
||||
}
|
||||
|
||||
private fun kotlinDefinition(writer: BufferedWriter, id: Int, line: String) {
|
||||
val path = extractJsonString(line, "path")
|
||||
private fun kotlinDefinition(writer: BufferedWriter, id: Int, params: JsonObject) {
|
||||
val path = params.stringField("path")
|
||||
if (path == null) {
|
||||
writeLine(writer, errorJson(id, "BAD_REQUEST", "kotlin/definition needs params.path"))
|
||||
return
|
||||
|
|
@ -338,19 +359,19 @@ private class ClientSession(
|
|||
return
|
||||
}
|
||||
|
||||
val requestLine = extractJsonInt(line, "line") ?: 1
|
||||
val requestColumn = extractJsonInt(line, "column") ?: 1
|
||||
val requestLine = params.intField("line") ?: 1
|
||||
val requestColumn = params.intField("column") ?: 1
|
||||
val normalizedPath = normalizedPath(path)
|
||||
val text = state.openTexts[normalizedPath] ?: runCatching { File(normalizedPath).readText() }.getOrDefault("")
|
||||
val offset = offsetForLineColumn(text, requestLine, requestColumn)
|
||||
val identifier = identifierAt(text, offset)
|
||||
val location = findSimpleDeclarationInText(normalizedPath, text, identifier)
|
||||
?: findSimpleDeclaration(workspace, identifier)
|
||||
writeLine(writer, okJson(id, "{\"locations\":${definitionLocationsJson(location)}}"))
|
||||
writeLine(writer, okJson(id, buildJsonObject { put("locations", definitionLocationsJson(location)) }))
|
||||
}
|
||||
|
||||
private fun kotlinReferences(writer: BufferedWriter, id: Int, line: String) {
|
||||
val path = extractJsonString(line, "path")
|
||||
private fun kotlinReferences(writer: BufferedWriter, id: Int, params: JsonObject) {
|
||||
val path = params.stringField("path")
|
||||
if (path == null) {
|
||||
writeLine(writer, errorJson(id, "BAD_REQUEST", "kotlin/references needs params.path"))
|
||||
return
|
||||
|
|
@ -362,24 +383,24 @@ private class ClientSession(
|
|||
return
|
||||
}
|
||||
|
||||
val requestLine = extractJsonInt(line, "line") ?: 1
|
||||
val requestColumn = extractJsonInt(line, "column") ?: 1
|
||||
val requestLine = params.intField("line") ?: 1
|
||||
val requestColumn = params.intField("column") ?: 1
|
||||
val normalizedPath = normalizedPath(path)
|
||||
val text = state.openTexts[normalizedPath] ?: runCatching { File(normalizedPath).readText() }.getOrDefault("")
|
||||
val offset = offsetForLineColumn(text, requestLine, requestColumn)
|
||||
val identifier = identifierAt(text, offset)
|
||||
val locations = findSimpleReferences(workspace, normalizedPath, text, identifier)
|
||||
writeLine(writer, okJson(id, "{\"locations\":${locationsJson(locations)}}"))
|
||||
writeLine(writer, okJson(id, buildJsonObject { put("locations", locationsJson(locations)) }))
|
||||
}
|
||||
|
||||
private fun kotlinRename(writer: BufferedWriter, id: Int, line: String) {
|
||||
val path = extractJsonString(line, "path")
|
||||
private fun kotlinRename(writer: BufferedWriter, id: Int, params: JsonObject) {
|
||||
val path = params.stringField("path")
|
||||
if (path == null) {
|
||||
writeLine(writer, errorJson(id, "BAD_REQUEST", "kotlin/rename needs params.path"))
|
||||
return
|
||||
}
|
||||
|
||||
val newName = extractJsonString(line, "newName")
|
||||
val newName = params.stringField("newName")
|
||||
if (newName == null || !isValidIdentifier(newName)) {
|
||||
writeLine(writer, errorJson(id, "BAD_REQUEST", "kotlin/rename needs valid params.newName"))
|
||||
return
|
||||
|
|
@ -391,19 +412,27 @@ private class ClientSession(
|
|||
return
|
||||
}
|
||||
|
||||
val requestLine = extractJsonInt(line, "line") ?: 1
|
||||
val requestColumn = extractJsonInt(line, "column") ?: 1
|
||||
val requestLine = params.intField("line") ?: 1
|
||||
val requestColumn = params.intField("column") ?: 1
|
||||
val normalizedPath = normalizedPath(path)
|
||||
val text = state.openTexts[normalizedPath] ?: runCatching { File(normalizedPath).readText() }.getOrDefault("")
|
||||
val offset = offsetForLineColumn(text, requestLine, requestColumn)
|
||||
val identifier = identifierAt(text, offset)
|
||||
if (identifier.isBlank() || identifier in allKeywordCompletionItems) {
|
||||
writeLine(writer, okJson(id, "{\"applied\":false,\"edits\":[]}"))
|
||||
writeLine(writer, okJson(id, buildJsonObject {
|
||||
put("applied", false)
|
||||
putJsonArray("edits") {}
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
val locations = findSimpleReferences(workspace, normalizedPath, text, identifier)
|
||||
writeLine(writer, okJson(id, "{\"applied\":false,\"oldName\":\"${escapeJson(identifier)}\",\"newName\":\"${escapeJson(newName)}\",\"edits\":${renameEditsJson(locations, identifier, newName)}}"))
|
||||
writeLine(writer, okJson(id, buildJsonObject {
|
||||
put("applied", false)
|
||||
put("oldName", identifier)
|
||||
put("newName", newName)
|
||||
put("edits", renameEditsJson(locations, identifier, newName))
|
||||
}))
|
||||
}
|
||||
|
||||
private fun scheduleDiagnostics(writer: BufferedWriter, path: String, version: Int) {
|
||||
|
|
@ -427,7 +456,11 @@ private class ClientSession(
|
|||
writer,
|
||||
eventJson(
|
||||
"diagnostics/publish",
|
||||
"{\"path\":\"${escapeJson(file.path)}\",\"version\":$version,\"diagnostics\":${diagnosticsJson(diagnostics)}}",
|
||||
buildJsonObject {
|
||||
put("path", file.path)
|
||||
put("version", version)
|
||||
put("diagnostics", diagnosticsJson(diagnostics))
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -577,32 +610,32 @@ private fun collectTasks(project: GradleProject): List<GradleTaskInfo> {
|
|||
return tasks.sortedWith(compareBy<GradleTaskInfo> { it.path }.thenBy { it.name })
|
||||
}
|
||||
|
||||
private fun workspaceJson(workspace: GradleWorkspace): String {
|
||||
return buildString {
|
||||
append("{\"root\":\"").append(escapeJson(workspace.root.path)).append("\"")
|
||||
append(",\"gradle\":true")
|
||||
append(",\"modules\":[")
|
||||
workspace.modules.forEachIndexed { index, module ->
|
||||
if (index > 0) append(',')
|
||||
append(moduleJson(module))
|
||||
}
|
||||
append(']')
|
||||
append(",\"sourceRoots\":").append(filesJson(workspace.modules.flatMap { it.sourceRoots + it.testSourceRoots }))
|
||||
append(",\"tasks\":").append(tasksJson(workspace.tasks))
|
||||
append('}')
|
||||
}
|
||||
private fun indexingStateJson(state: String): JsonObject = buildJsonObject { put("state", state) }
|
||||
|
||||
private fun gradleRunStateJson(task: String, state: String): JsonObject = buildJsonObject {
|
||||
put("task", task)
|
||||
put("state", state)
|
||||
}
|
||||
|
||||
private fun moduleJson(module: GradleModule): String = buildString {
|
||||
append("{\"name\":\"").append(escapeJson(module.name)).append("\"")
|
||||
append(",\"gradlePath\":\"").append(escapeJson(module.gradlePath)).append("\"")
|
||||
append(",\"directory\":\"").append(escapeJson(module.directory.path)).append("\"")
|
||||
append(",\"sourceRoots\":").append(filesJson(module.sourceRoots))
|
||||
append(",\"testSourceRoots\":").append(filesJson(module.testSourceRoots))
|
||||
append(",\"resourceRoots\":").append(filesJson(module.resourceRoots))
|
||||
append(",\"testResourceRoots\":").append(filesJson(module.testResourceRoots))
|
||||
append(",\"classpath\":").append(filesJson(module.classpath))
|
||||
append('}')
|
||||
private fun workspaceJson(workspace: GradleWorkspace): JsonObject = buildJsonObject {
|
||||
put("root", workspace.root.path)
|
||||
put("gradle", true)
|
||||
putJsonArray("modules") {
|
||||
workspace.modules.forEach { add(moduleJson(it)) }
|
||||
}
|
||||
put("sourceRoots", filesJson(workspace.modules.flatMap { it.sourceRoots + it.testSourceRoots }))
|
||||
put("tasks", tasksJson(workspace.tasks))
|
||||
}
|
||||
|
||||
private fun moduleJson(module: GradleModule): JsonObject = buildJsonObject {
|
||||
put("name", module.name)
|
||||
put("gradlePath", module.gradlePath)
|
||||
put("directory", module.directory.path)
|
||||
put("sourceRoots", filesJson(module.sourceRoots))
|
||||
put("testSourceRoots", filesJson(module.testSourceRoots))
|
||||
put("resourceRoots", filesJson(module.resourceRoots))
|
||||
put("testResourceRoots", filesJson(module.testResourceRoots))
|
||||
put("classpath", filesJson(module.classpath))
|
||||
}
|
||||
|
||||
private fun compileForDiagnostics(file: File, module: GradleModule, openText: String?): List<KotlinDiagnostic> {
|
||||
|
|
@ -1017,90 +1050,65 @@ private fun sourceLine(text: String, oneBasedLine: Int): String {
|
|||
return if (line == oneBasedLine) text.substring(start) else ""
|
||||
}
|
||||
|
||||
private fun completionItemsJson(items: List<CompletionCandidate>): String = buildString {
|
||||
append('[')
|
||||
items.forEachIndexed { index, item ->
|
||||
if (index > 0) append(',')
|
||||
append("{\"label\":\"").append(escapeJson(item.label)).append("\",\"kind\":\"").append(escapeJson(item.kind)).append("\"}")
|
||||
private fun completionItemsJson(items: List<CompletionCandidate>): JsonElement = buildJsonArray {
|
||||
items.forEach { item ->
|
||||
add(buildJsonObject {
|
||||
put("label", item.label)
|
||||
put("kind", item.kind)
|
||||
})
|
||||
}
|
||||
append(']')
|
||||
}
|
||||
|
||||
private fun definitionLocationsJson(location: SourceLocation?): String = buildString {
|
||||
append('[')
|
||||
if (location != null) {
|
||||
append("{\"path\":\"").append(escapeJson(location.path)).append("\"")
|
||||
append(",\"line\":").append(location.line)
|
||||
append(",\"column\":").append(location.column)
|
||||
append('}')
|
||||
}
|
||||
append(']')
|
||||
private fun locationJson(location: SourceLocation): JsonObject = buildJsonObject {
|
||||
put("path", location.path)
|
||||
put("line", location.line)
|
||||
put("column", location.column)
|
||||
}
|
||||
|
||||
private fun locationsJson(locations: List<SourceLocation>): String = buildString {
|
||||
append('[')
|
||||
locations.forEachIndexed { index, location ->
|
||||
if (index > 0) append(',')
|
||||
append("{\"path\":\"").append(escapeJson(location.path)).append("\"")
|
||||
append(",\"line\":").append(location.line)
|
||||
append(",\"column\":").append(location.column)
|
||||
append('}')
|
||||
}
|
||||
append(']')
|
||||
private fun definitionLocationsJson(location: SourceLocation?): JsonElement = buildJsonArray {
|
||||
if (location != null) add(locationJson(location))
|
||||
}
|
||||
|
||||
private fun renameEditsJson(locations: List<SourceLocation>, oldName: String, newName: String): String = buildString {
|
||||
append('[')
|
||||
locations.forEachIndexed { index, location ->
|
||||
if (index > 0) append(',')
|
||||
append("{\"path\":\"").append(escapeJson(location.path)).append("\"")
|
||||
append(",\"line\":").append(location.line)
|
||||
append(",\"column\":").append(location.column)
|
||||
append(",\"oldText\":\"").append(escapeJson(oldName)).append("\"")
|
||||
append(",\"newText\":\"").append(escapeJson(newName)).append("\"}")
|
||||
}
|
||||
append(']')
|
||||
private fun locationsJson(locations: List<SourceLocation>): JsonElement = buildJsonArray {
|
||||
locations.forEach { add(locationJson(it)) }
|
||||
}
|
||||
|
||||
private fun diagnosticsJson(diagnostics: List<KotlinDiagnostic>): String = buildString {
|
||||
append('[')
|
||||
diagnostics.forEachIndexed { index, diagnostic ->
|
||||
if (index > 0) append(',')
|
||||
append("{\"severity\":\"").append(escapeJson(diagnostic.severity)).append("\"")
|
||||
append(",\"message\":\"").append(escapeJson(diagnostic.message)).append("\"")
|
||||
append(",\"path\":")
|
||||
if (diagnostic.path == null) append("null") else append('\"').append(escapeJson(diagnostic.path)).append('\"')
|
||||
append(",\"line\":").append(diagnostic.line ?: "null")
|
||||
append(",\"column\":").append(diagnostic.column ?: "null")
|
||||
append('}')
|
||||
private fun renameEditsJson(locations: List<SourceLocation>, oldName: String, newName: String): JsonElement = buildJsonArray {
|
||||
locations.forEach { location ->
|
||||
add(buildJsonObject {
|
||||
put("path", location.path)
|
||||
put("line", location.line)
|
||||
put("column", location.column)
|
||||
put("oldText", oldName)
|
||||
put("newText", newName)
|
||||
})
|
||||
}
|
||||
append(']')
|
||||
}
|
||||
|
||||
private fun filesJson(files: List<File>): String = buildString {
|
||||
append('[')
|
||||
files.distinctBy { it.path }.forEachIndexed { index, file ->
|
||||
if (index > 0) append(',')
|
||||
append('\"').append(escapeJson(file.path)).append('\"')
|
||||
private fun diagnosticsJson(diagnostics: List<KotlinDiagnostic>): JsonElement = buildJsonArray {
|
||||
diagnostics.forEach { diagnostic ->
|
||||
add(buildJsonObject {
|
||||
put("severity", diagnostic.severity)
|
||||
put("message", diagnostic.message)
|
||||
put("path", diagnostic.path)
|
||||
put("line", diagnostic.line)
|
||||
put("column", diagnostic.column)
|
||||
})
|
||||
}
|
||||
append(']')
|
||||
}
|
||||
|
||||
private fun tasksJson(tasks: List<GradleTaskInfo>): String = buildString {
|
||||
append('[')
|
||||
tasks.forEachIndexed { index, task ->
|
||||
if (index > 0) append(',')
|
||||
append("{\"path\":\"").append(escapeJson(task.path)).append("\"")
|
||||
append(",\"name\":\"").append(escapeJson(task.name)).append("\"")
|
||||
append(",\"description\":")
|
||||
if (task.description == null) {
|
||||
append("null")
|
||||
} else {
|
||||
append('\"').append(escapeJson(task.description)).append('\"')
|
||||
private fun filesJson(files: List<File>): JsonElement = buildJsonArray {
|
||||
files.distinctBy { it.path }.forEach { add(it.path) }
|
||||
}
|
||||
|
||||
private fun tasksJson(tasks: List<GradleTaskInfo>): JsonElement = buildJsonArray {
|
||||
tasks.forEach { task ->
|
||||
add(buildJsonObject {
|
||||
put("path", task.path)
|
||||
put("name", task.name)
|
||||
put("description", task.description)
|
||||
})
|
||||
}
|
||||
append('}')
|
||||
}
|
||||
append(']')
|
||||
}
|
||||
|
||||
private fun writeLineLocked(writer: BufferedWriter, text: String) {
|
||||
|
|
@ -1109,80 +1117,29 @@ private fun writeLineLocked(writer: BufferedWriter, text: String) {
|
|||
writer.flush()
|
||||
}
|
||||
|
||||
private fun okJson(id: Int, result: String): String = "{\"id\":$id,\"ok\":true,\"result\":$result}"
|
||||
private fun okJson(id: Int, result: JsonElement): String = buildJsonObject {
|
||||
put("id", id)
|
||||
put("ok", true)
|
||||
put("result", result)
|
||||
}.toString()
|
||||
|
||||
private fun errorJson(id: Int, code: String, message: String): String =
|
||||
"{\"id\":$id,\"ok\":false,\"error\":{\"code\":\"${escapeJson(code)}\",\"message\":\"${escapeJson(message)}\"}}"
|
||||
|
||||
private fun eventJson(event: String, params: String): String =
|
||||
"{\"event\":\"${escapeJson(event)}\",\"params\":$params}"
|
||||
|
||||
private fun extractJsonString(json: String, key: String): String? {
|
||||
val marker = "\"$key\""
|
||||
val keyIndex = json.indexOf(marker)
|
||||
if (keyIndex < 0) return null
|
||||
val colon = json.indexOf(':', keyIndex + marker.length)
|
||||
if (colon < 0) return null
|
||||
var index = colon + 1
|
||||
while (index < json.length && json[index].isWhitespace()) index++
|
||||
if (index >= json.length || json[index] != '\"') return null
|
||||
return readJsonString(json, index)
|
||||
}
|
||||
|
||||
private fun extractJsonInt(json: String, key: String): Int? {
|
||||
val marker = "\"$key\""
|
||||
val keyIndex = json.indexOf(marker)
|
||||
if (keyIndex < 0) return null
|
||||
val colon = json.indexOf(':', keyIndex + marker.length)
|
||||
if (colon < 0) return null
|
||||
var index = colon + 1
|
||||
while (index < json.length && json[index].isWhitespace()) index++
|
||||
val start = index
|
||||
if (index < json.length && json[index] == '-') index++
|
||||
while (index < json.length && json[index].isDigit()) index++
|
||||
if (index == start) return null
|
||||
return json.substring(start, index).toIntOrNull()
|
||||
}
|
||||
|
||||
private fun readJsonString(json: String, quoteIndex: Int): String? {
|
||||
val out = StringBuilder()
|
||||
var index = quoteIndex + 1
|
||||
while (index < json.length) {
|
||||
val ch = json[index]
|
||||
if (ch == '\"') return out.toString()
|
||||
if (ch == '\\') {
|
||||
index++
|
||||
if (index >= json.length) return null
|
||||
out.append(
|
||||
when (val escaped = json[index]) {
|
||||
'\"' -> '\"'
|
||||
'\\' -> '\\'
|
||||
'/' -> '/'
|
||||
'b' -> '\b'
|
||||
'f' -> '\u000C'
|
||||
'n' -> '\n'
|
||||
'r' -> '\r'
|
||||
't' -> '\t'
|
||||
else -> escaped
|
||||
private fun errorJson(id: Int, code: String, message: String): String = buildJsonObject {
|
||||
put("id", id)
|
||||
put("ok", false)
|
||||
putJsonObject("error") {
|
||||
put("code", code)
|
||||
put("message", message)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
out.append(ch)
|
||||
}
|
||||
index++
|
||||
}
|
||||
return null
|
||||
}
|
||||
}.toString()
|
||||
|
||||
private fun eventJson(event: String, params: JsonElement): String = buildJsonObject {
|
||||
put("event", event)
|
||||
put("params", params)
|
||||
}.toString()
|
||||
|
||||
private fun JsonObject.stringField(key: String): String? =
|
||||
(this[key] as? JsonPrimitive)?.takeIf { it.isString }?.content
|
||||
|
||||
private fun JsonObject.intField(key: String): Int? =
|
||||
(this[key] as? JsonPrimitive)?.takeIf { !it.isString }?.intOrNull
|
||||
|
||||
private fun escapeJson(value: String): String = buildString {
|
||||
value.forEach { ch ->
|
||||
when (ch) {
|
||||
'\\' -> append("\\\\")
|
||||
'\"' -> append("\\\"")
|
||||
'\n' -> append("\\n")
|
||||
'\r' -> append("\\r")
|
||||
'\t' -> append("\\t")
|
||||
else -> append(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue