722 lines
22 KiB
Odin
722 lines
22 KiB
Odin
package main
|
|
|
|
import "core:fmt"
|
|
import "core:net"
|
|
import "core:os"
|
|
import "core:strconv"
|
|
import "core:strings"
|
|
import "core:sync"
|
|
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,
|
|
Event,
|
|
}
|
|
|
|
Protocol_Message :: struct {
|
|
kind: Protocol_Message_Kind,
|
|
id: int,
|
|
ok: bool,
|
|
event: string,
|
|
}
|
|
|
|
Daemon_Response :: struct {
|
|
id: int,
|
|
line: [dynamic]u8,
|
|
}
|
|
|
|
Daemon_Event :: struct {
|
|
line: [dynamic]u8,
|
|
}
|
|
|
|
Daemon_Process :: struct {
|
|
process: os.Process,
|
|
stdout: ^os.File,
|
|
running: bool,
|
|
}
|
|
|
|
Daemon_Client :: struct {
|
|
socket: net.TCP_Socket,
|
|
connected: bool,
|
|
next_id: int,
|
|
reader: ^thread.Thread,
|
|
event_mutex: sync.Mutex,
|
|
response_mutex: sync.Mutex,
|
|
diagnostic_events: [dynamic]Daemon_Event,
|
|
gradle_events: [dynamic]Daemon_Event,
|
|
workspace_response_id: int,
|
|
pending_response_ids: [dynamic]int,
|
|
responses: [dynamic]Daemon_Response,
|
|
}
|
|
|
|
daemon_connect :: proc(port: int) -> Daemon_Client {
|
|
client := Daemon_Client{next_id = 1}
|
|
if port <= 0 do return client
|
|
|
|
socket, err := net.dial_tcp("127.0.0.1", port)
|
|
if err != nil {
|
|
fmt.println("daemon connect failed:", err)
|
|
return client
|
|
}
|
|
|
|
client.socket = socket
|
|
client.connected = true
|
|
return client
|
|
}
|
|
|
|
daemon_start_process :: proc(workspace: string) -> (Daemon_Process, int, bool) {
|
|
child, ok := daemon_start_process_begin(workspace)
|
|
if !ok {
|
|
return child, 0, false
|
|
}
|
|
|
|
port, port_ok := daemon_read_port(&child)
|
|
if !port_ok {
|
|
daemon_stop_process(&child)
|
|
return child, 0, false
|
|
}
|
|
return child, port, true
|
|
}
|
|
|
|
// The daemon is its own Gradle project; the workspace is sent later over the
|
|
// protocol. Locate the daemon project relative to the environment or the
|
|
// editor executable so any folder can be opened as a workspace.
|
|
daemon_project_dir :: proc() -> (string, bool) {
|
|
env := os.get_env("NATIVE_EDITOR_DAEMON_DIR", context.temp_allocator)
|
|
if len(env) > 0 && os.is_dir(env) {
|
|
return env, true
|
|
}
|
|
if os.is_file("daemon/build.gradle.kts") {
|
|
return "daemon", true
|
|
}
|
|
exe, exe_err := os.get_executable_path(context.temp_allocator)
|
|
if exe_err == nil {
|
|
dir := exe
|
|
for _ in 0 ..< 5 {
|
|
parent, _ := os.split_path(dir)
|
|
if len(parent) == 0 || parent == dir do break
|
|
dir = strings.trim_suffix(parent, "/")
|
|
if len(dir) == 0 do break
|
|
if os.is_file(fmt.tprintf("%s/daemon/build.gradle.kts", dir)) {
|
|
return fmt.tprintf("%s/daemon", dir), true
|
|
}
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
daemon_start_process_begin :: proc(workspace: string) -> (Daemon_Process, bool) {
|
|
child := Daemon_Process{}
|
|
|
|
project_dir, found := daemon_project_dir()
|
|
if !found {
|
|
fmt.println("daemon project not found; set NATIVE_EDITOR_DAEMON_DIR to the daemon directory")
|
|
return child, false
|
|
}
|
|
|
|
stdout_r, stdout_w, pipe_err := os.pipe()
|
|
if pipe_err != nil {
|
|
fmt.println("daemon pipe failed:", pipe_err)
|
|
return child, false
|
|
}
|
|
|
|
command := []string{"gradle", "-p", project_dir, "run", "--quiet"}
|
|
process, start_err := os.process_start(os.Process_Desc{
|
|
command = command,
|
|
stdout = stdout_w,
|
|
})
|
|
_ = os.close(stdout_w)
|
|
|
|
if start_err != nil {
|
|
fmt.println("daemon start failed:", start_err)
|
|
_ = os.close(stdout_r)
|
|
return child, false
|
|
}
|
|
|
|
child = Daemon_Process{process = process, stdout = stdout_r, running = true}
|
|
return child, true
|
|
}
|
|
|
|
daemon_stop_process :: proc(child: ^Daemon_Process) {
|
|
if child.stdout != nil {
|
|
_ = os.close(child.stdout)
|
|
child.stdout = nil
|
|
}
|
|
if child.running {
|
|
_ = os.process_terminate(child.process)
|
|
state, wait_err := os.process_wait(child.process, 2 * time.Second)
|
|
if wait_err != nil || !state.exited {
|
|
_ = os.process_kill(child.process)
|
|
_, _ = os.process_wait(child.process, 500 * time.Millisecond)
|
|
}
|
|
child.running = false
|
|
}
|
|
}
|
|
|
|
daemon_read_port :: proc(child: ^Daemon_Process) -> (int, bool) {
|
|
line: [dynamic]u8
|
|
defer delete(line)
|
|
|
|
start := time.now()
|
|
buf: [1024]u8
|
|
for time.diff(start, time.now()) < 30 * time.Second {
|
|
state, wait_err := os.process_wait(child.process, 0)
|
|
if wait_err == nil && state.exited {
|
|
fmt.println("daemon exited before announcing port")
|
|
return 0, false
|
|
}
|
|
|
|
has_data, data_err := os.pipe_has_data(child.stdout)
|
|
if data_err != nil {
|
|
fmt.println("daemon stdout failed:", data_err)
|
|
return 0, false
|
|
}
|
|
if !has_data {
|
|
time.sleep(100 * time.Millisecond)
|
|
continue
|
|
}
|
|
|
|
n, read_err := os.read(child.stdout, buf[:])
|
|
if read_err != nil {
|
|
fmt.println("daemon stdout read failed:", read_err)
|
|
return 0, false
|
|
}
|
|
|
|
for b in buf[:n] {
|
|
if b == '\n' {
|
|
port, ok := daemon_parse_port_line(string(line[:]))
|
|
if ok do return port, true
|
|
clear(&line)
|
|
} else {
|
|
append(&line, b)
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.println("daemon start timed out")
|
|
return 0, false
|
|
}
|
|
|
|
daemon_poll_port :: proc(child: ^Daemon_Process, line: ^[dynamic]u8) -> (int, bool, bool) {
|
|
if child == nil || !child.running || child.stdout == nil do return 0, false, true
|
|
|
|
state, wait_err := os.process_wait(child.process, 0)
|
|
if wait_err == nil && state.exited {
|
|
fmt.println("daemon exited before announcing port")
|
|
return 0, false, true
|
|
}
|
|
|
|
has_data, data_err := os.pipe_has_data(child.stdout)
|
|
if data_err != nil {
|
|
fmt.println("daemon stdout failed:", data_err)
|
|
return 0, false, true
|
|
}
|
|
if !has_data do return 0, false, false
|
|
|
|
buf: [1024]u8
|
|
n, read_err := os.read(child.stdout, buf[:])
|
|
if read_err != nil {
|
|
fmt.println("daemon stdout read failed:", read_err)
|
|
return 0, false, true
|
|
}
|
|
|
|
for b in buf[:n] {
|
|
if b == '\n' {
|
|
port, ok := daemon_parse_port_line(string(line[:]))
|
|
clear(line)
|
|
if ok do return port, true, false
|
|
} else {
|
|
append(line, b)
|
|
}
|
|
}
|
|
|
|
return 0, false, false
|
|
}
|
|
|
|
daemon_parse_port_line :: proc(line: string) -> (int, bool) {
|
|
if !strings.has_prefix(line, "PORT ") do return 0, false
|
|
port, ok := strconv.parse_int(line[len("PORT "):])
|
|
if !ok do return 0, false
|
|
return int(port), true
|
|
}
|
|
|
|
daemon_start_reader :: proc(client: ^Daemon_Client) {
|
|
if client.connected && client.reader == nil {
|
|
client.reader = thread.create_and_start_with_data(rawptr(client), daemon_reader_thread)
|
|
}
|
|
}
|
|
|
|
daemon_close :: proc(client: ^Daemon_Client) {
|
|
if client.connected {
|
|
net.close(client.socket)
|
|
client.connected = false
|
|
}
|
|
if client.reader != nil {
|
|
thread.join(client.reader)
|
|
thread.destroy(client.reader)
|
|
client.reader = nil
|
|
}
|
|
for &event in client.diagnostic_events {
|
|
delete(event.line)
|
|
}
|
|
delete(client.diagnostic_events)
|
|
client.diagnostic_events = nil
|
|
for &event in client.gradle_events {
|
|
delete(event.line)
|
|
}
|
|
delete(client.gradle_events)
|
|
client.gradle_events = nil
|
|
delete(client.pending_response_ids)
|
|
client.pending_response_ids = nil
|
|
for &response in client.responses {
|
|
delete(response.line)
|
|
}
|
|
delete(client.responses)
|
|
client.responses = nil
|
|
}
|
|
|
|
daemon_send :: proc(client: ^Daemon_Client, message: string) {
|
|
if !client.connected do return
|
|
bytes := transmute([]byte)message
|
|
_, err := net.send_tcp(client.socket, bytes)
|
|
if err != nil {
|
|
fmt.println("daemon send failed:", err)
|
|
daemon_close(client)
|
|
}
|
|
}
|
|
|
|
daemon_track_response :: proc(client: ^Daemon_Client, id: int) {
|
|
if id == 0 do return
|
|
if sync.mutex_guard(&client.response_mutex) {
|
|
append(&client.pending_response_ids, id)
|
|
}
|
|
}
|
|
|
|
daemon_send_workspace_open :: proc(client: ^Daemon_Client, workspace: string) {
|
|
if !client.connected do return
|
|
id := client.next_id
|
|
client.next_id += 1
|
|
client.workspace_response_id = id
|
|
daemon_track_response(client, id)
|
|
request := fmt.tprintf("{{\"id\":%d,\"method\":\"workspace/open\",\"params\":{{\"root\":%s}}}}\n", id, json_quote(workspace))
|
|
daemon_send(client, request)
|
|
}
|
|
|
|
daemon_send_text_open :: proc(client: ^Daemon_Client, path: string, version: int, text: string) {
|
|
if !client.connected do return
|
|
id := client.next_id
|
|
client.next_id += 1
|
|
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)
|
|
}
|
|
|
|
daemon_send_text_change :: proc(client: ^Daemon_Client, path: string, version: int, text: string) {
|
|
if !client.connected do return
|
|
id := client.next_id
|
|
client.next_id += 1
|
|
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)
|
|
}
|
|
|
|
daemon_send_completion :: proc(client: ^Daemon_Client, path: string, line, column: int) -> int {
|
|
if !client.connected do return 0
|
|
id := client.next_id
|
|
client.next_id += 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
|
|
}
|
|
|
|
daemon_send_hover :: proc(client: ^Daemon_Client, path: string, line, column: int) -> int {
|
|
if !client.connected do return 0
|
|
id := client.next_id
|
|
client.next_id += 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
|
|
}
|
|
|
|
daemon_send_definition :: proc(client: ^Daemon_Client, path: string, line, column: int) -> int {
|
|
if !client.connected do return 0
|
|
id := client.next_id
|
|
client.next_id += 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
|
|
}
|
|
|
|
daemon_send_references :: proc(client: ^Daemon_Client, path: string, line, column: int) -> int {
|
|
if !client.connected do return 0
|
|
id := client.next_id
|
|
client.next_id += 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
|
|
}
|
|
|
|
daemon_send_rename :: proc(client: ^Daemon_Client, path: string, line, column: int, new_name: string) -> int {
|
|
if !client.connected do return 0
|
|
id := client.next_id
|
|
client.next_id += 1
|
|
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
|
|
}
|
|
|
|
daemon_send_gradle_tasks :: proc(client: ^Daemon_Client) -> int {
|
|
if !client.connected do return 0
|
|
id := client.next_id
|
|
client.next_id += 1
|
|
request := fmt.tprintf("{{\"id\":%d,\"method\":\"gradle/tasks\",\"params\":{{}}}}\n", id)
|
|
daemon_track_response(client, id)
|
|
daemon_send(client, request)
|
|
return id
|
|
}
|
|
|
|
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\":%s}}}}\n", id, json_quote(task))
|
|
daemon_track_response(client, id)
|
|
daemon_send(client, request)
|
|
return id
|
|
}
|
|
|
|
daemon_sync_active_buffer :: proc(client: ^Daemon_Client, editor: ^Editor, open: bool) {
|
|
active := editor_active_buffer(editor)
|
|
if active == nil || active.scratch do return
|
|
|
|
text := editor_active_text(editor)
|
|
defer delete(text)
|
|
|
|
if open {
|
|
daemon_send_text_open(client, active.path, active.buffer.version, string(text[:]))
|
|
} else {
|
|
daemon_send_text_change(client, active.path, active.buffer.version, string(text[:]))
|
|
}
|
|
}
|
|
|
|
daemon_reader_thread :: proc(data: rawptr) {
|
|
client := (^Daemon_Client)(data)
|
|
buf: [4096]u8
|
|
line: [dynamic]u8
|
|
defer delete(line)
|
|
|
|
for client.connected {
|
|
n, err := net.recv_tcp(client.socket, buf[:])
|
|
if err != nil || n == 0 {
|
|
client.connected = false
|
|
break
|
|
}
|
|
|
|
for b in buf[:n] {
|
|
if b == '\n' {
|
|
daemon_store_line(client, string(line[:]))
|
|
clear(&line)
|
|
} else {
|
|
append(&line, b)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
daemon_store_line :: proc(client: ^Daemon_Client, line: string) {
|
|
message, value, ok := parse_protocol_message(line)
|
|
if !ok do return
|
|
defer json.destroy_value(value)
|
|
if message.kind == .Response {
|
|
if sync.mutex_guard(&client.response_mutex) {
|
|
tracked := false
|
|
for pending_id, index in client.pending_response_ids {
|
|
if pending_id == message.id {
|
|
ordered_remove(&client.pending_response_ids, index)
|
|
tracked = true
|
|
break
|
|
}
|
|
}
|
|
if !tracked do return
|
|
|
|
response := Daemon_Response{id = message.id}
|
|
for b in transmute([]u8)line {
|
|
append(&response.line, b)
|
|
}
|
|
append(&client.responses, response)
|
|
}
|
|
return
|
|
}
|
|
if message.kind != .Event do return
|
|
|
|
if message.event == "gradle/run" {
|
|
if sync.mutex_guard(&client.event_mutex) {
|
|
event := Daemon_Event{}
|
|
for b in transmute([]u8)line {
|
|
append(&event.line, b)
|
|
}
|
|
append(&client.gradle_events, event)
|
|
}
|
|
return
|
|
}
|
|
|
|
if message.event != "diagnostics/publish" do return
|
|
|
|
if sync.mutex_guard(&client.event_mutex) {
|
|
event := Daemon_Event{}
|
|
for b in transmute([]u8)line {
|
|
append(&event.line, b)
|
|
}
|
|
append(&client.diagnostic_events, event)
|
|
}
|
|
}
|
|
|
|
daemon_take_response :: proc(client: ^Daemon_Client, expected_id: int) -> [dynamic]u8 {
|
|
out: [dynamic]u8
|
|
if expected_id == 0 do return out
|
|
|
|
if sync.mutex_guard(&client.response_mutex) {
|
|
for &response, index in client.responses {
|
|
if response.id != expected_id do continue
|
|
for b in response.line {
|
|
append(&out, b)
|
|
}
|
|
delete(response.line)
|
|
ordered_remove(&client.responses, index)
|
|
break
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
daemon_take_diagnostic_event :: proc(client: ^Daemon_Client) -> [dynamic]u8 {
|
|
out: [dynamic]u8
|
|
if !client.connected && len(client.diagnostic_events) == 0 do return out
|
|
|
|
if sync.mutex_guard(&client.event_mutex) {
|
|
if len(client.diagnostic_events) == 0 do return out
|
|
event := client.diagnostic_events[0]
|
|
for b in event.line {
|
|
append(&out, b)
|
|
}
|
|
delete(event.line)
|
|
ordered_remove(&client.diagnostic_events, 0)
|
|
}
|
|
return out
|
|
}
|
|
|
|
daemon_take_gradle_event :: proc(client: ^Daemon_Client) -> [dynamic]u8 {
|
|
out: [dynamic]u8
|
|
if !client.connected && len(client.gradle_events) == 0 do return out
|
|
|
|
if sync.mutex_guard(&client.event_mutex) {
|
|
if len(client.gradle_events) == 0 do return out
|
|
event := client.gradle_events[0]
|
|
for b in event.line {
|
|
append(&out, b)
|
|
}
|
|
delete(event.line)
|
|
ordered_remove(&client.gradle_events, 0)
|
|
}
|
|
return out
|
|
}
|
|
|
|
daemon_apply_latest_diagnostics :: proc(client: ^Daemon_Client, editor: ^Editor) {
|
|
for {
|
|
event := daemon_take_diagnostic_event(client)
|
|
if len(event) == 0 {
|
|
delete(event)
|
|
return
|
|
}
|
|
|
|
path, diagnostics := parse_diagnostics_event(string(event[:]))
|
|
if len(path) == 0 || !editor_set_diagnostics_for_path(editor, path, diagnostics[:]) {
|
|
diagnostics_destroy(diagnostics[:])
|
|
}
|
|
delete(path)
|
|
delete(diagnostics)
|
|
delete(event)
|
|
}
|
|
}
|
|
|
|
daemon_apply_workspace_response :: proc(client: ^Daemon_Client, editor: ^Editor) {
|
|
if client.workspace_response_id == 0 do return
|
|
|
|
response := daemon_take_response(client, client.workspace_response_id)
|
|
defer delete(response)
|
|
if len(response) == 0 do return
|
|
|
|
message, value, ok := parse_protocol_message(string(response[:]))
|
|
if !ok {
|
|
client.workspace_response_id = 0
|
|
return
|
|
}
|
|
defer json.destroy_value(value)
|
|
if message.kind != .Response || message.id != client.workspace_response_id do return
|
|
|
|
client.workspace_response_id = 0
|
|
if message.ok {
|
|
editor_set_status(editor, "Kotlin daemon workspace ready")
|
|
return
|
|
}
|
|
|
|
error_message := protocol_error_message(value)
|
|
defer delete(error_message)
|
|
if len(error_message) > 0 {
|
|
editor_set_status(editor, fmt.tprintf("Kotlin daemon workspace failed: %s", error_message))
|
|
} else {
|
|
editor_set_status(editor, "Kotlin daemon workspace failed")
|
|
}
|
|
}
|
|
|
|
protocol_error_message :: proc(value: json.Value) -> string {
|
|
error_value, has_error := json_object_get(value, "error")
|
|
if !has_error do return strings.clone("")
|
|
message, _ := json_get_string(error_value, "message")
|
|
if len(message) > 0 do return strings.clone(message)
|
|
code, _ := json_get_string(error_value, "code")
|
|
return strings.clone(code)
|
|
}
|
|
|
|
diagnostics_destroy :: proc(diagnostics: []Diagnostic) {
|
|
for diagnostic in diagnostics {
|
|
delete(diagnostic.severity)
|
|
delete(diagnostic.message)
|
|
}
|
|
}
|
|
|
|
parse_diagnostics_event :: proc(event: string) -> (string, [dynamic]Diagnostic) {
|
|
diagnostics: [dynamic]Diagnostic
|
|
|
|
message, value, parsed := parse_protocol_message(event)
|
|
if !parsed do return "", diagnostics
|
|
defer json.destroy_value(value)
|
|
if message.kind != .Event || message.event != "diagnostics/publish" do return "", diagnostics
|
|
|
|
params, has_params := json_object_get(value, "params")
|
|
if !has_params do return "", diagnostics
|
|
path, _ := json_get_string(params, "path")
|
|
diagnostics_value, has_diagnostics := json_object_get(params, "diagnostics")
|
|
if !has_diagnostics do return strings.clone(path), diagnostics
|
|
|
|
#partial switch items in diagnostics_value {
|
|
case json.Array:
|
|
for item in items {
|
|
severity, _ := json_get_string(item, "severity")
|
|
diagnostic_message, _ := json_get_string(item, "message")
|
|
line, _ := json_get_int(item, "line")
|
|
column, _ := json_get_int(item, "column")
|
|
|
|
append(&diagnostics, Diagnostic{
|
|
line = max_int(line - 1, 0),
|
|
column = max_int(column - 1, 0),
|
|
severity = strings.clone(severity),
|
|
message = strings.clone(diagnostic_message),
|
|
})
|
|
}
|
|
}
|
|
|
|
return strings.clone(path), diagnostics
|
|
}
|
|
|
|
parse_protocol_message :: proc(line: string) -> (Protocol_Message, json.Value, bool) {
|
|
value, err := json.parse_string(line, .JSON, true)
|
|
if err != nil {
|
|
return Protocol_Message{}, value, false
|
|
}
|
|
|
|
message := Protocol_Message{}
|
|
if event, ok := json_get_string(value, "event"); ok {
|
|
message.kind = .Event
|
|
message.event = event
|
|
return message, value, true
|
|
}
|
|
|
|
if id, ok := json_get_int(value, "id"); ok {
|
|
message.kind = .Response
|
|
message.id = id
|
|
message.ok, _ = json_get_bool(value, "ok")
|
|
return message, value, true
|
|
}
|
|
|
|
return message, value, false
|
|
}
|
|
|
|
json_object_get :: proc(value: json.Value, key: string) -> (json.Value, bool) {
|
|
#partial switch object in value {
|
|
case json.Object:
|
|
if item, ok := object[key]; ok {
|
|
return item, true
|
|
}
|
|
}
|
|
return json.Value{}, false
|
|
}
|
|
|
|
json_get_string :: proc(value: json.Value, key: string) -> (string, bool) {
|
|
item, ok := json_object_get(value, key)
|
|
if !ok do return "", false
|
|
#partial switch s in item {
|
|
case json.String:
|
|
return string(s), true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
json_get_int :: proc(value: json.Value, key: string) -> (int, bool) {
|
|
item, ok := json_object_get(value, key)
|
|
if !ok do return 0, false
|
|
#partial switch n in item {
|
|
case json.Integer:
|
|
return int(n), true
|
|
case json.Float:
|
|
return int(n), true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
json_get_bool :: proc(value: json.Value, key: string) -> (bool, bool) {
|
|
item, ok := json_object_get(value, key)
|
|
if !ok do return false, false
|
|
#partial switch b in item {
|
|
case json.Boolean:
|
|
return bool(b), true
|
|
}
|
|
return false, false
|
|
}
|