498 lines
15 KiB
Odin
498 lines
15 KiB
Odin
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,
|
|
scratch: bool, // in-memory tab (e.g. a git diff); never saved or synced
|
|
diff: ^Diff_Document, // when set, the tab renders a split diff view
|
|
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)
|
|
}
|
|
|
|
// Opens (or replaces) an in-memory tab that is not backed by a file.
|
|
editor_open_scratch :: proc(editor: ^Editor, name, contents: string) {
|
|
data := make([]u8, len(contents))
|
|
copy(data, transmute([]u8)contents)
|
|
|
|
for &buffer, index in editor.buffers {
|
|
if buffer.scratch && buffer.path == name {
|
|
editor_buffer_destroy(&buffer)
|
|
buffer = Editor_Buffer{
|
|
path = strings.clone(name),
|
|
daemon_path = strings.clone(""),
|
|
original_storage = data,
|
|
buffer = buffer_make(string(data)),
|
|
scratch = true,
|
|
}
|
|
editor.active = index
|
|
return
|
|
}
|
|
}
|
|
|
|
append(&editor.buffers, Editor_Buffer{
|
|
path = strings.clone(name),
|
|
daemon_path = strings.clone(""),
|
|
original_storage = data,
|
|
buffer = buffer_make(string(data)),
|
|
scratch = true,
|
|
})
|
|
editor.active = len(editor.buffers) - 1
|
|
}
|
|
|
|
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
|
|
if active.buffer.version == active.saved_version {
|
|
active.dirty = false
|
|
return
|
|
}
|
|
|
|
text := buffer_bytes(&active.buffer)
|
|
defer delete(text)
|
|
active.dirty = !bytes_equal(text[:], active.original_storage[:])
|
|
}
|
|
|
|
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 || active.scratch 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
|
|
}
|
|
|
|
delete(active.original_storage)
|
|
saved_text := clone_bytes(text[:])
|
|
active.original_storage = saved_text[:]
|
|
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 || buffer.scratch 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 {
|
|
delete(buffer.original_storage)
|
|
saved_text := clone_bytes(text[:])
|
|
buffer.original_storage = saved_text[:]
|
|
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) {
|
|
if editor_buffer.diff != nil {
|
|
diff_document_destroy(editor_buffer.diff)
|
|
editor_buffer.diff = nil
|
|
}
|
|
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
|
|
}
|