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" 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, 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 } daemon_start_process_begin :: proc(workspace: string) -> (Daemon_Process, bool) { child := Daemon_Process{} 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", workspace, "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) } 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.is_done(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 request := fmt.tprintf("{{\"id\":%d,\"method\":\"workspace/open\",\"params\":{{\"root\":%q}}}}\n", id, 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\":%q,\"version\":%d,\"text\":%q}}}}\n", id, path, version, 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\":%q,\"version\":%d,\"text\":%q}}}}\n", id, path, version, 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\":%q,\"line\":%d,\"column\":%d}}}}\n", id, 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\":%q,\"line\":%d,\"column\":%d}}}}\n", id, 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\":%q,\"line\":%d,\"column\":%d}}}}\n", id, 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\":%q,\"line\":%d,\"column\":%d}}}}\n", id, 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\":%q,\"line\":%d,\"column\":%d,\"newName\":%q}}}}\n", id, path, line + 1, column + 1, 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\":%q}}}}\n", id, 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 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) } } 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 }