editor/client/odin/main.odin
2026-07-08 10:28:32 +02:00

208 lines
6.6 KiB
Odin

package main
import "core:fmt"
import "core:net"
import "core:os"
import "core:strconv"
main :: proc() {
args := os.args
if len(args) >= 2 && args[1] == "--terminal-selftest" {
if !terminal_selftest() {
os.exit(1)
}
return
}
if len(args) >= 3 && args[1] == "--git-status-selftest" {
if !git_status_selftest(args[2]) {
os.exit(1)
}
return
}
if len(args) >= 2 && args[1] == "--git-selftest" {
workspace := "." if len(args) < 3 else args[2]
if !git_panel_selftest(workspace) {
os.exit(1)
}
return
}
// Legacy smoke-test mode: a bare numeric first argument is a daemon port.
if len(args) >= 2 && args[1] != "--sdl" {
if port, is_port := strconv.parse_int(args[1]); is_port {
workspace := "."
if len(args) >= 3 {
workspace = args[2]
}
run_smoke_client(int(port), workspace)
return
}
}
// SDL editor mode (default). All arguments are optional and positional in
// any order: a directory is the workspace, a number is a daemon port, and
// anything else is a file to open. `--sdl` is accepted for compatibility.
sdl_args := args[1:]
if len(sdl_args) > 0 && sdl_args[0] == "--sdl" {
sdl_args = sdl_args[1:]
}
workspace := ""
port := 0
file := ""
for arg in sdl_args {
if parsed_port, is_port := strconv.parse_int(arg); is_port && port == 0 {
port = int(parsed_port)
} else if os.is_dir(arg) && len(workspace) == 0 {
workspace = arg
} else if len(file) == 0 {
file = arg
} else {
fmt.println("usage: odin run client/odin -- [workspace] [port] [file]")
fmt.println(" or: odin run client/odin -- <port> [workspace] (smoke test)")
return
}
}
run_sdl_editor(workspace, port, file)
}
run_smoke_client :: proc(port: int, workspace: string) {
run_buffer_smoke()
editor := run_editor_smoke(workspace)
defer editor_destroy(&editor)
socket, err := net.dial_tcp("127.0.0.1", int(port))
if err != nil {
fmt.println("connect failed:", err)
return
}
defer net.close(socket)
send_line(socket, "{\"id\":1,\"method\":\"ping\",\"params\":{}}\n")
read_messages(socket, 1)
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)
send_line(socket, "{\"id\":3,\"method\":\"gradle/tasks\",\"params\":{}}\n")
read_messages(socket, 1)
active := editor_active_buffer(&editor)
diagnostics_path := active.path
active_text := editor_active_text(&editor)
defer delete(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\":%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\":%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\":%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\":%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\":%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\":%s,\"line\":1,\"column\":5,\"newName\":\"renamedBroken\"}}}}\n", json_quote(diagnostics_path))
send_line(socket, rename_request)
read_messages(socket, 1)
}
run_editor_smoke :: proc(workspace: string) -> Editor {
editor := Editor{}
path := fmt.tprintf("%s/src/main/kotlin/dev/nativeeditor/daemon/Main.kt", workspace)
if !editor_open_file(&editor, path) {
return editor
}
fmt.println("editor-visible-before:")
editor_print_visible(&editor, 0, 2)
editor_replace_active_text(&editor, "fun broken( {")
fmt.println("editor-visible-after:")
editor_print_visible(&editor, 0, 2)
return editor
}
run_buffer_smoke :: proc() {
buffer := buffer_make("hello world\nsecond line\n")
defer buffer_destroy(&buffer)
buffer_insert(&buffer, 6, "native ")
buffer_delete_range(&buffer, 0, 6)
cursor := Cursor{}
cursor_move_to_line_col(&buffer, &cursor, 1, 6)
cursor_insert(&buffer, &cursor, " edited")
cursor_backspace(&buffer, &cursor)
_ = buffer_undo(&buffer, &cursor)
_ = buffer_redo(&buffer, &cursor)
cursor_move_vertical(&buffer, &cursor, -1)
line, col := buffer_offset_to_line_col(&buffer, cursor.offset)
second_line := buffer_line_bytes(&buffer, 1)
defer delete(second_line)
text := buffer_bytes(&buffer)
defer delete(text)
fmt.printf("buffer-smoke: %s", string(text[:]))
fmt.printf("buffer-lines: %d\n", buffer_line_count(&buffer))
fmt.printf("buffer-line-1: %s\n", string(second_line[:]))
fmt.printf("buffer-cursor: %d:%d\n", line, col)
}
send_line :: proc(socket: net.TCP_Socket, line: string) {
bytes := transmute([]byte)line
_, err := net.send_tcp(socket, bytes)
if err != nil {
fmt.println("send failed:", err)
}
}
read_messages :: proc(socket: net.TCP_Socket, expected: int) {
seen := 0
for seen < expected {
seen += read_some(socket)
}
}
read_some :: proc(socket: net.TCP_Socket) -> int {
buf: [4096]byte
n, err := net.recv_tcp(socket, buf[:])
if err != nil {
fmt.println("recv failed:", err)
return 0
}
if n == 0 {
fmt.println("connection closed")
return 0
}
lines := 0
for b in buf[:n] {
if b == '\n' {
lines += 1
}
}
fmt.print(string(buf[:n]))
return lines
}