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 }