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 -- [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 }