diff options
Diffstat (limited to 'v_windows/v/examples/term.ui/text_editor.v')
-rw-r--r-- | v_windows/v/examples/term.ui/text_editor.v | 583 |
1 files changed, 583 insertions, 0 deletions
diff --git a/v_windows/v/examples/term.ui/text_editor.v b/v_windows/v/examples/term.ui/text_editor.v new file mode 100644 index 0000000..78e6e40 --- /dev/null +++ b/v_windows/v/examples/term.ui/text_editor.v @@ -0,0 +1,583 @@ +// Copyright (c) 2020 Lars Pontoppidan. All rights reserved. +// Use of this source code is governed by the MIT license distributed with this software. +// Don't use this editor for any serious work. +// A lot of funtionality is missing compared to your favourite editor :) +import strings +import math.mathutil as mu +import os +import term.ui as tui + +enum Movement { + up + down + left + right + home + end + page_up + page_down +} + +struct View { +pub: + raw string + cursor Cursor +} + +struct App { +mut: + tui &tui.Context = 0 + ed &Buffer = 0 + current_file int + files []string + status string + t int + magnet_x int + footer_height int = 2 + viewport int +} + +fn (mut a App) set_status(msg string, duration_ms int) { + a.status = msg + a.t = duration_ms +} + +fn (mut a App) save() { + if a.cfile().len > 0 { + b := a.ed + os.write_file(a.cfile(), b.raw()) or { panic(err) } + a.set_status('Saved', 2000) + } else { + a.set_status('No file loaded', 4000) + } +} + +fn (mut a App) cfile() string { + if a.files.len == 0 { + return '' + } + if a.current_file >= a.files.len { + return '' + } + return a.files[a.current_file] +} + +fn (mut a App) visit_prev_file() { + if a.files.len == 0 { + a.current_file = 0 + } else { + a.current_file = (a.current_file + a.files.len - 1) % a.files.len + } + a.init_file() +} + +fn (mut a App) visit_next_file() { + if a.files.len == 0 { + a.current_file = 0 + } else { + a.current_file = (a.current_file + a.files.len + 1) % a.files.len + } + a.init_file() +} + +fn (mut a App) footer() { + w, h := a.tui.window_width, a.tui.window_height + mut b := a.ed + // flat := b.flat() + // snip := if flat.len > 19 { flat[..20] } else { flat } + finfo := if a.cfile().len > 0 { ' (' + os.file_name(a.cfile()) + ')' } else { '' } + mut status := a.status + a.tui.draw_text(0, h - 1, '─'.repeat(w)) + footer := '$finfo Line ${b.cursor.pos_y + 1:4}/${b.lines.len:-4}, Column ${b.cursor.pos_x + 1:3}/${b.cur_line().len:-3} index: ${b.cursor_index():5} (ESC = quit, Ctrl+s = save)' + if footer.len < w { + a.tui.draw_text((w - footer.len) / 2, h, footer) + } else if footer.len == w { + a.tui.draw_text(0, h, footer) + } else { + a.tui.draw_text(0, h, footer[..w]) + } + if a.t <= 0 { + status = '' + } else { + a.tui.set_bg_color( + r: 200 + g: 200 + b: 200 + ) + a.tui.set_color( + r: 0 + g: 0 + b: 0 + ) + a.tui.draw_text((w + 4 - status.len) / 2, h - 1, ' $status ') + a.tui.reset() + a.t -= 33 + } +} + +struct Buffer { + tab_width int = 4 +pub mut: + lines []string + cursor Cursor +} + +fn (b Buffer) flat() string { + return b.raw().replace_each(['\n', r'\n', '\t', r'\t']) +} + +fn (b Buffer) raw() string { + return b.lines.join('\n') +} + +fn (b Buffer) view(from int, to int) View { + l := b.cur_line() + mut x := 0 + for i := 0; i < b.cursor.pos_x && i < l.len; i++ { + if l[i] == `\t` { + x += b.tab_width + continue + } + x++ + } + mut lines := []string{} + for i, line in b.lines { + if i >= from && i <= to { + lines << line + } + } + raw := lines.join('\n') + return View{ + raw: raw.replace('\t', strings.repeat(` `, b.tab_width)) + cursor: Cursor{ + pos_x: x + pos_y: b.cursor.pos_y + } + } +} + +fn (b Buffer) line(i int) string { + if i < 0 || i >= b.lines.len { + return '' + } + return b.lines[i] +} + +fn (b Buffer) cur_line() string { + return b.line(b.cursor.pos_y) +} + +fn (b Buffer) cursor_index() int { + mut i := 0 + for y, line in b.lines { + if b.cursor.pos_y == y { + i += b.cursor.pos_x + break + } + i += line.len + 1 + } + return i +} + +fn (mut b Buffer) put(s string) { + has_line_ending := s.contains('\n') + x, y := b.cursor.xy() + if b.lines.len == 0 { + b.lines.prepend('') + } + line := b.lines[y] + l, r := line[..x], line[x..] + if has_line_ending { + mut lines := s.split('\n') + lines[0] = l + lines[0] + lines[lines.len - 1] += r + b.lines.delete(y) + b.lines.insert(y, lines) + last := lines[lines.len - 1] + b.cursor.set(last.len, y + lines.len - 1) + if s == '\n' { + b.cursor.set(0, b.cursor.pos_y) + } + } else { + b.lines[y] = l + s + r + b.cursor.set(x + s.len, y) + } + $if debug { + flat := s.replace('\n', r'\n') + eprintln(@MOD + '.' + @STRUCT + '::' + @FN + ' "$flat"') + } +} + +fn (mut b Buffer) del(amount int) string { + if amount == 0 { + return '' + } + x, y := b.cursor.xy() + if amount < 0 { // don't delete left if we're at 0,0 + if x == 0 && y == 0 { + return '' + } + } else if x >= b.cur_line().len && y >= b.lines.len - 1 { + return '' + } + mut removed := '' + if amount < 0 { // backspace (backward) + i := b.cursor_index() + removed = b.raw()[i + amount..i] + mut left := amount * -1 + for li := y; li >= 0 && left > 0; li-- { + ln := b.lines[li] + if left > ln.len { + b.lines.delete(li) + if ln.len == 0 { // line break delimiter + left-- + if y == 0 { + return '' + } + line_above := b.lines[li - 1] + b.cursor.pos_x = line_above.len + } else { + left -= ln.len + } + b.cursor.pos_y-- + } else { + if x == 0 { + if y == 0 { + return '' + } + line_above := b.lines[li - 1] + if ln.len == 0 { // at line break + b.lines.delete(li) + b.cursor.pos_y-- + b.cursor.pos_x = line_above.len + } else { + b.lines[li - 1] = line_above + ln + b.lines.delete(li) + b.cursor.pos_y-- + b.cursor.pos_x = line_above.len + } + } else if x == 1 { + b.lines[li] = b.lines[li][left..] + b.cursor.pos_x = 0 + } else { + b.lines[li] = ln[..x - left] + ln[x..] + b.cursor.pos_x -= left + } + left = 0 + break + } + } + } else { // delete (forward) + i := b.cursor_index() + 1 + removed = b.raw()[i - amount..i] + mut left := amount + for li := y; li >= 0 && left > 0; li++ { + ln := b.lines[li] + if x == ln.len { // at line end + if y + 1 <= b.lines.len { + b.lines[li] = ln + b.lines[y + 1] + b.lines.delete(y + 1) + left-- + b.del(left) + } + } else if left > ln.len { + b.lines.delete(li) + left -= ln.len + } else { + b.lines[li] = ln[..x] + ln[x + left..] + left = 0 + } + } + } + $if debug { + flat := removed.replace('\n', r'\n') + eprintln(@MOD + '.' + @STRUCT + '::' + @FN + ' "$flat"') + } + return removed +} + +fn (mut b Buffer) free() { + $if debug { + eprintln(@MOD + '.' + @STRUCT + '::' + @FN) + } + for line in b.lines { + unsafe { line.free() } + } + unsafe { b.lines.free() } +} + +fn (mut b Buffer) move_updown(amount int) { + b.cursor.move(0, amount) + // Check the move + line := b.cur_line() + if b.cursor.pos_x > line.len { + b.cursor.set(line.len, b.cursor.pos_y) + } +} + +// move_cursor will navigate the cursor within the buffer bounds +fn (mut b Buffer) move_cursor(amount int, movement Movement) { + cur_line := b.cur_line() + match movement { + .up { + if b.cursor.pos_y - amount >= 0 { + b.move_updown(-amount) + } + } + .down { + if b.cursor.pos_y + amount < b.lines.len { + b.move_updown(amount) + } + } + .page_up { + dlines := mu.min(b.cursor.pos_y, amount) + b.move_updown(-dlines) + } + .page_down { + dlines := mu.min(b.lines.len - 1, b.cursor.pos_y + amount) - b.cursor.pos_y + b.move_updown(dlines) + } + .left { + if b.cursor.pos_x - amount >= 0 { + b.cursor.move(-amount, 0) + } else if b.cursor.pos_y > 0 { + b.cursor.set(b.line(b.cursor.pos_y - 1).len, b.cursor.pos_y - 1) + } + } + .right { + if b.cursor.pos_x + amount <= cur_line.len { + b.cursor.move(amount, 0) + } else if b.cursor.pos_y + 1 < b.lines.len { + b.cursor.set(0, b.cursor.pos_y + 1) + } + } + .home { + b.cursor.set(0, b.cursor.pos_y) + } + .end { + b.cursor.set(cur_line.len, b.cursor.pos_y) + } + } +} + +fn (mut b Buffer) move_to_word(movement Movement) { + a := if movement == .left { -1 } else { 1 } + mut line := b.cur_line() + mut x, mut y := b.cursor.pos_x, b.cursor.pos_y + if x + a < 0 && y > 0 { + y-- + line = b.line(b.cursor.pos_y - 1) + x = line.len + } else if x + a >= line.len && y + 1 < b.lines.len { + y++ + line = b.line(b.cursor.pos_y + 1) + x = 0 + } + // first, move past all non-`a-zA-Z0-9_` characters + for x + a >= 0 && x + a < line.len && !(line[x + a].is_letter() + || line[x + a].is_digit() || line[x + a] == `_`) { + x += a + } + // then, move past all the letters and numbers + for x + a >= 0 && x + a < line.len && (line[x + a].is_letter() + || line[x + a].is_digit() || line[x + a] == `_`) { + x += a + } + // if the cursor is out of bounds, move it to the next/previous line + if x + a >= 0 && x + a <= line.len { + x += a + } else if a < 0 && y + 1 > b.lines.len && y - 1 >= 0 { + y += a + x = 0 + } + b.cursor.set(x, y) +} + +struct Cursor { +pub mut: + pos_x int + pos_y int +} + +fn (mut c Cursor) set(x int, y int) { + c.pos_x = x + c.pos_y = y +} + +fn (mut c Cursor) move(x int, y int) { + c.pos_x += x + c.pos_y += y +} + +fn (c Cursor) xy() (int, int) { + return c.pos_x, c.pos_y +} + +// App callbacks +fn init(x voidptr) { + mut a := &App(x) + a.init_file() +} + +fn (mut a App) init_file() { + a.ed = &Buffer{} + mut init_y := 0 + mut init_x := 0 + if a.files.len > 0 && a.current_file < a.files.len && a.files[a.current_file].len > 0 { + if !os.is_file(a.files[a.current_file]) && a.files[a.current_file].contains(':') { + // support the file:line:col: format + fparts := a.files[a.current_file].split(':') + if fparts.len > 0 { + a.files[a.current_file] = fparts[0] + } + if fparts.len > 1 { + init_y = fparts[1].int() - 1 + } + if fparts.len > 2 { + init_x = fparts[2].int() - 1 + } + } + if os.is_file(a.files[a.current_file]) { + // 'vico: ' + + a.tui.set_window_title(a.files[a.current_file]) + mut b := a.ed + content := os.read_file(a.files[a.current_file]) or { panic(err) } + b.put(content) + a.ed.cursor.pos_x = init_x + a.ed.cursor.pos_y = init_y + } + } +} + +fn (a &App) view_height() int { + return a.tui.window_height - a.footer_height - 1 +} + +// magnet_cursor_x will place the cursor as close to it's last move left or right as possible +fn (mut a App) magnet_cursor_x() { + mut buffer := a.ed + if buffer.cursor.pos_x < a.magnet_x { + if a.magnet_x < buffer.cur_line().len { + move_x := a.magnet_x - buffer.cursor.pos_x + buffer.move_cursor(move_x, .right) + } + } +} + +fn frame(x voidptr) { + mut a := &App(x) + mut ed := a.ed + a.tui.clear() + scroll_limit := a.view_height() + // scroll down + if ed.cursor.pos_y > a.viewport + scroll_limit { // scroll down + a.viewport = ed.cursor.pos_y - scroll_limit + } else if ed.cursor.pos_y < a.viewport { // scroll up + a.viewport = ed.cursor.pos_y + } + view := ed.view(a.viewport, scroll_limit + a.viewport) + a.tui.draw_text(0, 0, view.raw) + a.footer() + a.tui.set_cursor_position(view.cursor.pos_x + 1, ed.cursor.pos_y + 1 - a.viewport) + a.tui.flush() +} + +fn event(e &tui.Event, x voidptr) { + mut a := &App(x) + mut buffer := a.ed + if e.typ == .key_down { + match e.code { + .escape { + exit(0) + } + .enter { + buffer.put('\n') + } + .backspace { + buffer.del(-1) + } + .delete { + buffer.del(1) + } + .left { + if e.modifiers == .ctrl { + buffer.move_to_word(.left) + } else if e.modifiers.is_empty() { + buffer.move_cursor(1, .left) + } + a.magnet_x = buffer.cursor.pos_x + } + .right { + if e.modifiers == .ctrl { + buffer.move_to_word(.right) + } else if e.modifiers.is_empty() { + buffer.move_cursor(1, .right) + } + a.magnet_x = buffer.cursor.pos_x + } + .up { + buffer.move_cursor(1, .up) + a.magnet_cursor_x() + } + .down { + buffer.move_cursor(1, .down) + a.magnet_cursor_x() + } + .page_up { + buffer.move_cursor(a.view_height(), .page_up) + } + .page_down { + buffer.move_cursor(a.view_height(), .page_down) + } + .home { + buffer.move_cursor(1, .home) + } + .end { + buffer.move_cursor(1, .end) + } + 48...57, 97...122 { // 0-9a-zA-Z + if e.modifiers == .ctrl { + if e.code == .s { + a.save() + } + } else if !(e.modifiers.has(.ctrl | .alt) || e.code == .null) { + buffer.put(e.ascii.ascii_str()) + } + } + else { + if e.modifiers == .alt { + if e.code == .comma { + a.visit_prev_file() + return + } + if e.code == .period { + a.visit_next_file() + return + } + } + buffer.put(e.utf8.bytes().bytestr()) + } + } + } else if e.typ == .mouse_scroll { + direction := if e.direction == .up { Movement.down } else { Movement.up } + buffer.move_cursor(1, direction) + } +} + +fn main() { + mut files := []string{} + if os.args.len > 1 { + files << os.args[1..] + } + mut a := &App{ + files: files + } + a.tui = tui.init( + user_data: a + init_fn: init + frame_fn: frame + event_fn: event + capture_events: true + ) + a.tui.run() ? +} |