Initial commit: Odin editor client + Kotlin daemon prototype
Native SDL3 editor written in Odin with a piece-table buffer core, backed by a Kotlin/JVM daemon speaking newline-delimited JSON over localhost TCP for Gradle import, Kotlin/Java diagnostics, and heuristic completion/hover/definition/references/rename. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
4a15350860
21 changed files with 8112 additions and 0 deletions
538
client/odin/buffer.odin
Normal file
538
client/odin/buffer.odin
Normal file
|
|
@ -0,0 +1,538 @@
|
|||
package main
|
||||
|
||||
Piece_Source :: enum {
|
||||
Original,
|
||||
Add,
|
||||
}
|
||||
|
||||
Piece :: struct {
|
||||
source: Piece_Source,
|
||||
start: int,
|
||||
len: int,
|
||||
}
|
||||
|
||||
Line_Index :: struct {
|
||||
starts: [dynamic]int,
|
||||
dirty: bool,
|
||||
}
|
||||
|
||||
Buffer_Edit_Kind :: enum {
|
||||
Insert,
|
||||
Delete,
|
||||
Replace,
|
||||
}
|
||||
|
||||
Buffer_Edit :: struct {
|
||||
kind: Buffer_Edit_Kind,
|
||||
offset: int,
|
||||
text: [dynamic]u8,
|
||||
replacement: [dynamic]u8,
|
||||
}
|
||||
|
||||
Buffer :: struct {
|
||||
original: string,
|
||||
add: [dynamic]u8,
|
||||
pieces: [dynamic]Piece,
|
||||
undo: [dynamic]Buffer_Edit,
|
||||
redo: [dynamic]Buffer_Edit,
|
||||
|
||||
line_index: Line_Index,
|
||||
version: int,
|
||||
}
|
||||
|
||||
Cursor :: struct {
|
||||
offset: int,
|
||||
wanted_column: int,
|
||||
}
|
||||
|
||||
buffer_make :: proc(original: string) -> Buffer {
|
||||
buffer: Buffer
|
||||
buffer.original = original
|
||||
buffer.line_index.dirty = true
|
||||
|
||||
if len(original) > 0 {
|
||||
append(&buffer.pieces, Piece{.Original, 0, len(original)})
|
||||
}
|
||||
|
||||
return buffer
|
||||
}
|
||||
|
||||
buffer_len :: proc(buffer: ^Buffer) -> int {
|
||||
total := 0
|
||||
for piece in buffer.pieces {
|
||||
total += piece.len
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
buffer_insert :: proc(buffer: ^Buffer, offset: int, text: string) {
|
||||
if len(text) == 0 do return
|
||||
|
||||
edit_text := clone_bytes(transmute([]u8)text)
|
||||
buffer_push_edit(&buffer.undo, Buffer_Edit{kind = .Insert, offset = clamp_int(offset, 0, buffer_len(buffer)), text = edit_text})
|
||||
buffer_clear_edits(&buffer.redo)
|
||||
buffer_insert_raw(buffer, offset, text)
|
||||
}
|
||||
|
||||
buffer_insert_raw :: proc(buffer: ^Buffer, offset: int, text: string) {
|
||||
buffer_insert_raw_internal(buffer, offset, text, true)
|
||||
}
|
||||
|
||||
buffer_insert_raw_internal :: proc(buffer: ^Buffer, offset: int, text: string, bump_version: bool) {
|
||||
if len(text) == 0 do return
|
||||
|
||||
insert_at := clamp_int(offset, 0, buffer_len(buffer))
|
||||
add_start := len(buffer.add)
|
||||
text_bytes := transmute([]u8)text
|
||||
for b in text_bytes {
|
||||
append(&buffer.add, b)
|
||||
}
|
||||
|
||||
new_piece := Piece{.Add, add_start, len(text_bytes)}
|
||||
new_pieces: [dynamic]Piece
|
||||
inserted := false
|
||||
cursor := 0
|
||||
|
||||
for piece in buffer.pieces {
|
||||
piece_end := cursor + piece.len
|
||||
|
||||
if !inserted && insert_at <= piece_end {
|
||||
inner := insert_at - cursor
|
||||
if inner > 0 {
|
||||
append(&new_pieces, Piece{piece.source, piece.start, inner})
|
||||
}
|
||||
append(&new_pieces, new_piece)
|
||||
if inner < piece.len {
|
||||
append(&new_pieces, Piece{piece.source, piece.start + inner, piece.len - inner})
|
||||
}
|
||||
inserted = true
|
||||
} else {
|
||||
append(&new_pieces, piece)
|
||||
}
|
||||
|
||||
cursor = piece_end
|
||||
}
|
||||
|
||||
if !inserted {
|
||||
append(&new_pieces, new_piece)
|
||||
}
|
||||
|
||||
delete(buffer.pieces)
|
||||
buffer.pieces = new_pieces
|
||||
if bump_version do buffer.version += 1
|
||||
buffer.line_index.dirty = true
|
||||
}
|
||||
|
||||
buffer_delete_range :: proc(buffer: ^Buffer, offset: int, count: int) {
|
||||
if count <= 0 do return
|
||||
|
||||
start := clamp_int(offset, 0, buffer_len(buffer))
|
||||
end := clamp_int(start + count, start, buffer_len(buffer))
|
||||
if start == end do return
|
||||
|
||||
deleted := buffer_range_bytes(buffer, start, end - start)
|
||||
buffer_push_edit(&buffer.undo, Buffer_Edit{kind = .Delete, offset = start, text = deleted})
|
||||
buffer_clear_edits(&buffer.redo)
|
||||
buffer_delete_range_raw(buffer, start, end - start)
|
||||
}
|
||||
|
||||
buffer_replace_range :: proc(buffer: ^Buffer, offset: int, count: int, replacement: string) {
|
||||
start := clamp_int(offset, 0, buffer_len(buffer))
|
||||
end := clamp_int(start + max_int(count, 0), start, buffer_len(buffer))
|
||||
if start == end && len(replacement) == 0 do return
|
||||
|
||||
deleted := buffer_range_bytes(buffer, start, end - start)
|
||||
if bytes_equal(deleted[:], transmute([]u8)replacement) {
|
||||
delete(deleted)
|
||||
return
|
||||
}
|
||||
replacement_bytes := clone_bytes(transmute([]u8)replacement)
|
||||
buffer_push_edit(&buffer.undo, Buffer_Edit{kind = .Replace, offset = start, text = deleted, replacement = replacement_bytes})
|
||||
buffer_clear_edits(&buffer.redo)
|
||||
buffer_delete_range_raw_internal(buffer, start, end - start, false)
|
||||
buffer_insert_raw_internal(buffer, start, replacement, false)
|
||||
buffer.version += 1
|
||||
buffer.line_index.dirty = true
|
||||
}
|
||||
|
||||
buffer_delete_range_raw :: proc(buffer: ^Buffer, offset: int, count: int) {
|
||||
buffer_delete_range_raw_internal(buffer, offset, count, true)
|
||||
}
|
||||
|
||||
buffer_delete_range_raw_internal :: proc(buffer: ^Buffer, offset: int, count: int, bump_version: bool) {
|
||||
if count <= 0 do return
|
||||
|
||||
start := clamp_int(offset, 0, buffer_len(buffer))
|
||||
end := clamp_int(start + count, start, buffer_len(buffer))
|
||||
if start == end do return
|
||||
|
||||
new_pieces: [dynamic]Piece
|
||||
cursor := 0
|
||||
|
||||
for piece in buffer.pieces {
|
||||
piece_start := cursor
|
||||
piece_end := cursor + piece.len
|
||||
|
||||
if piece_end <= start || piece_start >= end {
|
||||
append(&new_pieces, piece)
|
||||
} else {
|
||||
keep_left := max_int(0, start - piece_start)
|
||||
keep_right := max_int(0, piece_end - end)
|
||||
|
||||
if keep_left > 0 {
|
||||
append(&new_pieces, Piece{piece.source, piece.start, keep_left})
|
||||
}
|
||||
if keep_right > 0 {
|
||||
right_start := piece.start + piece.len - keep_right
|
||||
append(&new_pieces, Piece{piece.source, right_start, keep_right})
|
||||
}
|
||||
}
|
||||
|
||||
cursor = piece_end
|
||||
}
|
||||
|
||||
delete(buffer.pieces)
|
||||
buffer.pieces = new_pieces
|
||||
if bump_version do buffer.version += 1
|
||||
buffer.line_index.dirty = true
|
||||
}
|
||||
|
||||
buffer_undo :: proc(buffer: ^Buffer, cursor: ^Cursor) -> bool {
|
||||
edit, ok := buffer_pop_edit(&buffer.undo)
|
||||
if !ok do return false
|
||||
|
||||
switch edit.kind {
|
||||
case .Insert:
|
||||
buffer_delete_range_raw(buffer, edit.offset, len(edit.text))
|
||||
cursor.offset = edit.offset
|
||||
case .Delete:
|
||||
buffer_insert_raw(buffer, edit.offset, string(edit.text[:]))
|
||||
cursor.offset = edit.offset + len(edit.text)
|
||||
case .Replace:
|
||||
buffer_delete_range_raw_internal(buffer, edit.offset, len(edit.replacement), false)
|
||||
buffer_insert_raw_internal(buffer, edit.offset, string(edit.text[:]), false)
|
||||
buffer.version += 1
|
||||
buffer.line_index.dirty = true
|
||||
cursor.offset = edit.offset + len(edit.text)
|
||||
}
|
||||
|
||||
_, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset)
|
||||
buffer_push_edit(&buffer.redo, edit)
|
||||
return true
|
||||
}
|
||||
|
||||
buffer_redo :: proc(buffer: ^Buffer, cursor: ^Cursor) -> bool {
|
||||
edit, ok := buffer_pop_edit(&buffer.redo)
|
||||
if !ok do return false
|
||||
|
||||
switch edit.kind {
|
||||
case .Insert:
|
||||
buffer_insert_raw(buffer, edit.offset, string(edit.text[:]))
|
||||
cursor.offset = edit.offset + len(edit.text)
|
||||
case .Delete:
|
||||
buffer_delete_range_raw(buffer, edit.offset, len(edit.text))
|
||||
cursor.offset = edit.offset
|
||||
case .Replace:
|
||||
buffer_delete_range_raw_internal(buffer, edit.offset, len(edit.text), false)
|
||||
buffer_insert_raw_internal(buffer, edit.offset, string(edit.replacement[:]), false)
|
||||
buffer.version += 1
|
||||
buffer.line_index.dirty = true
|
||||
cursor.offset = edit.offset + len(edit.replacement)
|
||||
}
|
||||
|
||||
_, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset)
|
||||
buffer_push_edit(&buffer.undo, edit)
|
||||
return true
|
||||
}
|
||||
|
||||
buffer_bytes :: proc(buffer: ^Buffer) -> [dynamic]u8 {
|
||||
out: [dynamic]u8
|
||||
|
||||
for piece in buffer.pieces {
|
||||
switch piece.source {
|
||||
case .Original:
|
||||
chunk := buffer.original[piece.start:piece.start + piece.len]
|
||||
chunk_bytes := transmute([]u8)chunk
|
||||
for b in chunk_bytes {
|
||||
append(&out, b)
|
||||
}
|
||||
case .Add:
|
||||
for b in buffer.add[piece.start:piece.start + piece.len] {
|
||||
append(&out, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
buffer_rebuild_line_index :: proc(buffer: ^Buffer) {
|
||||
delete(buffer.line_index.starts)
|
||||
buffer.line_index.starts = make([dynamic]int)
|
||||
append(&buffer.line_index.starts, 0)
|
||||
|
||||
text := buffer_bytes(buffer)
|
||||
defer delete(text)
|
||||
|
||||
for b, i in text {
|
||||
if b == '\n' {
|
||||
append(&buffer.line_index.starts, i + 1)
|
||||
}
|
||||
}
|
||||
|
||||
buffer.line_index.dirty = false
|
||||
}
|
||||
|
||||
buffer_line_count :: proc(buffer: ^Buffer) -> int {
|
||||
if buffer.line_index.dirty {
|
||||
buffer_rebuild_line_index(buffer)
|
||||
}
|
||||
return len(buffer.line_index.starts)
|
||||
}
|
||||
|
||||
buffer_line_start :: proc(buffer: ^Buffer, line: int) -> int {
|
||||
if buffer.line_index.dirty {
|
||||
buffer_rebuild_line_index(buffer)
|
||||
}
|
||||
|
||||
if len(buffer.line_index.starts) == 0 do return 0
|
||||
index := clamp_int(line, 0, len(buffer.line_index.starts) - 1)
|
||||
return buffer.line_index.starts[index]
|
||||
}
|
||||
|
||||
buffer_line_end :: proc(buffer: ^Buffer, line: int) -> int {
|
||||
if buffer.line_index.dirty {
|
||||
buffer_rebuild_line_index(buffer)
|
||||
}
|
||||
|
||||
line_count := len(buffer.line_index.starts)
|
||||
if line_count == 0 do return 0
|
||||
|
||||
index := clamp_int(line, 0, line_count - 1)
|
||||
if index + 1 < line_count {
|
||||
return max_int(buffer.line_index.starts[index], buffer.line_index.starts[index + 1] - 1)
|
||||
}
|
||||
|
||||
return buffer_len(buffer)
|
||||
}
|
||||
|
||||
buffer_offset_to_line_col :: proc(buffer: ^Buffer, offset: int) -> (line: int, column: int) {
|
||||
if buffer.line_index.dirty {
|
||||
buffer_rebuild_line_index(buffer)
|
||||
}
|
||||
|
||||
target := clamp_int(offset, 0, buffer_len(buffer))
|
||||
result := 0
|
||||
for start, i in buffer.line_index.starts {
|
||||
if start > target do break
|
||||
result = i
|
||||
}
|
||||
|
||||
return result, target - buffer.line_index.starts[result]
|
||||
}
|
||||
|
||||
buffer_line_col_to_offset :: proc(buffer: ^Buffer, line: int, column: int) -> int {
|
||||
start := buffer_line_start(buffer, line)
|
||||
end := buffer_line_end(buffer, line)
|
||||
return clamp_int(start + max_int(column, 0), start, end)
|
||||
}
|
||||
|
||||
buffer_line_bytes :: proc(buffer: ^Buffer, line: int) -> [dynamic]u8 {
|
||||
start := buffer_line_start(buffer, line)
|
||||
end := buffer_line_end(buffer, line)
|
||||
return buffer_range_bytes(buffer, start, end - start)
|
||||
}
|
||||
|
||||
buffer_range_bytes :: proc(buffer: ^Buffer, offset: int, count: int) -> [dynamic]u8 {
|
||||
out: [dynamic]u8
|
||||
if count <= 0 do return out
|
||||
|
||||
start := clamp_int(offset, 0, buffer_len(buffer))
|
||||
end := clamp_int(start + count, start, buffer_len(buffer))
|
||||
cursor := 0
|
||||
|
||||
for piece in buffer.pieces {
|
||||
piece_start := cursor
|
||||
piece_end := cursor + piece.len
|
||||
|
||||
if piece_end > start && piece_start < end {
|
||||
local_start := max_int(start - piece_start, 0)
|
||||
local_end := piece.len - max_int(piece_end - end, 0)
|
||||
|
||||
switch piece.source {
|
||||
case .Original:
|
||||
chunk := buffer.original[piece.start + local_start:piece.start + local_end]
|
||||
chunk_bytes := transmute([]u8)chunk
|
||||
for b in chunk_bytes {
|
||||
append(&out, b)
|
||||
}
|
||||
case .Add:
|
||||
for b in buffer.add[piece.start + local_start:piece.start + local_end] {
|
||||
append(&out, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cursor = piece_end
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
cursor_insert :: proc(buffer: ^Buffer, cursor: ^Cursor, text: string) {
|
||||
buffer_insert(buffer, cursor.offset, text)
|
||||
cursor.offset += len(text)
|
||||
_, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset)
|
||||
}
|
||||
|
||||
cursor_backspace :: proc(buffer: ^Buffer, cursor: ^Cursor) {
|
||||
if cursor.offset == 0 do return
|
||||
buffer_delete_range(buffer, cursor.offset - 1, 1)
|
||||
cursor.offset -= 1
|
||||
_, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset)
|
||||
}
|
||||
|
||||
cursor_delete_forward :: proc(buffer: ^Buffer, cursor: ^Cursor) -> bool {
|
||||
if cursor.offset >= buffer_len(buffer) do return false
|
||||
buffer_delete_range(buffer, cursor.offset, 1)
|
||||
_, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset)
|
||||
return true
|
||||
}
|
||||
|
||||
cursor_insert_newline_auto_indent :: proc(buffer: ^Buffer, cursor: ^Cursor) {
|
||||
line, _ := buffer_offset_to_line_col(buffer, cursor.offset)
|
||||
line_bytes := buffer_line_bytes(buffer, line)
|
||||
defer delete(line_bytes)
|
||||
|
||||
indent_len := 0
|
||||
for b in line_bytes {
|
||||
if b == ' ' || b == '\t' {
|
||||
indent_len += 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
text: [dynamic]u8
|
||||
defer delete(text)
|
||||
append(&text, '\n')
|
||||
for b in line_bytes[:indent_len] {
|
||||
append(&text, b)
|
||||
}
|
||||
cursor_insert(buffer, cursor, string(text[:]))
|
||||
}
|
||||
|
||||
cursor_move_word_left :: proc(buffer: ^Buffer, cursor: ^Cursor) {
|
||||
text := buffer_bytes(buffer)
|
||||
defer delete(text)
|
||||
|
||||
offset := clamp_int(cursor.offset, 0, len(text))
|
||||
for offset > 0 && is_space_byte(text[offset - 1]) {
|
||||
offset -= 1
|
||||
}
|
||||
if offset > 0 && is_identifier_byte(text[offset - 1]) {
|
||||
for offset > 0 && is_identifier_byte(text[offset - 1]) {
|
||||
offset -= 1
|
||||
}
|
||||
} else if offset > 0 {
|
||||
offset -= 1
|
||||
}
|
||||
|
||||
cursor.offset = offset
|
||||
_, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset)
|
||||
}
|
||||
|
||||
cursor_move_word_right :: proc(buffer: ^Buffer, cursor: ^Cursor) {
|
||||
text := buffer_bytes(buffer)
|
||||
defer delete(text)
|
||||
|
||||
offset := clamp_int(cursor.offset, 0, len(text))
|
||||
if offset < len(text) && is_identifier_byte(text[offset]) {
|
||||
for offset < len(text) && is_identifier_byte(text[offset]) {
|
||||
offset += 1
|
||||
}
|
||||
} else if offset < len(text) {
|
||||
offset += 1
|
||||
}
|
||||
for offset < len(text) && is_space_byte(text[offset]) {
|
||||
offset += 1
|
||||
}
|
||||
|
||||
cursor.offset = offset
|
||||
_, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset)
|
||||
}
|
||||
|
||||
cursor_move_to_line_col :: proc(buffer: ^Buffer, cursor: ^Cursor, line: int, column: int) {
|
||||
cursor.offset = buffer_line_col_to_offset(buffer, line, column)
|
||||
_, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset)
|
||||
}
|
||||
|
||||
cursor_move_vertical :: proc(buffer: ^Buffer, cursor: ^Cursor, delta: int) {
|
||||
line, _ := buffer_offset_to_line_col(buffer, cursor.offset)
|
||||
target_line := clamp_int(line + delta, 0, buffer_line_count(buffer) - 1)
|
||||
cursor.offset = buffer_line_col_to_offset(buffer, target_line, cursor.wanted_column)
|
||||
}
|
||||
|
||||
is_identifier_byte :: proc(b: u8) -> bool {
|
||||
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || b == '_'
|
||||
}
|
||||
|
||||
is_space_byte :: proc(b: u8) -> bool {
|
||||
return b == ' ' || b == '\t' || b == '\n' || b == '\r'
|
||||
}
|
||||
|
||||
buffer_destroy :: proc(buffer: ^Buffer) {
|
||||
buffer_clear_edits(&buffer.undo)
|
||||
buffer_clear_edits(&buffer.redo)
|
||||
delete(buffer.add)
|
||||
delete(buffer.pieces)
|
||||
delete(buffer.line_index.starts)
|
||||
}
|
||||
|
||||
buffer_push_edit :: proc(stack: ^[dynamic]Buffer_Edit, edit: Buffer_Edit) {
|
||||
append(stack, edit)
|
||||
}
|
||||
|
||||
buffer_pop_edit :: proc(stack: ^[dynamic]Buffer_Edit) -> (edit: Buffer_Edit, ok: bool) {
|
||||
if len(stack^) == 0 do return {}, false
|
||||
|
||||
index := len(stack^) - 1
|
||||
edit = stack^[index]
|
||||
resize(stack, index)
|
||||
return edit, true
|
||||
}
|
||||
|
||||
buffer_clear_edits :: proc(stack: ^[dynamic]Buffer_Edit) {
|
||||
for edit in stack^ {
|
||||
delete(edit.text)
|
||||
delete(edit.replacement)
|
||||
}
|
||||
clear(stack)
|
||||
}
|
||||
|
||||
clone_bytes :: proc(bytes: []u8) -> [dynamic]u8 {
|
||||
out: [dynamic]u8
|
||||
for b in bytes {
|
||||
append(&out, b)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
bytes_equal :: proc(a, b: []u8) -> bool {
|
||||
if len(a) != len(b) do return false
|
||||
for value, index in a {
|
||||
if value != b[index] do return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
clamp_int :: proc(value, low, high: int) -> int {
|
||||
if value < low do return low
|
||||
if value > high do return high
|
||||
return value
|
||||
}
|
||||
|
||||
max_int :: proc(a, b: int) -> int {
|
||||
if a > b do return a
|
||||
return b
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue