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
BIN
client/odin/assets/fonts/NotoSansMono-Regular.ttf
Normal file
BIN
client/odin/assets/fonts/NotoSansMono-Regular.ttf
Normal file
Binary file not shown.
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
|
||||
}
|
||||
615
client/odin/daemon_client.odin
Normal file
615
client/odin/daemon_client.odin
Normal file
|
|
@ -0,0 +1,615 @@
|
|||
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
|
||||
}
|
||||
449
client/odin/editor.odin
Normal file
449
client/odin/editor.odin
Normal file
|
|
@ -0,0 +1,449 @@
|
|||
package main
|
||||
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
import "core:strings"
|
||||
|
||||
Diagnostic :: struct {
|
||||
line: int,
|
||||
column: int,
|
||||
severity: string,
|
||||
message: string,
|
||||
}
|
||||
|
||||
Editor_Buffer :: struct {
|
||||
path: string,
|
||||
daemon_path: string,
|
||||
original_storage: []u8,
|
||||
buffer: Buffer,
|
||||
cursor: Cursor,
|
||||
dirty: bool,
|
||||
saved_version: int,
|
||||
selection_active: bool,
|
||||
selection_anchor: int,
|
||||
diagnostics: [dynamic]Diagnostic,
|
||||
}
|
||||
|
||||
Editor :: struct {
|
||||
buffers: [dynamic]Editor_Buffer,
|
||||
active: int,
|
||||
status: string,
|
||||
}
|
||||
|
||||
editor_open_file :: proc(editor: ^Editor, path: string) -> bool {
|
||||
data, err := os.read_entire_file(path, context.allocator)
|
||||
if err != nil {
|
||||
fmt.println("open file failed:", err)
|
||||
return false
|
||||
}
|
||||
|
||||
editor_buffer := Editor_Buffer{
|
||||
path = strings.clone(path),
|
||||
daemon_path = strings.clone(""),
|
||||
original_storage = data,
|
||||
buffer = buffer_make(string(data)),
|
||||
saved_version = 0,
|
||||
}
|
||||
|
||||
append(&editor.buffers, editor_buffer)
|
||||
editor.active = len(editor.buffers) - 1
|
||||
return true
|
||||
}
|
||||
|
||||
editor_open_or_focus_file :: proc(editor: ^Editor, path: string) -> bool {
|
||||
for &buffer, index in editor.buffers {
|
||||
if buffer.path == path {
|
||||
editor.active = index
|
||||
return true
|
||||
}
|
||||
}
|
||||
return editor_open_file(editor, path)
|
||||
}
|
||||
|
||||
editor_replace_active_file :: proc(editor: ^Editor, path: string) -> bool {
|
||||
active := editor_active_buffer(editor)
|
||||
if active == nil do return false
|
||||
if active.path == path do return true
|
||||
|
||||
for &buffer, index in editor.buffers {
|
||||
if buffer.path == path {
|
||||
if index != editor.active {
|
||||
editor.buffers[editor.active], editor.buffers[index] = editor.buffers[index], editor.buffers[editor.active]
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if active.dirty {
|
||||
editor_set_status(editor, "Save current buffer before navigating to another file")
|
||||
return false
|
||||
}
|
||||
|
||||
data, err := os.read_entire_file(path, context.allocator)
|
||||
if err != nil {
|
||||
fmt.println("open file failed:", err)
|
||||
return false
|
||||
}
|
||||
|
||||
editor_buffer_destroy(active)
|
||||
active.path = strings.clone(path)
|
||||
active.daemon_path = strings.clone("")
|
||||
active.original_storage = data
|
||||
active.buffer = buffer_make(string(data))
|
||||
active.cursor = Cursor{}
|
||||
active.dirty = false
|
||||
active.saved_version = active.buffer.version
|
||||
active.selection_active = false
|
||||
active.selection_anchor = 0
|
||||
active.diagnostics = make([dynamic]Diagnostic)
|
||||
return true
|
||||
}
|
||||
|
||||
editor_active_buffer :: proc(editor: ^Editor) -> ^Editor_Buffer {
|
||||
if len(editor.buffers) == 0 do return nil
|
||||
return &editor.buffers[editor.active]
|
||||
}
|
||||
|
||||
editor_replace_active_text :: proc(editor: ^Editor, text: string) {
|
||||
active := editor_active_buffer(editor)
|
||||
if active == nil do return
|
||||
|
||||
buffer_replace_range(&active.buffer, 0, buffer_len(&active.buffer), text)
|
||||
active.cursor.offset = len(text)
|
||||
_, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset)
|
||||
editor_clear_selection(active)
|
||||
editor_update_dirty(active)
|
||||
}
|
||||
|
||||
editor_active_text :: proc(editor: ^Editor) -> [dynamic]u8 {
|
||||
active := editor_active_buffer(editor)
|
||||
if active == nil {
|
||||
empty: [dynamic]u8
|
||||
return empty
|
||||
}
|
||||
|
||||
return buffer_bytes(&active.buffer)
|
||||
}
|
||||
|
||||
editor_clear_selection :: proc(active: ^Editor_Buffer) {
|
||||
if active == nil do return
|
||||
active.selection_active = false
|
||||
active.selection_anchor = active.cursor.offset
|
||||
}
|
||||
|
||||
editor_start_selection :: proc(active: ^Editor_Buffer) {
|
||||
if active == nil do return
|
||||
if !active.selection_active {
|
||||
active.selection_active = true
|
||||
active.selection_anchor = active.cursor.offset
|
||||
}
|
||||
}
|
||||
|
||||
editor_selection_range :: proc(active: ^Editor_Buffer) -> (start: int, end: int, ok: bool) {
|
||||
if active == nil || !active.selection_active do return 0, 0, false
|
||||
start = min_int(active.selection_anchor, active.cursor.offset)
|
||||
end = max_int(active.selection_anchor, active.cursor.offset)
|
||||
return start, end, start < end
|
||||
}
|
||||
|
||||
editor_delete_selection :: proc(active: ^Editor_Buffer) -> bool {
|
||||
start, end, ok := editor_selection_range(active)
|
||||
if !ok do return false
|
||||
|
||||
buffer_delete_range(&active.buffer, start, end - start)
|
||||
active.cursor.offset = start
|
||||
_, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset)
|
||||
editor_clear_selection(active)
|
||||
return true
|
||||
}
|
||||
|
||||
editor_insert_text :: proc(active: ^Editor_Buffer, text: string) {
|
||||
if active == nil do return
|
||||
selection_start, selection_end, has_selection := editor_selection_range(active)
|
||||
if has_selection {
|
||||
buffer_replace_range(&active.buffer, selection_start, selection_end - selection_start, text)
|
||||
active.cursor.offset = selection_start + len(text)
|
||||
_, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset)
|
||||
editor_clear_selection(active)
|
||||
editor_update_dirty(active)
|
||||
return
|
||||
}
|
||||
cursor_insert(&active.buffer, &active.cursor, text)
|
||||
editor_clear_selection(active)
|
||||
editor_update_dirty(active)
|
||||
}
|
||||
|
||||
editor_update_dirty :: proc(active: ^Editor_Buffer) {
|
||||
if active == nil do return
|
||||
active.dirty = active.buffer.version != active.saved_version
|
||||
}
|
||||
|
||||
editor_outdent :: proc(active: ^Editor_Buffer) -> bool {
|
||||
if active == nil do return false
|
||||
|
||||
selection_start, selection_end, has_selection := editor_selection_range(active)
|
||||
start_line, _ := buffer_offset_to_line_col(&active.buffer, selection_start if has_selection else active.cursor.offset)
|
||||
end_line, end_col := buffer_offset_to_line_col(&active.buffer, selection_end if has_selection else active.cursor.offset)
|
||||
if has_selection && end_col == 0 && end_line > start_line {
|
||||
end_line -= 1
|
||||
}
|
||||
|
||||
total_removed := 0
|
||||
cursor_removed_before := 0
|
||||
anchor_removed_before := 0
|
||||
cursor_offset := active.cursor.offset
|
||||
anchor_offset := active.selection_anchor
|
||||
|
||||
for line := end_line; line >= start_line; line -= 1 {
|
||||
line_start := buffer_line_start(&active.buffer, line)
|
||||
line_end := buffer_line_end(&active.buffer, line)
|
||||
remove_count := editor_line_outdent_count(&active.buffer, line_start, line_end)
|
||||
if remove_count == 0 do continue
|
||||
|
||||
buffer_delete_range(&active.buffer, line_start, remove_count)
|
||||
total_removed += remove_count
|
||||
if line_start < cursor_offset {
|
||||
cursor_removed_before += min_int(remove_count, cursor_offset - line_start)
|
||||
}
|
||||
if line_start < anchor_offset {
|
||||
anchor_removed_before += min_int(remove_count, anchor_offset - line_start)
|
||||
}
|
||||
}
|
||||
|
||||
if total_removed == 0 do return false
|
||||
|
||||
active.cursor.offset = clamp_int(cursor_offset - cursor_removed_before, 0, buffer_len(&active.buffer))
|
||||
if has_selection {
|
||||
active.selection_anchor = clamp_int(anchor_offset - anchor_removed_before, 0, buffer_len(&active.buffer))
|
||||
} else {
|
||||
editor_clear_selection(active)
|
||||
}
|
||||
_, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset)
|
||||
editor_update_dirty(active)
|
||||
return true
|
||||
}
|
||||
|
||||
editor_line_outdent_count :: proc(buffer: ^Buffer, line_start, line_end: int) -> int {
|
||||
if line_start >= line_end do return 0
|
||||
bytes := buffer_range_bytes(buffer, line_start, min_int(4, line_end - line_start))
|
||||
defer delete(bytes)
|
||||
|
||||
remove_count := 0
|
||||
for b in bytes {
|
||||
if b == ' ' && remove_count < 4 {
|
||||
remove_count += 1
|
||||
} else if b == '\t' && remove_count == 0 {
|
||||
return 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return remove_count
|
||||
}
|
||||
|
||||
editor_insert_newline_auto_indent :: proc(active: ^Editor_Buffer) {
|
||||
if active == nil do return
|
||||
selection_start, selection_end, has_selection := editor_selection_range(active)
|
||||
if has_selection {
|
||||
buffer_replace_range(&active.buffer, selection_start, selection_end - selection_start, "\n")
|
||||
active.cursor.offset = selection_start + 1
|
||||
_, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset)
|
||||
} else {
|
||||
cursor_insert_newline_auto_indent(&active.buffer, &active.cursor)
|
||||
}
|
||||
editor_clear_selection(active)
|
||||
editor_update_dirty(active)
|
||||
}
|
||||
|
||||
editor_save_active :: proc(editor: ^Editor) -> bool {
|
||||
active := editor_active_buffer(editor)
|
||||
if active == nil do return false
|
||||
|
||||
text := buffer_bytes(&active.buffer)
|
||||
defer delete(text)
|
||||
|
||||
err := os.write_entire_file(active.path, text[:])
|
||||
if err != nil {
|
||||
fmt.println("save failed:", err)
|
||||
return false
|
||||
}
|
||||
|
||||
active.saved_version = active.buffer.version
|
||||
active.dirty = false
|
||||
return true
|
||||
}
|
||||
|
||||
editor_save_all :: proc(editor: ^Editor) -> bool {
|
||||
ok := true
|
||||
for &buffer in editor.buffers {
|
||||
if !buffer.dirty do continue
|
||||
|
||||
text := buffer_bytes(&buffer.buffer)
|
||||
err := os.write_entire_file(buffer.path, text[:])
|
||||
delete(text)
|
||||
if err != nil {
|
||||
fmt.println("save failed:", buffer.path, err)
|
||||
ok = false
|
||||
} else {
|
||||
buffer.saved_version = buffer.buffer.version
|
||||
buffer.dirty = false
|
||||
}
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
editor_close_active :: proc(editor: ^Editor) -> bool {
|
||||
return editor_close_buffer(editor, editor.active)
|
||||
}
|
||||
|
||||
editor_close_buffer :: proc(editor: ^Editor, index: int) -> bool {
|
||||
if len(editor.buffers) <= 1 {
|
||||
editor_set_status(editor, "Cannot close the last buffer")
|
||||
return false
|
||||
}
|
||||
if index < 0 || index >= len(editor.buffers) do return false
|
||||
|
||||
active := &editor.buffers[index]
|
||||
if active.dirty {
|
||||
editor_set_status(editor, "Buffer has unsaved changes; save before closing")
|
||||
return false
|
||||
}
|
||||
|
||||
editor_buffer_destroy(active)
|
||||
ordered_remove(&editor.buffers, index)
|
||||
if editor.active > index {
|
||||
editor.active -= 1
|
||||
}
|
||||
editor.active = clamp_int(editor.active, 0, len(editor.buffers) - 1)
|
||||
editor_set_status(editor, "Closed buffer")
|
||||
return true
|
||||
}
|
||||
|
||||
editor_set_status :: proc(editor: ^Editor, message: string) {
|
||||
delete(editor.status)
|
||||
editor.status = strings.clone(message)
|
||||
}
|
||||
|
||||
editor_set_active_diagnostics :: proc(editor: ^Editor, diagnostics: []Diagnostic) {
|
||||
active := editor_active_buffer(editor)
|
||||
if active == nil do return
|
||||
|
||||
editor_buffer_set_diagnostics(active, diagnostics)
|
||||
}
|
||||
|
||||
editor_set_diagnostics_for_path :: proc(editor: ^Editor, path: string, diagnostics: []Diagnostic) -> bool {
|
||||
for &buffer in editor.buffers {
|
||||
if buffer.path == path || buffer.daemon_path == path {
|
||||
editor_buffer_set_diagnostics(&buffer, diagnostics)
|
||||
if buffer.daemon_path != path {
|
||||
delete(buffer.daemon_path)
|
||||
buffer.daemon_path = strings.clone(path)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
_, target_name := os.split_path(path)
|
||||
match_index := -1
|
||||
match_count := 0
|
||||
for &buffer, index in editor.buffers {
|
||||
_, buffer_name := os.split_path(buffer.path)
|
||||
if buffer_name == target_name {
|
||||
match_index = index
|
||||
match_count += 1
|
||||
}
|
||||
}
|
||||
if match_count == 1 {
|
||||
editor_buffer_set_diagnostics(&editor.buffers[match_index], diagnostics)
|
||||
delete(editor.buffers[match_index].daemon_path)
|
||||
editor.buffers[match_index].daemon_path = strings.clone(path)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
editor_clear_all_diagnostics :: proc(editor: ^Editor) {
|
||||
for &buffer in editor.buffers {
|
||||
editor_buffer_set_diagnostics(&buffer, []Diagnostic{})
|
||||
}
|
||||
}
|
||||
|
||||
editor_buffer_set_diagnostics :: proc(buffer: ^Editor_Buffer, diagnostics: []Diagnostic) {
|
||||
if buffer == nil do return
|
||||
|
||||
for diagnostic in buffer.diagnostics {
|
||||
delete(diagnostic.severity)
|
||||
delete(diagnostic.message)
|
||||
}
|
||||
delete(buffer.diagnostics)
|
||||
buffer.diagnostics = make([dynamic]Diagnostic)
|
||||
for diagnostic in diagnostics {
|
||||
append(&buffer.diagnostics, diagnostic)
|
||||
}
|
||||
}
|
||||
|
||||
editor_diagnostic_on_line :: proc(editor: ^Editor, line: int) -> (diagnostic: Diagnostic, ok: bool) {
|
||||
active := editor_active_buffer(editor)
|
||||
if active == nil do return {}, false
|
||||
|
||||
for diagnostic in active.diagnostics {
|
||||
if diagnostic.line == line {
|
||||
return diagnostic, true
|
||||
}
|
||||
}
|
||||
|
||||
return {}, false
|
||||
}
|
||||
|
||||
editor_diagnostic_counts :: proc(buffer: ^Editor_Buffer) -> (errors, warnings, infos: int) {
|
||||
if buffer == nil do return 0, 0, 0
|
||||
|
||||
for diagnostic in buffer.diagnostics {
|
||||
switch diagnostic.severity {
|
||||
case "error":
|
||||
errors += 1
|
||||
case "warning":
|
||||
warnings += 1
|
||||
case:
|
||||
infos += 1
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
editor_print_visible :: proc(editor: ^Editor, first_line, line_count: int) {
|
||||
active := editor_active_buffer(editor)
|
||||
if active == nil do return
|
||||
|
||||
last_line := min_int(first_line + line_count, buffer_line_count(&active.buffer))
|
||||
for line := first_line; line < last_line; line += 1 {
|
||||
bytes := buffer_line_bytes(&active.buffer, line)
|
||||
fmt.printf("%4d | %s\n", line + 1, string(bytes[:]))
|
||||
delete(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
editor_destroy :: proc(editor: ^Editor) {
|
||||
for &editor_buffer in editor.buffers {
|
||||
editor_buffer_destroy(&editor_buffer)
|
||||
}
|
||||
delete(editor.buffers)
|
||||
delete(editor.status)
|
||||
}
|
||||
|
||||
editor_buffer_destroy :: proc(editor_buffer: ^Editor_Buffer) {
|
||||
delete(editor_buffer.path)
|
||||
delete(editor_buffer.daemon_path)
|
||||
buffer_destroy(&editor_buffer.buffer)
|
||||
delete(editor_buffer.original_storage)
|
||||
for diagnostic in editor_buffer.diagnostics {
|
||||
delete(diagnostic.severity)
|
||||
delete(diagnostic.message)
|
||||
}
|
||||
delete(editor_buffer.diagnostics)
|
||||
}
|
||||
|
||||
min_int :: proc(a, b: int) -> int {
|
||||
if a < b do return a
|
||||
return b
|
||||
}
|
||||
581
client/odin/gpu_renderer.odin
Normal file
581
client/odin/gpu_renderer.odin
Normal file
|
|
@ -0,0 +1,581 @@
|
|||
package main
|
||||
|
||||
import "core:fmt"
|
||||
import "core:mem"
|
||||
import "core:os"
|
||||
import stbtt "vendor:stb/truetype"
|
||||
import SDL "vendor:sdl3"
|
||||
|
||||
GPU_Vertex :: struct {
|
||||
pos: [2]f32,
|
||||
color: [4]f32,
|
||||
}
|
||||
|
||||
GPU_Text_Vertex :: struct {
|
||||
pos: [2]f32,
|
||||
uv: [2]f32,
|
||||
color: [4]f32,
|
||||
}
|
||||
|
||||
GPU_Uniforms :: struct {
|
||||
viewport: [2]f32,
|
||||
}
|
||||
|
||||
GPU_Renderer :: struct {
|
||||
available: bool,
|
||||
device: ^SDL.GPUDevice,
|
||||
window: ^SDL.Window,
|
||||
pipeline: ^SDL.GPUGraphicsPipeline,
|
||||
text_pipeline: ^SDL.GPUGraphicsPipeline,
|
||||
vertex_shader: ^SDL.GPUShader,
|
||||
fragment_shader: ^SDL.GPUShader,
|
||||
text_vertex_shader: ^SDL.GPUShader,
|
||||
text_fragment_shader: ^SDL.GPUShader,
|
||||
vertex_buffer: ^SDL.GPUBuffer,
|
||||
transfer_buffer: ^SDL.GPUTransferBuffer,
|
||||
text_vertex_buffer: ^SDL.GPUBuffer,
|
||||
text_transfer_buffer: ^SDL.GPUTransferBuffer,
|
||||
font_texture: ^SDL.GPUTexture,
|
||||
font_sampler: ^SDL.GPUSampler,
|
||||
font_chars: [95]stbtt.bakedchar,
|
||||
font_advance: f32,
|
||||
vertices: [dynamic]GPU_Vertex,
|
||||
text_vertices: [dynamic]GPU_Text_Vertex,
|
||||
max_vertices: int,
|
||||
max_text_vertices: int,
|
||||
width: int,
|
||||
height: int,
|
||||
}
|
||||
|
||||
GPU_MAX_VERTICES :: 240000
|
||||
GPU_MAX_TEXT_VERTICES :: 120000
|
||||
GPU_FONT_ATLAS_SIZE :: 512
|
||||
GPU_FONT_PIXEL_HEIGHT :: 15.0
|
||||
GPU_FONT_BASELINE_OFFSET :: 11.0
|
||||
GPU_FONT_CELL_PADDING :: 1.0
|
||||
GPU_FONT_PATHS := [?]string{
|
||||
"client/odin/assets/fonts/EditorMono.ttf",
|
||||
"client/odin/assets/fonts/NotoSansMono-Regular.ttf",
|
||||
"/usr/share/fonts/google-noto/NotoSansMono-Regular.ttf",
|
||||
"/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono.ttf",
|
||||
"/usr/share/fonts/dejavu/DejaVuSansMono.ttf",
|
||||
}
|
||||
|
||||
gpu_renderer_make :: proc(window: ^SDL.Window) -> GPU_Renderer {
|
||||
gpu: GPU_Renderer
|
||||
gpu.window = window
|
||||
gpu.max_vertices = GPU_MAX_VERTICES
|
||||
gpu.max_text_vertices = GPU_MAX_TEXT_VERTICES
|
||||
gpu.width = SDL_WINDOW_WIDTH
|
||||
gpu.height = SDL_WINDOW_HEIGHT
|
||||
gpu.vertices = make([dynamic]GPU_Vertex, 0, gpu.max_vertices)
|
||||
gpu.text_vertices = make([dynamic]GPU_Text_Vertex, 0, gpu.max_text_vertices)
|
||||
|
||||
gpu.device = SDL.CreateGPUDevice(SDL.GPUShaderFormat{.SPIRV}, true, nil)
|
||||
if gpu.device == nil {
|
||||
fmt.println("SDL GPU device failed, falling back:", SDL.GetError())
|
||||
return gpu
|
||||
}
|
||||
|
||||
if !SDL.ClaimWindowForGPUDevice(gpu.device, window) {
|
||||
fmt.println("SDL GPU window claim failed, falling back:", SDL.GetError())
|
||||
gpu_renderer_destroy(&gpu)
|
||||
return GPU_Renderer{}
|
||||
}
|
||||
|
||||
gpu.vertex_shader = gpu_create_shader(gpu.device, "client/odin/shaders/compiled/rect.vert.spv", .VERTEX)
|
||||
gpu.fragment_shader = gpu_create_shader(gpu.device, "client/odin/shaders/compiled/rect.frag.spv", .FRAGMENT)
|
||||
gpu.text_vertex_shader = gpu_create_shader(gpu.device, "client/odin/shaders/compiled/text.vert.spv", .VERTEX)
|
||||
gpu.text_fragment_shader = gpu_create_shader(gpu.device, "client/odin/shaders/compiled/text.frag.spv", .FRAGMENT)
|
||||
if gpu.vertex_shader == nil || gpu.fragment_shader == nil || gpu.text_vertex_shader == nil || gpu.text_fragment_shader == nil {
|
||||
fmt.println("SDL GPU shaders unavailable; run scripts/compile-shaders.sh")
|
||||
gpu_renderer_destroy(&gpu)
|
||||
return GPU_Renderer{}
|
||||
}
|
||||
|
||||
color_format := SDL.GetGPUSwapchainTextureFormat(gpu.device, window)
|
||||
if color_format == .INVALID {
|
||||
fmt.println("SDL GPU swapchain format failed, falling back:", SDL.GetError())
|
||||
gpu_renderer_destroy(&gpu)
|
||||
return GPU_Renderer{}
|
||||
}
|
||||
|
||||
vb_desc := SDL.GPUVertexBufferDescription{
|
||||
slot = 0,
|
||||
pitch = size_of(GPU_Vertex),
|
||||
input_rate = .VERTEX,
|
||||
instance_step_rate = 0,
|
||||
}
|
||||
attrs := [?]SDL.GPUVertexAttribute{
|
||||
{location = 0, buffer_slot = 0, format = .FLOAT2, offset = u32(offset_of(GPU_Vertex, pos))},
|
||||
{location = 1, buffer_slot = 0, format = .FLOAT4, offset = u32(offset_of(GPU_Vertex, color))},
|
||||
}
|
||||
blend := SDL.GPUColorTargetBlendState{
|
||||
src_color_blendfactor = .SRC_ALPHA,
|
||||
dst_color_blendfactor = .ONE_MINUS_SRC_ALPHA,
|
||||
color_blend_op = .ADD,
|
||||
src_alpha_blendfactor = .ONE,
|
||||
dst_alpha_blendfactor = .ONE_MINUS_SRC_ALPHA,
|
||||
alpha_blend_op = .ADD,
|
||||
color_write_mask = SDL.GPUColorComponentFlags{.R, .G, .B, .A},
|
||||
enable_blend = true,
|
||||
enable_color_write_mask = true,
|
||||
}
|
||||
color_target := SDL.GPUColorTargetDescription{format = color_format, blend_state = blend}
|
||||
pipeline_info := SDL.GPUGraphicsPipelineCreateInfo{
|
||||
vertex_shader = gpu.vertex_shader,
|
||||
fragment_shader = gpu.fragment_shader,
|
||||
vertex_input_state = {
|
||||
vertex_buffer_descriptions = &vb_desc,
|
||||
num_vertex_buffers = 1,
|
||||
vertex_attributes = &attrs[0],
|
||||
num_vertex_attributes = len(attrs),
|
||||
},
|
||||
primitive_type = .TRIANGLELIST,
|
||||
rasterizer_state = {fill_mode = .FILL, cull_mode = .NONE, front_face = .COUNTER_CLOCKWISE},
|
||||
multisample_state = {sample_count = ._1},
|
||||
depth_stencil_state = {},
|
||||
target_info = {
|
||||
color_target_descriptions = &color_target,
|
||||
num_color_targets = 1,
|
||||
has_depth_stencil_target = false,
|
||||
},
|
||||
}
|
||||
|
||||
gpu.pipeline = SDL.CreateGPUGraphicsPipeline(gpu.device, pipeline_info)
|
||||
if gpu.pipeline == nil {
|
||||
fmt.println("SDL GPU pipeline failed, falling back:", SDL.GetError())
|
||||
gpu_renderer_destroy(&gpu)
|
||||
return GPU_Renderer{}
|
||||
}
|
||||
|
||||
text_vb_desc := SDL.GPUVertexBufferDescription{
|
||||
slot = 0,
|
||||
pitch = size_of(GPU_Text_Vertex),
|
||||
input_rate = .VERTEX,
|
||||
instance_step_rate = 0,
|
||||
}
|
||||
text_attrs := [?]SDL.GPUVertexAttribute{
|
||||
{location = 0, buffer_slot = 0, format = .FLOAT2, offset = u32(offset_of(GPU_Text_Vertex, pos))},
|
||||
{location = 1, buffer_slot = 0, format = .FLOAT2, offset = u32(offset_of(GPU_Text_Vertex, uv))},
|
||||
{location = 2, buffer_slot = 0, format = .FLOAT4, offset = u32(offset_of(GPU_Text_Vertex, color))},
|
||||
}
|
||||
text_pipeline_info := pipeline_info
|
||||
text_pipeline_info.vertex_shader = gpu.text_vertex_shader
|
||||
text_pipeline_info.fragment_shader = gpu.text_fragment_shader
|
||||
text_pipeline_info.vertex_input_state = {
|
||||
vertex_buffer_descriptions = &text_vb_desc,
|
||||
num_vertex_buffers = 1,
|
||||
vertex_attributes = &text_attrs[0],
|
||||
num_vertex_attributes = len(text_attrs),
|
||||
}
|
||||
gpu.text_pipeline = SDL.CreateGPUGraphicsPipeline(gpu.device, text_pipeline_info)
|
||||
if gpu.text_pipeline == nil {
|
||||
fmt.println("SDL GPU text pipeline failed, falling back:", SDL.GetError())
|
||||
gpu_renderer_destroy(&gpu)
|
||||
return GPU_Renderer{}
|
||||
}
|
||||
|
||||
buffer_size := u32(gpu.max_vertices * size_of(GPU_Vertex))
|
||||
gpu.vertex_buffer = SDL.CreateGPUBuffer(gpu.device, {usage = {.VERTEX}, size = buffer_size})
|
||||
gpu.transfer_buffer = SDL.CreateGPUTransferBuffer(gpu.device, {usage = .UPLOAD, size = buffer_size})
|
||||
text_buffer_size := u32(gpu.max_text_vertices * size_of(GPU_Text_Vertex))
|
||||
gpu.text_vertex_buffer = SDL.CreateGPUBuffer(gpu.device, {usage = {.VERTEX}, size = text_buffer_size})
|
||||
gpu.text_transfer_buffer = SDL.CreateGPUTransferBuffer(gpu.device, {usage = .UPLOAD, size = text_buffer_size})
|
||||
if gpu.vertex_buffer == nil || gpu.transfer_buffer == nil || gpu.text_vertex_buffer == nil || gpu.text_transfer_buffer == nil {
|
||||
fmt.println("SDL GPU buffers failed, falling back:", SDL.GetError())
|
||||
gpu_renderer_destroy(&gpu)
|
||||
return GPU_Renderer{}
|
||||
}
|
||||
|
||||
if !gpu_create_font_atlas(&gpu) {
|
||||
gpu_renderer_destroy(&gpu)
|
||||
return GPU_Renderer{}
|
||||
}
|
||||
|
||||
gpu.available = true
|
||||
return gpu
|
||||
}
|
||||
|
||||
gpu_renderer_destroy :: proc(gpu: ^GPU_Renderer) {
|
||||
if gpu.device != nil {
|
||||
_ = SDL.WaitForGPUIdle(gpu.device)
|
||||
if gpu.window != nil {
|
||||
SDL.ReleaseWindowFromGPUDevice(gpu.device, gpu.window)
|
||||
}
|
||||
if gpu.transfer_buffer != nil do SDL.ReleaseGPUTransferBuffer(gpu.device, gpu.transfer_buffer)
|
||||
if gpu.text_transfer_buffer != nil do SDL.ReleaseGPUTransferBuffer(gpu.device, gpu.text_transfer_buffer)
|
||||
if gpu.vertex_buffer != nil do SDL.ReleaseGPUBuffer(gpu.device, gpu.vertex_buffer)
|
||||
if gpu.text_vertex_buffer != nil do SDL.ReleaseGPUBuffer(gpu.device, gpu.text_vertex_buffer)
|
||||
if gpu.font_sampler != nil do SDL.ReleaseGPUSampler(gpu.device, gpu.font_sampler)
|
||||
if gpu.font_texture != nil do SDL.ReleaseGPUTexture(gpu.device, gpu.font_texture)
|
||||
if gpu.pipeline != nil do SDL.ReleaseGPUGraphicsPipeline(gpu.device, gpu.pipeline)
|
||||
if gpu.text_pipeline != nil do SDL.ReleaseGPUGraphicsPipeline(gpu.device, gpu.text_pipeline)
|
||||
if gpu.fragment_shader != nil do SDL.ReleaseGPUShader(gpu.device, gpu.fragment_shader)
|
||||
if gpu.vertex_shader != nil do SDL.ReleaseGPUShader(gpu.device, gpu.vertex_shader)
|
||||
if gpu.text_fragment_shader != nil do SDL.ReleaseGPUShader(gpu.device, gpu.text_fragment_shader)
|
||||
if gpu.text_vertex_shader != nil do SDL.ReleaseGPUShader(gpu.device, gpu.text_vertex_shader)
|
||||
SDL.DestroyGPUDevice(gpu.device)
|
||||
}
|
||||
delete(gpu.vertices)
|
||||
delete(gpu.text_vertices)
|
||||
gpu^ = {}
|
||||
}
|
||||
|
||||
gpu_create_shader :: proc(device: ^SDL.GPUDevice, path: string, stage: SDL.GPUShaderStage) -> ^SDL.GPUShader {
|
||||
bytes, err := os.read_entire_file(path, context.allocator)
|
||||
if err != nil {
|
||||
fmt.println("read shader failed:", path, err)
|
||||
return nil
|
||||
}
|
||||
defer delete(bytes)
|
||||
|
||||
shader := SDL.CreateGPUShader(device, {
|
||||
code_size = len(bytes),
|
||||
code = raw_data(bytes),
|
||||
entrypoint = "main",
|
||||
format = {.SPIRV},
|
||||
stage = stage,
|
||||
num_uniform_buffers = 1 if stage == .VERTEX else 0,
|
||||
num_samplers = 1 if stage == .FRAGMENT && strings_has_suffix(path, "text.frag.spv") else 0,
|
||||
})
|
||||
if shader == nil {
|
||||
fmt.println("CreateGPUShader failed:", path, SDL.GetError())
|
||||
}
|
||||
return shader
|
||||
}
|
||||
|
||||
gpu_begin :: proc(gpu: ^GPU_Renderer) {
|
||||
clear(&gpu.vertices)
|
||||
clear(&gpu.text_vertices)
|
||||
w, h: i32
|
||||
if SDL.GetWindowSize(gpu.window, &w, &h) {
|
||||
gpu.width = int(w)
|
||||
gpu.height = int(h)
|
||||
}
|
||||
}
|
||||
|
||||
gpu_rect :: proc(gpu: ^GPU_Renderer, x, y, w, h: f32, r, g, b, a: u8) {
|
||||
if w <= 0 || h <= 0 do return
|
||||
c := color_f32(r, g, b, a)
|
||||
gpu_push_quad(gpu, x, y, x + w, y + h, c)
|
||||
}
|
||||
|
||||
gpu_line :: proc(gpu: ^GPU_Renderer, x1, y1, x2, y2: f32, r, g, b, a: u8) {
|
||||
if abs_f32(x2 - x1) < 1 {
|
||||
x := min_f32(x1, x2)
|
||||
y := min_f32(y1, y2)
|
||||
gpu_rect(gpu, x, y, 1, abs_f32(y2 - y1), r, g, b, a)
|
||||
} else if abs_f32(y2 - y1) < 1 {
|
||||
x := min_f32(x1, x2)
|
||||
y := min_f32(y1, y2)
|
||||
gpu_rect(gpu, x, y, abs_f32(x2 - x1), 1, r, g, b, a)
|
||||
} else {
|
||||
gpu_rect(gpu, x1, y1, max_f32(abs_f32(x2 - x1), 1), 1, r, g, b, a)
|
||||
}
|
||||
}
|
||||
|
||||
gpu_text :: proc(gpu: ^GPU_Renderer, x, y: f32, text: string, r, g, b, a: u8) {
|
||||
if len(text) == 0 do return
|
||||
xpos := round_f32(x)
|
||||
ypos := y + GPU_FONT_BASELINE_OFFSET
|
||||
color := color_f32(r, g, b, a)
|
||||
for raw_ch in transmute([]u8)text {
|
||||
ch := raw_ch
|
||||
if ch == '\n' {
|
||||
xpos = x
|
||||
ypos += SDL_LINE_HEIGHT
|
||||
continue
|
||||
}
|
||||
if ch < 32 || ch > 126 {
|
||||
ch = '?'
|
||||
}
|
||||
if ch == ' ' {
|
||||
xpos += gpu.font_advance
|
||||
continue
|
||||
}
|
||||
glyph := gpu.font_chars[ch - 32]
|
||||
glyph_w := f32(glyph.x1 - glyph.x0)
|
||||
glyph_h := f32(glyph.y1 - glyph.y0)
|
||||
x0 := round_f32(xpos + glyph.xoff)
|
||||
y0 := round_f32(ypos + glyph.yoff)
|
||||
x1 := x0 + glyph_w
|
||||
y1 := y0 + glyph_h
|
||||
s0 := f32(glyph.x0) / GPU_FONT_ATLAS_SIZE
|
||||
t0 := f32(glyph.y0) / GPU_FONT_ATLAS_SIZE
|
||||
s1 := f32(glyph.x1) / GPU_FONT_ATLAS_SIZE
|
||||
t1 := f32(glyph.y1) / GPU_FONT_ATLAS_SIZE
|
||||
gpu_push_text_quad(gpu, x0, y0, x1, y1, s0, t0, s1, t1, color)
|
||||
xpos += gpu.font_advance
|
||||
}
|
||||
}
|
||||
|
||||
gpu_text_limited :: proc(gpu: ^GPU_Renderer, x, y: f32, text: string, max_chars: int, r, g, b, a: u8) {
|
||||
if max_chars <= 0 do return
|
||||
if len(text) <= max_chars {
|
||||
gpu_text(gpu, x, y, text, r, g, b, a)
|
||||
} else if max_chars <= 3 {
|
||||
gpu_text(gpu, x, y, text[:max_chars], r, g, b, a)
|
||||
} else {
|
||||
clipped := fmt.tprintf("%s...", text[:max_chars - 3])
|
||||
gpu_text(gpu, x, y, clipped, r, g, b, a)
|
||||
}
|
||||
}
|
||||
|
||||
gpu_text_width :: proc(text: string) -> int {
|
||||
if len(text) == 0 do return 0
|
||||
width: f32 = 0
|
||||
max_width: f32 = 0
|
||||
for raw_ch in transmute([]u8)text {
|
||||
ch := raw_ch
|
||||
if ch == '\n' {
|
||||
if width > max_width do max_width = width
|
||||
width = 0
|
||||
continue
|
||||
}
|
||||
if ch < 32 || ch > 126 do ch = '?'
|
||||
width += gpu_global_font_advance(ch)
|
||||
}
|
||||
if width > max_width do max_width = width
|
||||
return int(max_width + 0.5)
|
||||
}
|
||||
|
||||
gpu_font_text_advance :: proc(gpu: ^GPU_Renderer, text: string) -> f32 {
|
||||
if len(text) == 0 do return 0
|
||||
|
||||
start_x: f32 = 0
|
||||
x: f32 = start_x
|
||||
y: f32 = GPU_FONT_BASELINE_OFFSET
|
||||
max_width: f32 = 0
|
||||
for raw_ch in transmute([]u8)text {
|
||||
ch := raw_ch
|
||||
if ch == '\n' {
|
||||
if x - start_x > max_width do max_width = x - start_x
|
||||
x = start_x
|
||||
y += SDL_LINE_HEIGHT
|
||||
continue
|
||||
}
|
||||
if ch < 32 || ch > 126 do ch = '?'
|
||||
quad: stbtt.aligned_quad
|
||||
x += gpu.font_advance
|
||||
}
|
||||
if x - start_x > max_width do max_width = x - start_x
|
||||
return max_width
|
||||
}
|
||||
|
||||
gpu_font_text_width :: proc(gpu: ^GPU_Renderer, text: string) -> int {
|
||||
return int(gpu_font_text_advance(gpu, text) + 0.5)
|
||||
}
|
||||
|
||||
gpu_present :: proc(gpu: ^GPU_Renderer) {
|
||||
if !gpu.available do return
|
||||
|
||||
command := SDL.AcquireGPUCommandBuffer(gpu.device)
|
||||
if command == nil do return
|
||||
|
||||
texture: ^SDL.GPUTexture
|
||||
width, height: u32
|
||||
if !SDL.WaitAndAcquireGPUSwapchainTexture(command, gpu.window, &texture, &width, &height) || texture == nil {
|
||||
_ = SDL.CancelGPUCommandBuffer(command)
|
||||
return
|
||||
}
|
||||
|
||||
vertex_count := len(gpu.vertices)
|
||||
text_vertex_count := len(gpu.text_vertices)
|
||||
if vertex_count > 0 {
|
||||
size := vertex_count * size_of(GPU_Vertex)
|
||||
mapped := SDL.MapGPUTransferBuffer(gpu.device, gpu.transfer_buffer, true)
|
||||
if mapped != nil {
|
||||
mem.copy(transmute([^]u8)mapped, raw_data(gpu.vertices[:]), size)
|
||||
SDL.UnmapGPUTransferBuffer(gpu.device, gpu.transfer_buffer)
|
||||
|
||||
copy_pass := SDL.BeginGPUCopyPass(command)
|
||||
SDL.UploadToGPUBuffer(copy_pass, {transfer_buffer = gpu.transfer_buffer}, {buffer = gpu.vertex_buffer, size = u32(size)}, true)
|
||||
SDL.EndGPUCopyPass(copy_pass)
|
||||
}
|
||||
}
|
||||
if text_vertex_count > 0 {
|
||||
size := text_vertex_count * size_of(GPU_Text_Vertex)
|
||||
mapped := SDL.MapGPUTransferBuffer(gpu.device, gpu.text_transfer_buffer, true)
|
||||
if mapped != nil {
|
||||
mem.copy(transmute([^]u8)mapped, raw_data(gpu.text_vertices[:]), size)
|
||||
SDL.UnmapGPUTransferBuffer(gpu.device, gpu.text_transfer_buffer)
|
||||
|
||||
copy_pass := SDL.BeginGPUCopyPass(command)
|
||||
SDL.UploadToGPUBuffer(copy_pass, {transfer_buffer = gpu.text_transfer_buffer}, {buffer = gpu.text_vertex_buffer, size = u32(size)}, true)
|
||||
SDL.EndGPUCopyPass(copy_pass)
|
||||
}
|
||||
}
|
||||
|
||||
target := SDL.GPUColorTargetInfo{
|
||||
texture = texture,
|
||||
clear_color = SDL.FColor{15.0 / 255.0, 17.0 / 255.0, 22.0 / 255.0, 1},
|
||||
load_op = .CLEAR,
|
||||
store_op = .STORE,
|
||||
}
|
||||
pass := SDL.BeginGPURenderPass(command, &target, 1, nil)
|
||||
if vertex_count > 0 {
|
||||
uniforms := GPU_Uniforms{viewport = {f32(gpu.width), f32(gpu.height)}}
|
||||
SDL.PushGPUVertexUniformData(command, 0, &uniforms, size_of(uniforms))
|
||||
SDL.BindGPUGraphicsPipeline(pass, gpu.pipeline)
|
||||
SDL.BindGPUVertexBuffers(pass, 0, &(SDL.GPUBufferBinding{buffer = gpu.vertex_buffer}), 1)
|
||||
SDL.DrawGPUPrimitives(pass, u32(vertex_count), 1, 0, 0)
|
||||
}
|
||||
if text_vertex_count > 0 {
|
||||
uniforms := GPU_Uniforms{viewport = {f32(gpu.width), f32(gpu.height)}}
|
||||
binding := SDL.GPUTextureSamplerBinding{texture = gpu.font_texture, sampler = gpu.font_sampler}
|
||||
SDL.PushGPUVertexUniformData(command, 0, &uniforms, size_of(uniforms))
|
||||
SDL.BindGPUGraphicsPipeline(pass, gpu.text_pipeline)
|
||||
SDL.BindGPUVertexBuffers(pass, 0, &(SDL.GPUBufferBinding{buffer = gpu.text_vertex_buffer}), 1)
|
||||
SDL.BindGPUFragmentSamplers(pass, 0, &binding, 1)
|
||||
SDL.DrawGPUPrimitives(pass, u32(text_vertex_count), 1, 0, 0)
|
||||
}
|
||||
SDL.EndGPURenderPass(pass)
|
||||
_ = SDL.SubmitGPUCommandBuffer(command)
|
||||
}
|
||||
|
||||
gpu_create_font_atlas :: proc(gpu: ^GPU_Renderer) -> bool {
|
||||
font_bytes, font_path, ok := gpu_read_font_file()
|
||||
if !ok {
|
||||
fmt.println("read font failed: no configured monospace font found")
|
||||
return false
|
||||
}
|
||||
defer delete(font_bytes)
|
||||
|
||||
atlas_size := GPU_FONT_ATLAS_SIZE * GPU_FONT_ATLAS_SIZE
|
||||
atlas := make([]u8, atlas_size)
|
||||
defer delete(atlas)
|
||||
|
||||
baked := stbtt.BakeFontBitmap(raw_data(font_bytes), 0, GPU_FONT_PIXEL_HEIGHT, raw_data(atlas), GPU_FONT_ATLAS_SIZE, GPU_FONT_ATLAS_SIZE, 32, len(gpu.font_chars), &gpu.font_chars[0])
|
||||
if baked <= 0 {
|
||||
fmt.println("font bake failed:", font_path)
|
||||
return false
|
||||
}
|
||||
widest_advance: f32 = 0
|
||||
for char in gpu.font_chars {
|
||||
glyph_width := char.xoff + f32(char.x1 - char.x0)
|
||||
widest_advance = max_f32(widest_advance, max_f32(char.xadvance, glyph_width))
|
||||
}
|
||||
gpu.font_advance = max_f32(ceil_f32(widest_advance + GPU_FONT_CELL_PADDING), 1)
|
||||
|
||||
gpu.font_texture = SDL.CreateGPUTexture(gpu.device, {
|
||||
type = .D2,
|
||||
format = .R8_UNORM,
|
||||
usage = {.SAMPLER},
|
||||
width = GPU_FONT_ATLAS_SIZE,
|
||||
height = GPU_FONT_ATLAS_SIZE,
|
||||
layer_count_or_depth = 1,
|
||||
num_levels = 1,
|
||||
sample_count = ._1,
|
||||
})
|
||||
gpu.font_sampler = SDL.CreateGPUSampler(gpu.device, {
|
||||
min_filter = .NEAREST,
|
||||
mag_filter = .NEAREST,
|
||||
mipmap_mode = .NEAREST,
|
||||
address_mode_u = .CLAMP_TO_EDGE,
|
||||
address_mode_v = .CLAMP_TO_EDGE,
|
||||
address_mode_w = .CLAMP_TO_EDGE,
|
||||
})
|
||||
font_transfer := SDL.CreateGPUTransferBuffer(gpu.device, {usage = .UPLOAD, size = u32(atlas_size)})
|
||||
if gpu.font_texture == nil || gpu.font_sampler == nil || font_transfer == nil {
|
||||
fmt.println("font GPU resources failed:", SDL.GetError())
|
||||
if font_transfer != nil do SDL.ReleaseGPUTransferBuffer(gpu.device, font_transfer)
|
||||
return false
|
||||
}
|
||||
defer SDL.ReleaseGPUTransferBuffer(gpu.device, font_transfer)
|
||||
|
||||
mapped := SDL.MapGPUTransferBuffer(gpu.device, font_transfer, false)
|
||||
if mapped == nil {
|
||||
fmt.println("font transfer map failed:", SDL.GetError())
|
||||
return false
|
||||
}
|
||||
mem.copy(transmute([^]u8)mapped, raw_data(atlas), atlas_size)
|
||||
SDL.UnmapGPUTransferBuffer(gpu.device, font_transfer)
|
||||
|
||||
command := SDL.AcquireGPUCommandBuffer(gpu.device)
|
||||
copy_pass := SDL.BeginGPUCopyPass(command)
|
||||
SDL.UploadToGPUTexture(copy_pass, {
|
||||
transfer_buffer = font_transfer,
|
||||
pixels_per_row = GPU_FONT_ATLAS_SIZE,
|
||||
rows_per_layer = GPU_FONT_ATLAS_SIZE,
|
||||
}, {
|
||||
texture = gpu.font_texture,
|
||||
w = GPU_FONT_ATLAS_SIZE,
|
||||
h = GPU_FONT_ATLAS_SIZE,
|
||||
d = 1,
|
||||
}, false)
|
||||
SDL.EndGPUCopyPass(copy_pass)
|
||||
return SDL.SubmitGPUCommandBuffer(command)
|
||||
}
|
||||
|
||||
gpu_read_font_file :: proc() -> ([]u8, string, bool) {
|
||||
for path in GPU_FONT_PATHS {
|
||||
bytes, err := os.read_entire_file(path, context.allocator)
|
||||
if err == nil && len(bytes) > 0 {
|
||||
return bytes, path, true
|
||||
}
|
||||
if err == nil {
|
||||
delete(bytes)
|
||||
}
|
||||
}
|
||||
return nil, "", false
|
||||
}
|
||||
|
||||
gpu_push_quad :: proc(gpu: ^GPU_Renderer, x0, y0, x1, y1: f32, c: [4]f32) {
|
||||
if len(gpu.vertices) + 6 > gpu.max_vertices do return
|
||||
append(&gpu.vertices, GPU_Vertex{{x0, y0}, c})
|
||||
append(&gpu.vertices, GPU_Vertex{{x1, y0}, c})
|
||||
append(&gpu.vertices, GPU_Vertex{{x0, y1}, c})
|
||||
append(&gpu.vertices, GPU_Vertex{{x0, y1}, c})
|
||||
append(&gpu.vertices, GPU_Vertex{{x1, y0}, c})
|
||||
append(&gpu.vertices, GPU_Vertex{{x1, y1}, c})
|
||||
}
|
||||
|
||||
gpu_push_text_quad :: proc(gpu: ^GPU_Renderer, x0, y0, x1, y1, s0, t0, s1, t1: f32, c: [4]f32) {
|
||||
if len(gpu.text_vertices) + 6 > gpu.max_text_vertices do return
|
||||
append(&gpu.text_vertices, GPU_Text_Vertex{{x0, y0}, {s0, t0}, c})
|
||||
append(&gpu.text_vertices, GPU_Text_Vertex{{x1, y0}, {s1, t0}, c})
|
||||
append(&gpu.text_vertices, GPU_Text_Vertex{{x0, y1}, {s0, t1}, c})
|
||||
append(&gpu.text_vertices, GPU_Text_Vertex{{x0, y1}, {s0, t1}, c})
|
||||
append(&gpu.text_vertices, GPU_Text_Vertex{{x1, y0}, {s1, t0}, c})
|
||||
append(&gpu.text_vertices, GPU_Text_Vertex{{x1, y1}, {s1, t1}, c})
|
||||
}
|
||||
|
||||
gpu_global_font_advance :: proc(ch: u8) -> f32 {
|
||||
return 8
|
||||
}
|
||||
|
||||
strings_has_suffix :: proc(s, suffix: string) -> bool {
|
||||
if len(suffix) > len(s) do return false
|
||||
return s[len(s) - len(suffix):] == suffix
|
||||
}
|
||||
|
||||
color_f32 :: proc(r, g, b, a: u8) -> [4]f32 {
|
||||
return {f32(r) / 255.0, f32(g) / 255.0, f32(b) / 255.0, f32(a) / 255.0}
|
||||
}
|
||||
|
||||
abs_f32 :: proc(v: f32) -> f32 {
|
||||
if v < 0 do return -v
|
||||
return v
|
||||
}
|
||||
|
||||
min_f32 :: proc(a, b: f32) -> f32 {
|
||||
if a < b do return a
|
||||
return b
|
||||
}
|
||||
|
||||
max_f32 :: proc(a, b: f32) -> f32 {
|
||||
if a > b do return a
|
||||
return b
|
||||
}
|
||||
|
||||
round_f32 :: proc(v: f32) -> f32 {
|
||||
if v >= 0 do return f32(int(v + 0.5))
|
||||
return f32(int(v - 0.5))
|
||||
}
|
||||
|
||||
ceil_f32 :: proc(v: f32) -> f32 {
|
||||
i := int(v)
|
||||
if f32(i) < v do return f32(i + 1)
|
||||
return f32(i)
|
||||
}
|
||||
184
client/odin/main.odin
Normal file
184
client/odin/main.odin
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
package main
|
||||
|
||||
import "core:fmt"
|
||||
import "core:net"
|
||||
import "core:os"
|
||||
import "core:strconv"
|
||||
|
||||
main :: proc() {
|
||||
args := os.args
|
||||
if len(args) < 2 {
|
||||
fmt.println("usage: odin run client/odin -- <port> [workspace]")
|
||||
fmt.println(" or: odin run client/odin -- --sdl [workspace] [port] [file]")
|
||||
return
|
||||
}
|
||||
|
||||
if args[1] == "--sdl" {
|
||||
workspace := "."
|
||||
if len(args) >= 3 {
|
||||
workspace = args[2]
|
||||
}
|
||||
port := 0
|
||||
file := ""
|
||||
if len(args) >= 4 {
|
||||
parsed_port, parsed_ok := strconv.parse_int(args[3])
|
||||
if parsed_ok {
|
||||
port = int(parsed_port)
|
||||
} else {
|
||||
file = args[3]
|
||||
}
|
||||
}
|
||||
if len(args) >= 5 {
|
||||
file = args[4]
|
||||
}
|
||||
run_sdl_editor(workspace, port, file)
|
||||
return
|
||||
}
|
||||
|
||||
port, ok := strconv.parse_int(args[1])
|
||||
if !ok {
|
||||
fmt.println("invalid port")
|
||||
return
|
||||
}
|
||||
|
||||
workspace := "."
|
||||
if len(args) >= 3 {
|
||||
workspace = args[2]
|
||||
}
|
||||
|
||||
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\":%q}}}}\n", 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\":%q,\"version\":1,\"text\":%q}}}}\n", diagnostics_path, string(active_text[:]))
|
||||
send_line(socket, text_change_request)
|
||||
read_messages(socket, 2)
|
||||
|
||||
diagnostics_request := fmt.tprintf("{{\"id\":5,\"method\":\"kotlin/diagnostics\",\"params\":{{\"path\":%q}}}}\n", diagnostics_path)
|
||||
send_line(socket, diagnostics_request)
|
||||
read_messages(socket, 1)
|
||||
|
||||
completion_request := fmt.tprintf("{{\"id\":6,\"method\":\"kotlin/completion\",\"params\":{{\"path\":%q,\"line\":1,\"column\":1}}}}\n", diagnostics_path)
|
||||
send_line(socket, completion_request)
|
||||
read_messages(socket, 1)
|
||||
|
||||
hover_request := fmt.tprintf("{{\"id\":7,\"method\":\"kotlin/hover\",\"params\":{{\"path\":%q,\"line\":1,\"column\":1}}}}\n", diagnostics_path)
|
||||
send_line(socket, hover_request)
|
||||
read_messages(socket, 1)
|
||||
|
||||
definition_request := fmt.tprintf("{{\"id\":8,\"method\":\"kotlin/definition\",\"params\":{{\"path\":%q,\"line\":1,\"column\":5}}}}\n", diagnostics_path)
|
||||
send_line(socket, definition_request)
|
||||
read_messages(socket, 1)
|
||||
|
||||
references_request := fmt.tprintf("{{\"id\":9,\"method\":\"kotlin/references\",\"params\":{{\"path\":%q,\"line\":1,\"column\":5}}}}\n", diagnostics_path)
|
||||
send_line(socket, references_request)
|
||||
read_messages(socket, 1)
|
||||
|
||||
rename_request := fmt.tprintf("{{\"id\":10,\"method\":\"kotlin/rename\",\"params\":{{\"path\":%q,\"line\":1,\"column\":5,\"newName\":\"renamedBroken\"}}}}\n", 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
|
||||
}
|
||||
3847
client/odin/sdl_app.odin
Normal file
3847
client/odin/sdl_app.odin
Normal file
File diff suppressed because it is too large
Load diff
8
client/odin/shaders/rect.frag.glsl
Normal file
8
client/odin/shaders/rect.frag.glsl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#version 450
|
||||
|
||||
layout(location = 0) in vec4 v_color;
|
||||
layout(location = 0) out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
out_color = v_color;
|
||||
}
|
||||
17
client/odin/shaders/rect.vert.glsl
Normal file
17
client/odin/shaders/rect.vert.glsl
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#version 450
|
||||
|
||||
layout(location = 0) in vec2 a_pos;
|
||||
layout(location = 1) in vec4 a_color;
|
||||
|
||||
layout(set = 1, binding = 0) uniform VertexUniforms {
|
||||
vec2 u_viewport;
|
||||
};
|
||||
|
||||
layout(location = 0) out vec4 v_color;
|
||||
|
||||
void main() {
|
||||
vec2 ndc = vec2((a_pos.x / u_viewport.x) * 2.0 - 1.0,
|
||||
1.0 - (a_pos.y / u_viewport.y) * 2.0);
|
||||
gl_Position = vec4(ndc, 0.0, 1.0);
|
||||
v_color = a_color;
|
||||
}
|
||||
12
client/odin/shaders/text.frag.glsl
Normal file
12
client/odin/shaders/text.frag.glsl
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#version 450
|
||||
|
||||
layout(location = 0) in vec2 v_uv;
|
||||
layout(location = 1) in vec4 v_color;
|
||||
layout(location = 0) out vec4 out_color;
|
||||
|
||||
layout(set = 2, binding = 0) uniform sampler2D u_font;
|
||||
|
||||
void main() {
|
||||
float alpha = texture(u_font, v_uv).r;
|
||||
out_color = vec4(v_color.rgb, v_color.a * alpha);
|
||||
}
|
||||
20
client/odin/shaders/text.vert.glsl
Normal file
20
client/odin/shaders/text.vert.glsl
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#version 450
|
||||
|
||||
layout(location = 0) in vec2 a_pos;
|
||||
layout(location = 1) in vec2 a_uv;
|
||||
layout(location = 2) in vec4 a_color;
|
||||
|
||||
layout(set = 1, binding = 0) uniform VertexUniforms {
|
||||
vec2 u_viewport;
|
||||
};
|
||||
|
||||
layout(location = 0) out vec2 v_uv;
|
||||
layout(location = 1) out vec4 v_color;
|
||||
|
||||
void main() {
|
||||
vec2 ndc = vec2((a_pos.x / u_viewport.x) * 2.0 - 1.0,
|
||||
1.0 - (a_pos.y / u_viewport.y) * 2.0);
|
||||
gl_Position = vec4(ndc, 0.0, 1.0);
|
||||
v_uv = a_uv;
|
||||
v_color = a_color;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue