aboutsummaryrefslogtreecommitdiff
path: root/v_windows/v/examples/term.ui/text_editor.v
diff options
context:
space:
mode:
Diffstat (limited to 'v_windows/v/examples/term.ui/text_editor.v')
-rw-r--r--v_windows/v/examples/term.ui/text_editor.v583
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() ?
+}