editor/client/odin/main.odin
2026-07-09 08:27:13 +02:00

298 lines
10 KiB
Odin

package main
import "core:fmt"
import "core:net"
import "core:os"
import "core:strconv"
import "core:strings"
main :: proc() {
args := os.args
if len(args) >= 2 && args[1] == "--terminal-selftest" {
if !terminal_selftest() {
os.exit(1)
}
return
}
if len(args) >= 2 && args[1] == "--editor-selftest" {
if !editor_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)
}
editor_selftest :: proc() -> bool {
ok := true
buffer := buffer_make("one two\nsecond\n")
defer buffer_destroy(&buffer)
cursor := Cursor{}
cursor_move_to_line_col(&buffer, &cursor, 0, 3)
cursor_insert(&buffer, &cursor, "!")
editor_selftest_check(&ok, "buffer insert", buffer_text_equals(&buffer, "one! two\nsecond\n"))
editor_selftest_check(&ok, "buffer undo", buffer_undo(&buffer, &cursor) && buffer_text_equals(&buffer, "one two\nsecond\n"))
editor_selftest_check(&ok, "buffer redo", buffer_redo(&buffer, &cursor) && buffer_text_equals(&buffer, "one! two\nsecond\n"))
version := buffer.version
buffer_replace_range(&buffer, 0, buffer_len(&buffer), "one! two\nsecond\n")
editor_selftest_check(&ok, "replace no-op", buffer.version == version)
editor := Editor{}
defer editor_destroy(&editor)
original := "alpha beta\n child\n"
original_bytes := clone_bytes(transmute([]u8)original)
append(&editor.buffers, Editor_Buffer{
path = strings.clone("selftest.kt"),
daemon_path = strings.clone(""),
original_storage = original_bytes[:],
buffer = buffer_make(string(original_bytes[:])),
saved_version = 0,
})
editor.active = 0
active := editor_active_buffer(&editor)
active.selection_active = true
active.selection_anchor = 0
active.cursor.offset = 5
editor_insert_text(active, "omega")
editor_selftest_check(&ok, "selection replace", buffer_text_equals(&active.buffer, "omega beta\n child\n") && active.dirty)
editor_selftest_check(&ok, "undo returns clean", buffer_undo(&active.buffer, &active.cursor))
editor_update_dirty(active)
editor_selftest_check(&ok, "dirty tracks saved text", buffer_text_equals(&active.buffer, original) && !active.dirty)
cursor_move_to_line_col(&active.buffer, &active.cursor, 1, 9)
editor_insert_newline_auto_indent(active)
editor_selftest_check(&ok, "auto indent newline", buffer_text_equals(&active.buffer, "alpha beta\n child\n \n"))
rename_path := "/tmp/opencode/native-editor-rename-selftest.txt"
_ = os.write_entire_file(rename_path, transmute([]u8)string("alpha beta\n"))
rename := Rename_Panel{open = true}
append(&rename.edits, Rename_Edit{
path = strings.clone(rename_path),
line = 1,
column = 7,
old_text = strings.clone("beta"),
new_text = strings.clone("gamma"),
label = strings.clone("native-editor-rename-selftest.txt:1:7"),
})
sync := Daemon_Sync_State{}
editor_selftest_check(&ok, "rename apply disk", rename_panel_apply_edits(&rename, &editor, &sync))
renamed_data, renamed_err := os.read_entire_file(rename_path, context.allocator)
if renamed_err == nil {
editor_selftest_check(&ok, "rename disk contents", bytes_equal(renamed_data[:], transmute([]u8)string("alpha gamma\n")))
delete(renamed_data)
} else {
editor_selftest_check(&ok, "rename disk contents", false)
}
rename_panel_destroy(&rename)
fmt.println(ok ? "editor selftest: PASS" : "editor selftest: FAIL")
return ok
}
editor_selftest_check :: proc(ok: ^bool, name: string, passed: bool) {
if passed {
fmt.println("ok ", name)
} else {
fmt.println("FAIL", name)
ok^ = false
}
}
buffer_text_equals :: proc(buffer: ^Buffer, expected: string) -> bool {
text := buffer_bytes(buffer)
defer delete(text)
return bytes_equal(text[:], transmute([]u8)expected)
}
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
}