Compare commits

...

5 Commits

Author SHA1 Message Date
8f7fa920bd refactor to using "Editor" struct 2025-12-11 12:29:24 -05:00
cfa6e37b85 add duplicate line function 2025-12-11 09:41:09 -05:00
83a2de0386 finish implementing selection 2025-12-09 19:45:03 -05:00
b2b4ffe1bd handle selection new line 2025-12-09 19:41:50 -05:00
f58b5e88a7 fix remaining selection backspace bugs 2025-12-09 19:30:20 -05:00
2 changed files with 194 additions and 143 deletions

View File

@@ -12,7 +12,7 @@
], ],
"build_systems": [ "build_systems": [
{ {
"cmd": ["odin", "build", "src", "-debug"], "cmd": ["odin", "build", "src", "-debug", "-o:none"],
"name": "odit", "name": "odit",
"selector": "source.odin", "selector": "source.odin",
"working_dir": "${project_path}" "working_dir": "${project_path}"

View File

@@ -2,7 +2,7 @@
- DONE implement movement up and down by paragraph - DONE implement movement up and down by paragraph
- DONE implement scrolling/viewport - DONE implement scrolling/viewport
- DONE implement deletion by word - DONE implement deletion by word
- TODO implement selection - DONE implement selection
- TODO implement duplicate selection/line - TODO implement duplicate selection/line
- TODO implement move selection/line up/down - TODO implement move selection/line up/down
- TODO implement copy/cut/paste - TODO implement copy/cut/paste
@@ -16,6 +16,7 @@ package main
import "core:fmt" import "core:fmt"
import "core:mem" import "core:mem"
import "core:os" import "core:os"
import "core:slice"
import "core:strings" import "core:strings"
import utf8 "core:unicode/utf8" import utf8 "core:unicode/utf8"
import r "vendor:raylib" import r "vendor:raylib"
@@ -50,34 +51,49 @@ Range :: struct {
end: int, end: int,
} }
window_width := 800 Editor :: struct {
window_height := 600 window_width: i32,
font_size: f32 = 20.0 window_height: i32,
text_height: f32 font_size: f32,
cursor: Cursor text_height: f32,
lines: [dynamic][dynamic]u8 cursor: Cursor,
viewport_size := window_height / int(font_size) lines: [dynamic][dynamic]u8,
viewport := Viewport { viewport: Viewport,
start = 0, viewport_size: int,
end = viewport_size, selection: Selection,
} }
selection := Selection{}
DEFAULT_WINDOW_WIDTH :: 800
DEFAULT_WINDOW_HEIGHT :: 600
DEFAULT_FONT_SIZE :: 20
DEFAULT_VIEWPORT_SIZE :: DEFAULT_WINDOW_HEIGHT / DEFAULT_FONT_SIZE
main :: proc() { main :: proc() {
r.InitWindow(i32(window_width), i32(window_height), "odit") editor := Editor {
window_width = DEFAULT_WINDOW_WIDTH,
window_height = DEFAULT_WINDOW_HEIGHT,
font_size = DEFAULT_FONT_SIZE,
viewport_size = DEFAULT_WINDOW_HEIGHT / DEFAULT_FONT_SIZE,
viewport = Viewport{start = 0, end = DEFAULT_VIEWPORT_SIZE},
selection = Selection{},
text_height = {},
cursor = {},
lines = {},
}
r.InitWindow(editor.window_width, editor.window_height, "odit")
r.SetTargetFPS(60) r.SetTargetFPS(60)
default_font := r.GetFontDefault() default_font := r.GetFontDefault()
font := r.LoadFont("/Users/grant/Library/Fonts/BerkeleyMono-Regular.otf") font := r.LoadFont("/Users/grant/Library/Fonts/BerkeleyMono-Regular.otf")
r.GuiSetFont(font) r.GuiSetFont(font)
two_lines_size := r.MeasureTextEx(font, "foo\nbar", font_size, 0.0) two_lines_size := r.MeasureTextEx(font, "foo\nbar", editor.font_size, 0.0)
text_height = two_lines_size[1] / 2 editor.text_height = two_lines_size[1] / 2
content, _success := read_file("src/main.odin") content, _success := read_file("src/main.odin")
for line in strings.split_lines_iterator(&content) { for line in strings.split_lines_iterator(&content) {
bs := make([dynamic]u8, 0, len(line) == 0 ? 1 : len(line) * 2) bs := make([dynamic]u8, 0, len(line) == 0 ? 1 : len(line) * 2)
append(&bs, line) append(&bs, line)
append(&lines, bs) append(&editor.lines, bs)
} }
text: string text: string
@@ -87,39 +103,41 @@ main :: proc() {
chars, _ := utf8.encode_rune(c) chars, _ := utf8.encode_rune(c)
char := chars[0] char := chars[0]
// TODO: selection // TODO: selection
if selection.active { if editor.selection.active {
delete_selection(&editor)
} else { editor.selection.active = false
inject_at(&lines[cursor.line], cursor.char, char)
cursor.char += 1
}
} }
move_cursor() inject_at(&editor.lines[editor.cursor.line], editor.cursor.char, char)
editor.cursor.char += 1
}
move_cursor(&editor)
handle_backspace(&editor)
if repeatable_key_pressed(.ENTER) { if repeatable_key_pressed(.ENTER) {
// TODO: selection // TODO: selection
if selection.active { if editor.selection.active {
delete_selection(&editor)
} else { editor.selection.active = false
new_line_cap := len(current_line()) - cursor.char }
bs := make([dynamic]u8, 0, len(current_line()) == 0 ? 1 : new_line_cap * 2) new_line_cap := len(current_line(&editor)) - editor.cursor.char
if cursor.char < len(current_line()) { bs := make([dynamic]u8, 0, len(current_line(&editor)) == 0 ? 1 : new_line_cap * 2)
for c, i in current_line()[cursor.char:] { if editor.cursor.char < len(current_line(&editor)) {
for c, i in current_line(&editor)[editor.cursor.char:] {
append(&bs, c) append(&bs, c)
} }
} }
inject_at(&lines, cursor.line + 1, bs) inject_at(&editor.lines, editor.cursor.line + 1, bs)
resize(current_line(), cursor.char) resize(current_line(&editor), editor.cursor.char)
cursor.line += 1 editor.cursor.line += 1
cursor.char = 0 editor.cursor.char = 0
}
} }
if r.IsKeyDown(.LEFT_SUPER) && r.IsKeyPressed(.S) { if r.IsKeyDown(.LEFT_SUPER) && r.IsKeyPressed(.S) {
builder := strings.builder_make() builder := strings.builder_make()
defer strings.builder_destroy(&builder) defer strings.builder_destroy(&builder)
for line in lines { for line in editor.lines {
strings.write_bytes(&builder, line[:]) strings.write_bytes(&builder, line[:])
strings.write_byte(&builder, '\n') strings.write_byte(&builder, '\n')
} }
@@ -127,83 +145,40 @@ main :: proc() {
fmt.println("Wrote file!") fmt.println("Wrote file!")
} }
skip_backspace: if repeatable_key_pressed(.BACKSPACE) { if r.IsKeyDown(.LEFT_SUPER) && r.IsKeyDown(.LEFT_SHIFT) && r.IsKeyPressed(.D) {
if cursor.line == 0 && cursor.char == 0 do break skip_backspace if editor.selection.active {
// TODO: selection earliest := selection_earliest(&editor)
if selection.active { latest := selection_latest(&editor)
earliest := selection_earliest() #reverse for line, index in editor.lines[earliest.line:latest.line + 1] {
latest := selection_latest() new_line, _ := slice.clone_to_dynamic(line[:])
earliest_line := &lines[earliest.line] inject_at(&editor.lines, latest.line + 1, new_line)
earliest_selection := get_selection_for_line(earliest_line[:], earliest.line)
remove_range(earliest_line, earliest_selection.start, earliest_selection.end)
if latest.line != earliest.line {
latest_line := &lines[latest.line]
latest_selection := get_selection_for_line(latest_line[:], latest.line)
append(earliest_line, string(latest_line[latest_selection.end:len(latest_line)]))
defer delete(latest_line^)
ordered_remove(&lines, latest.line)
cursor.line -= 1
for i := earliest.line + 1; i < latest.line; i += 1 {
line_to_remove := lines[i]
defer delete(line_to_remove)
ordered_remove(&lines, i)
// NOTE(grant): Should we do this here?
if i < cursor.line {
cursor.line -= 1
}
} }
// NOTE: should we move the selection as well?
} else { } else {
cursor.char -= earliest_selection.end - earliest_selection.start new_line, _ := slice.clone_to_dynamic(current_line(&editor)[:])
} inject_at(&editor.lines, editor.cursor.line + 1, new_line)
selection.active = false editor.cursor.line += 1
} else {
if cursor.char == 0 {
// join lines
old_len := len(lines[cursor.line - 1])
append(&lines[cursor.line - 1], string(current_line()[:]))
line_to_remove := current_line()^
defer delete(line_to_remove)
ordered_remove(&lines, cursor.line)
cursor.line -= 1
cursor.char = old_len
} else {
if r.IsKeyDown(.LEFT_ALT) {
// delete by word
delete_to, found := find_previous_space(cursor.char, current_line()[:])
if found {
remove_range(current_line(), delete_to, cursor.char)
cursor.char = delete_to
} else {
remove_range(current_line(), 0, cursor.char)
cursor.char = 0
}
} else {
// delete single char
ordered_remove(&lines[cursor.line], cursor.char - 1)
cursor.char -= 1
}
}
} }
} }
cursor_padding := 3 cursor_padding := 3
// move viewport up // move viewport up
if cursor.line < viewport.start + cursor_padding { if editor.cursor.line < editor.viewport.start + cursor_padding {
diff := (cursor.line - viewport.start) - cursor_padding diff := (editor.cursor.line - editor.viewport.start) - cursor_padding
viewport.start = min(max(0, viewport.start + diff), len(lines) - viewport_size) editor.viewport.start = min(max(0, editor.viewport.start + diff), len(editor.lines) - editor.viewport_size)
viewport.end = min(max(viewport_size, viewport.end + diff), len(lines)) editor.viewport.end = min(max(editor.viewport_size, editor.viewport.end + diff), len(editor.lines))
} }
// move viewport down // move viewport down
if viewport.end - cursor_padding < cursor.line { if editor.viewport.end - cursor_padding < editor.cursor.line {
diff := cursor.line - (viewport.end - cursor_padding) diff := editor.cursor.line - (editor.viewport.end - cursor_padding)
viewport.start = min(max(0, viewport.start + diff), len(lines) - viewport_size) editor.viewport.start = min(max(0, editor.viewport.start + diff), len(editor.lines) - editor.viewport_size)
viewport.end = min(max(viewport_size, viewport.end + diff), len(lines)) editor.viewport.end = min(max(editor.viewport_size, editor.viewport.end + diff), len(editor.lines))
} }
r.BeginDrawing() r.BeginDrawing()
r.ClearBackground(r.BLACK) r.ClearBackground(r.BLACK)
for &line, line_index in lines[viewport.start:viewport.end] { for &line, line_index in editor.lines[editor.viewport.start:editor.viewport.end] {
if cap(line) == 0 { if cap(line) == 0 {
reserve(&line, 1) reserve(&line, 1)
} }
@@ -212,23 +187,23 @@ main :: proc() {
} }
raw_data(line)[len(line)] = 0 raw_data(line)[len(line)] = 0
if selection.active && if editor.selection.active &&
selection_earliest().line <= line_index && selection_earliest(&editor).line <= line_index &&
line_index <= selection_latest().line { line_index <= selection_latest(&editor).line {
render_line_with_selection(line[:], line_index, font) render_line_with_selection(&editor, line[:], line_index, font)
} else { } else {
// No selection active // No selection active
cstr := strings.unsafe_string_to_cstring(string(line[:])) cstr := strings.unsafe_string_to_cstring(string(line[:]))
pos := r.Vector2{0, font_size * f32(line_index)} pos := r.Vector2{0, editor.font_size * f32(line_index)}
r.DrawTextEx(font, cstr, pos, font_size, 0, r.WHITE) r.DrawTextEx(font, cstr, pos, editor.font_size, 0, r.WHITE)
} }
} }
render_cursor(&cursor) render_cursor(&editor)
fps_width := r.MeasureText("60", default_font.baseSize) fps_width := r.MeasureText("60", default_font.baseSize)
r.DrawFPS(i32(window_width) - (fps_width + 20), 10) r.DrawFPS(editor.window_width - (fps_width + 20), 10)
r.EndDrawing() r.EndDrawing()
@@ -237,15 +212,16 @@ main :: proc() {
} }
} }
render_cursor :: proc(cursor: ^Cursor) { render_cursor :: proc(editor: ^Editor) {
using editor
x := cursor.char * int(font_size / 2) x := cursor.char * int(font_size / 2)
y := i32(font_size) * i32(cursor.line - viewport.start) y := i32(font_size) * i32(cursor.line - viewport.start)
r.DrawLine(i32(x), y, i32(x), y + i32(font_size), r.WHITE) r.DrawLine(i32(x), y, i32(x), y + i32(font_size), r.WHITE)
r.DrawLine(i32(x) + 1, y, i32(x) + 1, y + i32(font_size), r.WHITE) r.DrawLine(i32(x) + 1, y, i32(x) + 1, y + i32(font_size), r.WHITE)
} }
current_line :: proc() -> ^[dynamic]u8 { current_line :: proc(editor: ^Editor) -> ^[dynamic]u8 {
return &lines[cursor.line] return &editor.lines[editor.cursor.line]
} }
is_whitespace :: proc(c: u8) -> bool { is_whitespace :: proc(c: u8) -> bool {
@@ -278,7 +254,8 @@ find_previous_space :: proc(current_position: int, line: []u8) -> (result: int,
return result, found return result, found
} }
handle_selection_start :: proc() { handle_selection_start :: proc(editor: ^Editor) {
using editor
was_active := selection.active was_active := selection.active
if r.IsKeyDown(.LEFT_SHIFT) { if r.IsKeyDown(.LEFT_SHIFT) {
selection.active = true selection.active = true
@@ -291,17 +268,19 @@ handle_selection_start :: proc() {
selection.start.char = cursor.char selection.start.char = cursor.char
} }
handle_selection_end :: proc() { handle_selection_end :: proc(editor: ^Editor) {
using editor
if !selection.active do return if !selection.active do return
selection.end.char = cursor.char selection.end.char = cursor.char
selection.end.line = cursor.line selection.end.line = cursor.line
} }
render_line_with_selection :: proc(line: []u8, line_index: int, font: r.Font) { render_line_with_selection :: proc(editor: ^Editor, line: []u8, line_index: int, font: r.Font) {
earliest := selection_earliest() using editor
latest := selection_latest() earliest := selection_earliest(editor)
latest := selection_latest(editor)
selection_range := get_selection_for_line(line, line_index) selection_range := get_selection_for_line(editor, line, line_index)
prefix := line[:selection_range.start] prefix := line[:selection_range.start]
suffix := line[selection_range.end:] if selection_range.end < len(line) else []u8{} suffix := line[selection_range.end:] if selection_range.end < len(line) else []u8{}
@@ -336,18 +315,19 @@ render_line_with_selection :: proc(line: []u8, line_index: int, font: r.Font) {
} }
} }
move_cursor :: proc() { move_cursor :: proc(editor: ^Editor) {
using editor
if repeatable_key_pressed(.RIGHT) { if repeatable_key_pressed(.RIGHT) {
handle_selection_start() handle_selection_start(editor)
defer handle_selection_end() defer handle_selection_end(editor)
preferred_position := cursor.char + 1 preferred_position := cursor.char + 1
if cursor.char == len(current_line()) && cursor.line != len(lines) - 1 { if cursor.char == len(current_line(editor)) && cursor.line != len(lines) - 1 {
preferred_position = 0 preferred_position = 0
cursor.line += 1 cursor.line += 1
} }
if r.IsKeyDown(.LEFT_ALT) && cursor.char + 1 < len(current_line()) { if r.IsKeyDown(.LEFT_ALT) && cursor.char + 1 < len(current_line(editor)) {
seen_space := false seen_space := false
for c, c_index in current_line()[cursor.char + 1:] { for c, c_index in current_line(editor)[cursor.char + 1:] {
if seen_space && !is_whitespace(c) { if seen_space && !is_whitespace(c) {
preferred_position = cursor.char + c_index preferred_position = cursor.char + c_index
break break
@@ -357,30 +337,30 @@ move_cursor :: proc() {
} }
} }
if !seen_space { if !seen_space {
preferred_position = len(current_line()) preferred_position = len(current_line(editor))
} }
} }
cursor.char = min(preferred_position, len(current_line())) cursor.char = min(preferred_position, len(current_line(editor)))
} }
if repeatable_key_pressed(.LEFT) { if repeatable_key_pressed(.LEFT) {
handle_selection_start() handle_selection_start(editor)
defer handle_selection_end() defer handle_selection_end(editor)
preferred_position := cursor.char - 1 preferred_position := cursor.char - 1
if cursor.char == 0 && cursor.line != 0 { if cursor.char == 0 && cursor.line != 0 {
cursor.line -= 1 cursor.line -= 1
preferred_position = len(current_line()) preferred_position = len(current_line(editor))
} }
if r.IsKeyDown(.LEFT_ALT) && 0 < cursor.char { if r.IsKeyDown(.LEFT_ALT) && 0 < cursor.char {
found := false found := false
preferred_position, found = find_previous_space(cursor.char, current_line()[:]) preferred_position, found = find_previous_space(cursor.char, current_line(editor)[:])
} }
cursor.char = max(preferred_position, 0) cursor.char = max(preferred_position, 0)
} }
if repeatable_key_pressed(.UP) { if repeatable_key_pressed(.UP) {
handle_selection_start() handle_selection_start(editor)
defer handle_selection_end() defer handle_selection_end(editor)
preferred_line := cursor.line - 1 preferred_line := cursor.line - 1
if r.IsKeyDown(.LEFT_ALT) && 0 < cursor.line { if r.IsKeyDown(.LEFT_ALT) && 0 < cursor.line {
@@ -393,14 +373,14 @@ move_cursor :: proc() {
} }
cursor.line = max(0, preferred_line) cursor.line = max(0, preferred_line)
if len(current_line()) < cursor.char { if len(current_line(editor)) < cursor.char {
cursor.char = len(current_line()) cursor.char = len(current_line(editor))
} }
} }
if repeatable_key_pressed(.DOWN) { if repeatable_key_pressed(.DOWN) {
handle_selection_start() handle_selection_start(editor)
defer handle_selection_end() defer handle_selection_end(editor)
preferred_line := cursor.line + 1 preferred_line := cursor.line + 1
if r.IsKeyDown(.LEFT_ALT) && cursor.line + 1 < len(lines) { if r.IsKeyDown(.LEFT_ALT) && cursor.line + 1 < len(lines) {
@@ -413,13 +393,14 @@ move_cursor :: proc() {
} }
cursor.line = min(preferred_line, len(lines) - 1) cursor.line = min(preferred_line, len(lines) - 1)
if len(current_line()) < cursor.char { if len(current_line(editor)) < cursor.char {
cursor.char = len(current_line()) cursor.char = len(current_line(editor))
} }
} }
} }
selection_earliest :: proc() -> Position { selection_earliest :: proc(editor: ^Editor) -> Position {
using editor
earliest: Position earliest: Position
latest: Position latest: Position
if selection.start.line < selection.end.line { if selection.start.line < selection.end.line {
@@ -443,7 +424,8 @@ selection_earliest :: proc() -> Position {
return earliest return earliest
} }
selection_latest :: proc() -> Position { selection_latest :: proc(editor: ^Editor) -> Position {
using editor
earliest: Position earliest: Position
latest: Position latest: Position
if selection.start.line < selection.end.line { if selection.start.line < selection.end.line {
@@ -467,9 +449,10 @@ selection_latest :: proc() -> Position {
return latest return latest
} }
get_selection_for_line :: proc(line: []u8, line_index: int) -> Range { get_selection_for_line :: proc(editor: ^Editor, line: []u8, line_index: int) -> Range {
earliest := selection_earliest() using editor
latest := selection_latest() earliest := selection_earliest(editor)
latest := selection_latest(editor)
selection_begin: int selection_begin: int
selection_end: int selection_end: int
if line_index == earliest.line && line_index == latest.line { if line_index == earliest.line && line_index == latest.line {
@@ -487,3 +470,71 @@ get_selection_for_line :: proc(line: []u8, line_index: int) -> Range {
} }
return Range{start = selection_begin, end = selection_end} return Range{start = selection_begin, end = selection_end}
} }
handle_backspace :: proc(editor: ^Editor) {
using editor
skip_backspace: if repeatable_key_pressed(.BACKSPACE) {
if cursor.line == 0 && cursor.char == 0 do break skip_backspace
// TODO: selection
if selection.active {
delete_selection(editor)
selection.active = false
} else if cursor.char == 0 {
// join lines
old_len := len(lines[cursor.line - 1])
append(&lines[cursor.line - 1], string(current_line(editor)[:]))
line_to_remove := current_line(editor)^
defer delete(line_to_remove)
ordered_remove(&lines, cursor.line)
cursor.line -= 1
cursor.char = old_len
} else if r.IsKeyDown(.LEFT_ALT) {
// delete by word
delete_to, found := find_previous_space(cursor.char, current_line(editor)[:])
if found {
remove_range(current_line(editor), delete_to, cursor.char)
cursor.char = delete_to
} else {
remove_range(current_line(editor), 0, cursor.char)
cursor.char = 0
}
} else {
// delete single char
ordered_remove(&lines[cursor.line], cursor.char - 1)
cursor.char -= 1
}
}
}
delete_selection :: proc(editor: ^Editor) {
using editor
earliest := selection_earliest(editor)
latest := selection_latest(editor)
earliest_line := &lines[earliest.line]
earliest_selection := get_selection_for_line(editor, earliest_line[:], earliest.line)
remove_range(earliest_line, earliest_selection.start, earliest_selection.end)
if latest.line != earliest.line {
latest_line := lines[latest.line]
latest_selection := get_selection_for_line(editor, latest_line[:], latest.line)
append(earliest_line, string(latest_line[latest_selection.end:len(latest_line)]))
ordered_remove(&lines, latest.line)
delete(latest_line)
if selection.start.line < selection.end.line {
cursor.line -= 1
if selection.start.char < selection.end.char {
cursor.char -= selection.end.char - selection.start.char
}
}
for i := earliest.line + 1; i < latest.line; i += 1 {
line_to_remove := lines[i]
defer delete(line_to_remove)
ordered_remove(&lines, i)
// NOTE(grant): Should we do this here?
if selection.start.line < selection.end.line {
cursor.line -= 1
}
}
} else {
cursor.char -= earliest_selection.end - earliest_selection.start
}
}