├── suspend.go ├── redo_mode.go ├── suspend_posix.go ├── macro_repeat_mode.go ├── overlay_mode.go ├── key_press_mode.go ├── LICENSE ├── autocomplete_mode.go ├── region_indent_mode.go ├── fill_region_mode.go ├── llrb_tree_test.go ├── line_edit_mode.go ├── llrb_tree.go ├── view_op_mode.go ├── extended_mode.go ├── isearch_mode.go ├── view_tree.go ├── README ├── action.go ├── buffer.go ├── cursor_location.go ├── utils.go ├── autocomplete.go ├── godit.go └── view.go /suspend.go: -------------------------------------------------------------------------------- 1 | // +build android plan9 nacl windows 2 | 3 | package main 4 | 5 | // do nothing, it's a posix specific feature at the moment 6 | func suspend(g *godit) {} 7 | -------------------------------------------------------------------------------- /redo_mode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/nsf/termbox-go" 5 | ) 6 | 7 | type redo_mode struct { 8 | stub_overlay_mode 9 | godit *godit 10 | } 11 | 12 | func init_redo_mode(godit *godit) redo_mode { 13 | r := redo_mode{godit: godit} 14 | return r 15 | } 16 | 17 | func (r redo_mode) on_key(ev *termbox.Event) { 18 | g := r.godit 19 | v := g.active.leaf 20 | if ev.Mod == 0 && ev.Key == termbox.KeyCtrlSlash { 21 | v.on_vcommand(vcommand_redo, 0) 22 | return 23 | } 24 | 25 | g.set_overlay_mode(nil) 26 | g.on_key(ev) 27 | } 28 | -------------------------------------------------------------------------------- /suspend_posix.go: -------------------------------------------------------------------------------- 1 | // +build linux darwin dragonfly solaris openbsd netbsd freebsd 2 | 3 | package main 4 | 5 | import ( 6 | "github.com/nsf/termbox-go" 7 | "syscall" 8 | ) 9 | 10 | func suspend(g *godit) { 11 | // finalize termbox 12 | termbox.Close() 13 | 14 | // suspend the process 15 | pid := syscall.Getpid() 16 | err := syscall.Kill(pid, syscall.SIGSTOP) 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | // reset the state so we can get back to work again 22 | err = termbox.Init() 23 | if err != nil { 24 | panic(err) 25 | } 26 | termbox.SetInputMode(termbox.InputAlt) 27 | g.resize() 28 | } 29 | -------------------------------------------------------------------------------- /macro_repeat_mode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/nsf/termbox-go" 5 | ) 6 | 7 | type macro_repeat_mode struct { 8 | stub_overlay_mode 9 | godit *godit 10 | } 11 | 12 | func init_macro_repeat_mode(godit *godit) macro_repeat_mode { 13 | m := macro_repeat_mode{godit: godit} 14 | godit.set_overlay_mode(nil) 15 | m.godit.replay_macro() 16 | m.godit.set_status("(Type e to repeat macro)") 17 | return m 18 | } 19 | 20 | func (m macro_repeat_mode) on_key(ev *termbox.Event) { 21 | g := m.godit 22 | if ev.Mod == 0 && ev.Ch == 'e' { 23 | g.set_overlay_mode(nil) 24 | g.replay_macro() 25 | g.set_overlay_mode(m) 26 | g.set_status("(Type e to repeat macro)") 27 | return 28 | } 29 | 30 | g.set_overlay_mode(nil) 31 | g.on_key(ev) 32 | } 33 | -------------------------------------------------------------------------------- /overlay_mode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/nsf/termbox-go" 5 | ) 6 | 7 | //---------------------------------------------------------------------------- 8 | // overlay mode 9 | //---------------------------------------------------------------------------- 10 | 11 | type overlay_mode interface { 12 | needs_cursor() bool 13 | cursor_position() (int, int) 14 | exit() 15 | draw() 16 | on_resize(ev *termbox.Event) 17 | on_key(ev *termbox.Event) 18 | } 19 | 20 | type stub_overlay_mode struct{} 21 | 22 | func (stub_overlay_mode) needs_cursor() bool { return false } 23 | func (stub_overlay_mode) cursor_position() (int, int) { return -1, -1 } 24 | func (stub_overlay_mode) exit() {} 25 | func (stub_overlay_mode) draw() {} 26 | func (stub_overlay_mode) on_resize(ev *termbox.Event) {} 27 | func (stub_overlay_mode) on_key(ev *termbox.Event) {} 28 | -------------------------------------------------------------------------------- /key_press_mode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/nsf/termbox-go" 5 | ) 6 | 7 | type key_press_mode struct { 8 | stub_overlay_mode 9 | godit *godit 10 | actions map[rune]func() 11 | def rune 12 | prompt string 13 | } 14 | 15 | func init_key_press_mode(godit *godit, actions map[rune]func(), def rune, prompt string) *key_press_mode { 16 | k := new(key_press_mode) 17 | k.godit = godit 18 | k.actions = actions 19 | k.def = def 20 | k.prompt = prompt 21 | k.godit.set_status(prompt) 22 | return k 23 | } 24 | 25 | func (k *key_press_mode) on_key(ev *termbox.Event) { 26 | if ev.Mod != 0 { 27 | return 28 | } 29 | 30 | ch := ev.Ch 31 | if ev.Key == termbox.KeyEnter || ev.Key == termbox.KeyCtrlJ { 32 | ch = k.def 33 | } 34 | 35 | action, ok := k.actions[ch] 36 | if ok { 37 | action() 38 | k.godit.set_overlay_mode(nil) 39 | } else { 40 | k.godit.set_status(k.prompt) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 nsf 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /autocomplete_mode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/nsf/termbox-go" 5 | ) 6 | 7 | type autocomplete_mode struct { 8 | stub_overlay_mode 9 | godit *godit 10 | origin cursor_location 11 | proposals []ac_proposal 12 | prefix_len int 13 | current int 14 | } 15 | 16 | func init_autocomplete_mode(godit *godit) *autocomplete_mode { 17 | view := godit.active.leaf 18 | 19 | a := new(autocomplete_mode) 20 | a.godit = godit 21 | a.origin = view.cursor 22 | a.proposals, a.prefix_len = local_ac(view) 23 | a.current = -1 24 | a.substitute_next() 25 | return a 26 | } 27 | 28 | func (a *autocomplete_mode) substitute_next() { 29 | view := a.godit.active.leaf 30 | if a.current != -1 { 31 | // undo previous substitution 32 | view.undo() 33 | a.godit.set_status("") // hide undo status message 34 | } 35 | 36 | a.current++ 37 | if a.current >= len(a.proposals) { 38 | a.current = -1 39 | a.godit.set_status("No further expansions found") 40 | return 41 | } 42 | 43 | // create a new one 44 | c := view.cursor 45 | view.finalize_action_group() 46 | if a.prefix_len != 0 { 47 | c.move_one_word_backward() 48 | wlen := a.origin.boffset - c.boffset 49 | view.action_delete(c, wlen) 50 | } 51 | newword := clone_byte_slice(a.proposals[a.current].content) 52 | view.action_insert(c, newword) 53 | view.last_vcommand = vcommand_none 54 | view.dirty = dirty_everything 55 | c.boffset += len(newword) 56 | view.move_cursor_to(c) 57 | view.finalize_action_group() 58 | } 59 | 60 | func (a *autocomplete_mode) on_key(ev *termbox.Event) { 61 | g := a.godit 62 | if ev.Mod&termbox.ModAlt != 0 && ev.Ch == '/' { 63 | a.substitute_next() 64 | return 65 | } 66 | 67 | g.set_overlay_mode(nil) 68 | g.on_key(ev) 69 | } 70 | -------------------------------------------------------------------------------- /region_indent_mode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/nsf/termbox-go" 5 | ) 6 | 7 | type region_indent_mode struct { 8 | stub_overlay_mode 9 | godit *godit 10 | } 11 | 12 | func init_region_indent_mode(godit *godit, dir int) region_indent_mode { 13 | v := godit.active.leaf 14 | r := region_indent_mode{godit: godit} 15 | 16 | beg, end := v.line_region() 17 | if dir > 0 { 18 | v.on_vcommand(vcommand_indent_region, 0) 19 | end.boffset++ 20 | } else if dir < 0 { 21 | v.on_vcommand(vcommand_deindent_region, 0) 22 | } 23 | v.set_tags(view_tag{ 24 | beg_line: beg.line_num, 25 | beg_offset: beg.boffset, 26 | end_line: end.line_num, 27 | end_offset: end.boffset, 28 | fg: termbox.ColorDefault, 29 | bg: termbox.ColorBlue, 30 | }) 31 | v.dirty = dirty_everything 32 | godit.set_status("(Type > or < to indent/deindent respectively)") 33 | return r 34 | } 35 | 36 | func (r region_indent_mode) exit() { 37 | v := r.godit.active.leaf 38 | v.set_tags() 39 | v.dirty = dirty_everything 40 | } 41 | 42 | func (r region_indent_mode) on_key(ev *termbox.Event) { 43 | g := r.godit 44 | v := g.active.leaf 45 | beg, end := v.line_region() 46 | if ev.Mod == 0 { 47 | switch ev.Ch { 48 | case '>': 49 | v.on_vcommand(vcommand_indent_region, 0) 50 | g.set_status("(Type > or < to indent/deindent respectively)") 51 | end.boffset++ 52 | goto update_tag 53 | case '<': 54 | v.on_vcommand(vcommand_deindent_region, 0) 55 | g.set_status("(Type > or < to indent/deindent respectively)") 56 | goto update_tag 57 | } 58 | } 59 | 60 | g.set_overlay_mode(nil) 61 | g.on_key(ev) 62 | return 63 | 64 | update_tag: 65 | v.set_tags(view_tag{ 66 | beg_line: beg.line_num, 67 | beg_offset: beg.boffset, 68 | end_line: end.line_num, 69 | end_offset: end.boffset, 70 | fg: termbox.ColorDefault, 71 | bg: termbox.ColorBlue, 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /fill_region_mode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "strconv" 6 | ) 7 | 8 | type fill_region_context struct { 9 | g *godit 10 | prefix []byte 11 | maxv int 12 | } 13 | 14 | // just a couple of prefixes for popular languages, sorted from long to short 15 | var fill_region_prefixes = [][]byte{ 16 | []byte(";;;;"), // Lisp 17 | []byte(";;;"), // Lisp 18 | []byte("REM"), // cmd.exe, COMMAND.COM, Basic 19 | []byte("///"), // doxygen 20 | []byte("//"), // C, C++, C#, D, Go, Java, JavaScript, Delphi, PHP, etc. 21 | []byte(";;"), // Lisp 22 | []byte("--"), // Haskell, Lua, Ada, SQL, etc. 23 | []byte("::"), // md.exe, COMMAND.COM, Basic 24 | []byte("#"), // Perl, Python, Ruby, Bash, PHP, etc. 25 | []byte(";"), // Lisp 26 | []byte(":"), // cmd.exe, COMMAND.COM, Basic 27 | // TODO: more? 28 | } 29 | 30 | func (f *fill_region_context) maxv_lemp() line_edit_mode_params { 31 | v := f.g.active.leaf 32 | return line_edit_mode_params{ 33 | prompt: "Fill width:", 34 | initial_content: "80", 35 | on_apply: func(buf *buffer) { 36 | if i, err := strconv.Atoi(string(buf.contents())); err == nil { 37 | f.maxv = i 38 | } 39 | v.finalize_action_group() 40 | v.last_vcommand = vcommand_none 41 | v.fill_region(f.maxv, f.prefix) 42 | v.finalize_action_group() 43 | }, 44 | } 45 | } 46 | 47 | func (f *fill_region_context) prefix_lemp() line_edit_mode_params { 48 | return line_edit_mode_params{ 49 | prompt: "Prefix:", 50 | initial_content: string(f.prefix), 51 | on_apply: func(buf *buffer) { 52 | f.prefix = buf.contents() 53 | f.g.set_overlay_mode(init_line_edit_mode(f.g, f.maxv_lemp())) 54 | }, 55 | } 56 | } 57 | 58 | func init_fill_region_mode(godit *godit) *line_edit_mode { 59 | v := godit.active.leaf 60 | f := fill_region_context{g: godit, maxv: 80} 61 | beg, _ := v.line_region() 62 | data := beg.line.data 63 | data = data[index_first_non_space(data):] 64 | for _, prefix := range fill_region_prefixes { 65 | if bytes.HasPrefix(data, prefix) { 66 | f.prefix = prefix 67 | break 68 | } 69 | } 70 | return init_line_edit_mode(godit, f.prefix_lemp()) 71 | } 72 | -------------------------------------------------------------------------------- /llrb_tree_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | import "strconv" 5 | import "math/rand" 6 | import "time" 7 | 8 | // Test never fails, just use it with `go test -v`, it prints some values, looks 9 | // like everything is fine. 10 | func TestLLRBTree(t *testing.T) { 11 | var tree llrb_tree 12 | rand.Seed(time.Now().UnixNano()) 13 | p := rand.Perm(1024) 14 | // insert 1024 different numbers 15 | for _, v := range p { 16 | var x []byte 17 | x = strconv.AppendInt(x, int64(v), 10) 18 | tree.insert_maybe(x) 19 | } 20 | tree.clear() 21 | // try inserting twice 22 | for _, v := range p { 23 | var x []byte 24 | x = strconv.AppendInt(x, int64(v), 10) 25 | tree.insert_maybe(x) 26 | } 27 | for _, v := range p { 28 | var x []byte 29 | x = strconv.AppendInt(x, int64(v), 10) 30 | tree.insert_maybe(x) 31 | } 32 | 33 | t.Logf("Length: %d\n", tree.count) 34 | // should be near 1/2 35 | t.Logf("Root: %s\n", string(tree.root.value)) 36 | // should be near 1/4 37 | t.Logf("Left: %s\n", string(tree.root.left.value)) 38 | // should be near 3/4 39 | t.Logf("Right: %s\n", string(tree.root.right.value)) 40 | contains := func(n int) { 41 | var x []byte 42 | x = strconv.AppendInt(x, int64(n), 10) 43 | t.Logf("Contains: %d, %v\n", n, tree.contains(x)) 44 | } 45 | contains(10) 46 | contains(0) 47 | contains(999) 48 | contains(54400) 49 | 50 | max_h := 0 51 | var traverse func(n *llrb_node, h int) 52 | traverse = func(n *llrb_node, h int) { 53 | if h > max_h { 54 | max_h = h 55 | } 56 | if n.left != nil { 57 | traverse(n.left, h+1) 58 | } 59 | if n.right != nil { 60 | traverse(n.right, h+1) 61 | } 62 | } 63 | traverse(tree.root, 0) 64 | 65 | // from what I've tested, max height seems to be 12 or 13, which is nice 66 | t.Logf("Max height: %d\n", max_h) 67 | 68 | // check if it's sorted correctly 69 | /* 70 | var printnodes func(n *llrb_node) 71 | printnodes = func(n *llrb_node) { 72 | if n == nil { 73 | return 74 | } 75 | printnodes(n.left) 76 | t.Logf("Node: %s\n", string(n.value)) 77 | printnodes(n.right) 78 | } 79 | printnodes(tree.root) 80 | */ 81 | // seems correct, the order is lexicographic 82 | } 83 | -------------------------------------------------------------------------------- /line_edit_mode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/nsf/termbox-go" 5 | "github.com/nsf/tulib" 6 | "strings" 7 | "unicode/utf8" 8 | ) 9 | 10 | //---------------------------------------------------------------------------- 11 | // line edit mode 12 | //---------------------------------------------------------------------------- 13 | 14 | type line_edit_mode struct { 15 | stub_overlay_mode 16 | line_edit_mode_params 17 | godit *godit 18 | linebuf *buffer 19 | lineview *view 20 | prompt []byte 21 | prompt_w int 22 | } 23 | 24 | type line_edit_mode_params struct { 25 | on_apply func(buffer *buffer) 26 | on_cancel func() 27 | ac_decide ac_decide_func 28 | prompt string 29 | initial_content string 30 | init_autocompl bool 31 | } 32 | 33 | func (l *line_edit_mode) exit() { 34 | if l.on_cancel != nil { 35 | l.on_cancel() 36 | } 37 | } 38 | 39 | func (l *line_edit_mode) on_key(ev *termbox.Event) { 40 | switch ev.Key { 41 | case termbox.KeyEnter, termbox.KeyCtrlJ: 42 | if l.lineview.ac != nil { 43 | l.lineview.on_key(ev) 44 | if !l.init_autocompl { 45 | break 46 | } 47 | } 48 | 49 | // reset overlay mode earlier so that 'on_apply' can 50 | // override it 51 | l.godit.set_overlay_mode(nil) 52 | if l.on_apply != nil { 53 | l.on_apply(l.linebuf) 54 | } 55 | case termbox.KeyTab: 56 | l.lineview.on_vcommand(vcommand_autocompl_init, 0) 57 | default: 58 | l.lineview.on_key(ev) 59 | } 60 | } 61 | 62 | func (l *line_edit_mode) resize(ev *termbox.Event) { 63 | w, h := ev.Width-l.prompt_w-1, 1 64 | if w < 1 || ev.Height < 1 { 65 | return 66 | } 67 | l.lineview.resize(w, h) 68 | } 69 | 70 | func (l *line_edit_mode) draw() { 71 | ui := l.godit.uibuf 72 | view := l.lineview 73 | 74 | // update label 75 | prompt_r := tulib.Rect{ 76 | 0, ui.Height - 1, 77 | l.prompt_w + 1, 1, 78 | } 79 | ui.Fill(prompt_r, termbox.Cell{ 80 | Fg: termbox.ColorDefault, 81 | Bg: termbox.ColorDefault, 82 | Ch: ' ', 83 | }) 84 | lp := default_label_params 85 | lp.Fg = termbox.ColorCyan 86 | ui.DrawLabel(prompt_r, &lp, l.prompt) 87 | 88 | // update line view 89 | view.resize(ui.Width-l.prompt_w-1, 1) 90 | view.draw() 91 | line_r := tulib.Rect{ 92 | l.prompt_w + 1, ui.Height - 1, 93 | view.uibuf.Width, view.uibuf.Height, 94 | } 95 | ui.Blit(line_r, 0, 0, &view.uibuf) 96 | if view.ac == nil { 97 | return 98 | } 99 | 100 | // draw autocompletion 101 | proposals := view.ac.actual_proposals() 102 | if len(proposals) > 0 { 103 | cx, cy := view.cursor_position_for(view.ac.origin) 104 | view.ac.draw_onto(&ui, line_r.X+cx, line_r.Y+cy) 105 | } 106 | } 107 | 108 | func (l *line_edit_mode) needs_cursor() bool { 109 | return true 110 | } 111 | 112 | func (l *line_edit_mode) cursor_position() (int, int) { 113 | y := l.godit.uibuf.Height - 1 114 | x := l.prompt_w + 1 115 | lx, ly := l.lineview.cursor_position() 116 | return x + lx, y + ly 117 | } 118 | 119 | func init_line_edit_mode(godit *godit, p line_edit_mode_params) *line_edit_mode { 120 | l := new(line_edit_mode) 121 | l.godit = godit 122 | l.line_edit_mode_params = p 123 | l.linebuf, _ = new_buffer(strings.NewReader(p.initial_content)) 124 | l.lineview = new_view(godit.view_context(), l.linebuf) 125 | l.lineview.oneline = true // enable one line mode 126 | l.lineview.ac_decide = p.ac_decide // override ac_decide function 127 | l.prompt = []byte(p.prompt) 128 | l.prompt_w = utf8.RuneCount(l.prompt) 129 | l.lineview.resize(l.godit.uibuf.Width-l.prompt_w-1, 1) 130 | l.lineview.on_vcommand(vcommand_move_cursor_end_of_line, 0) 131 | if l.init_autocompl { 132 | l.lineview.on_vcommand(vcommand_autocompl_init, 0) 133 | } 134 | return l 135 | } 136 | -------------------------------------------------------------------------------- /llrb_tree.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | // LLRB tree with single key values as byte slices. 8 | // I use 2-3 tree algorithms for it. Only insertion is implemented (no delete). 9 | type llrb_tree struct { 10 | root *llrb_node 11 | count int 12 | free_nodes *llrb_node 13 | } 14 | 15 | func (t *llrb_tree) free_node(n *llrb_node) { 16 | *n = llrb_node{left: t.free_nodes} 17 | t.free_nodes = n 18 | } 19 | 20 | func (t *llrb_tree) alloc_node(value []byte) *llrb_node { 21 | if t.free_nodes == nil { 22 | return &llrb_node{value: value} 23 | } 24 | 25 | n := t.free_nodes 26 | t.free_nodes = n.left 27 | *n = llrb_node{value: value} 28 | return n 29 | } 30 | 31 | func (t *llrb_tree) clear() { 32 | t.clear_recursive(t.root) 33 | t.root = nil 34 | t.count = 0 35 | } 36 | 37 | func (t *llrb_tree) clear_recursive(n *llrb_node) { 38 | if n == nil { 39 | return 40 | } 41 | t.clear_recursive(n.left) 42 | t.clear_recursive(n.right) 43 | t.free_node(n) 44 | } 45 | 46 | func (t *llrb_tree) walk(cb func(value []byte)) { 47 | t.root.walk(cb) 48 | } 49 | 50 | func (t *llrb_tree) insert_maybe(value []byte) bool { 51 | var ok bool 52 | t.root, ok = t.root.insert_maybe(value) 53 | if ok { 54 | t.count++ 55 | } 56 | return ok 57 | } 58 | 59 | func (t *llrb_tree) insert_maybe_recursive(n *llrb_node, value []byte) (*llrb_node, bool) { 60 | if n == nil { 61 | return t.alloc_node(value), true 62 | } 63 | 64 | var inserted bool 65 | switch cmp := bytes.Compare(value, n.value); { 66 | case cmp < 0: 67 | n.left, inserted = t.insert_maybe_recursive(n.left, value) 68 | case cmp > 0: 69 | n.right, inserted = t.insert_maybe_recursive(n.right, value) 70 | default: 71 | // don't insert anything 72 | } 73 | 74 | if n.right.is_red() && !n.left.is_red() { 75 | n = n.rotate_left() 76 | } 77 | if n.left.is_red() && n.left.left.is_red() { 78 | n = n.rotate_right() 79 | } 80 | if n.left.is_red() && n.right.is_red() { 81 | n.flip_colors() 82 | } 83 | 84 | return n, inserted 85 | } 86 | 87 | func (t *llrb_tree) contains(value []byte) bool { 88 | return t.root.contains(value) 89 | } 90 | 91 | const ( 92 | llrb_red = false 93 | llrb_black = true 94 | ) 95 | 96 | type llrb_node struct { 97 | value []byte 98 | left *llrb_node 99 | right *llrb_node 100 | color bool 101 | } 102 | 103 | func (n *llrb_node) walk(cb func(value []byte)) { 104 | if n == nil { 105 | return 106 | } 107 | n.left.walk(cb) 108 | cb(n.value) 109 | n.right.walk(cb) 110 | } 111 | 112 | func (n *llrb_node) rotate_left() *llrb_node { 113 | x := n.right 114 | n.right = x.left 115 | x.left = n 116 | x.color = n.color 117 | n.color = llrb_red 118 | return x 119 | } 120 | 121 | func (n *llrb_node) rotate_right() *llrb_node { 122 | x := n.left 123 | n.left = x.right 124 | x.right = n 125 | x.color = n.color 126 | n.color = llrb_red 127 | return x 128 | } 129 | 130 | func (n *llrb_node) flip_colors() { 131 | n.color = !n.color 132 | n.left.color = !n.left.color 133 | n.right.color = !n.right.color 134 | } 135 | 136 | func (n *llrb_node) is_red() bool { 137 | return n != nil && !n.color 138 | } 139 | 140 | func (n *llrb_node) insert_maybe(value []byte) (*llrb_node, bool) { 141 | if n == nil { 142 | return &llrb_node{value: value}, true 143 | } 144 | 145 | var inserted bool 146 | switch cmp := bytes.Compare(value, n.value); { 147 | case cmp < 0: 148 | n.left, inserted = n.left.insert_maybe(value) 149 | case cmp > 0: 150 | n.right, inserted = n.right.insert_maybe(value) 151 | default: 152 | // don't insert anything 153 | } 154 | 155 | if n.right.is_red() && !n.left.is_red() { 156 | n = n.rotate_left() 157 | } 158 | if n.left.is_red() && n.left.left.is_red() { 159 | n = n.rotate_right() 160 | } 161 | if n.left.is_red() && n.right.is_red() { 162 | n.flip_colors() 163 | } 164 | 165 | return n, inserted 166 | } 167 | 168 | func (n *llrb_node) contains(value []byte) bool { 169 | for n != nil { 170 | switch cmp := bytes.Compare(value, n.value); { 171 | case cmp < 0: 172 | n = n.left 173 | case cmp > 0: 174 | n = n.right 175 | default: 176 | return true 177 | } 178 | } 179 | return false 180 | } 181 | -------------------------------------------------------------------------------- /view_op_mode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/nsf/termbox-go" 5 | ) 6 | 7 | //---------------------------------------------------------------------------- 8 | // view op mode 9 | //---------------------------------------------------------------------------- 10 | 11 | type view_op_mode struct { 12 | stub_overlay_mode 13 | godit *godit 14 | } 15 | 16 | const view_names = `1234567890abcdefgijlmnpqrstuwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ` 17 | 18 | var view_op_mode_name = []byte("view operations mode") 19 | 20 | func init_view_op_mode(godit *godit) view_op_mode { 21 | termbox.HideCursor() 22 | v := view_op_mode{godit: godit} 23 | return v 24 | } 25 | 26 | func (v view_op_mode) draw() { 27 | g := v.godit 28 | r := g.uibuf.Rect 29 | r.Y = r.Height - 1 30 | r.Height = 1 31 | g.uibuf.Fill(r, termbox.Cell{ 32 | Fg: termbox.ColorDefault, 33 | Bg: termbox.ColorDefault, 34 | Ch: ' ', 35 | }) 36 | lp := default_label_params 37 | lp.Fg = termbox.ColorYellow 38 | g.uibuf.DrawLabel(r, &lp, view_op_mode_name) 39 | 40 | // draw views names 41 | name := 0 42 | g.views.traverse(func(leaf *view_tree) { 43 | if name >= len(view_names) { 44 | return 45 | } 46 | bg := termbox.ColorBlue 47 | if leaf == g.active { 48 | bg = termbox.ColorRed 49 | } 50 | r := leaf.Rect 51 | r.Width = 3 52 | r.Height = 1 53 | x := r.X + 1 54 | y := r.Y 55 | g.uibuf.Fill(r, termbox.Cell{ 56 | Fg: termbox.ColorDefault, 57 | Bg: bg, 58 | Ch: ' ', 59 | }) 60 | g.uibuf.Set(x, y, termbox.Cell{ 61 | Fg: termbox.ColorWhite | termbox.AttrBold, 62 | Bg: bg, 63 | Ch: rune(view_names[name]), 64 | }) 65 | name++ 66 | }) 67 | 68 | // draw splitters 69 | r = g.active.Rect 70 | var x, y int 71 | 72 | // horizontal ---------------------- 73 | hr := r 74 | hr.X += (r.Width - 1) / 2 75 | hr.Width = 1 76 | hr.Height = 3 77 | g.uibuf.Fill(hr, termbox.Cell{ 78 | Fg: termbox.ColorWhite, 79 | Bg: termbox.ColorRed, 80 | Ch: '|', 81 | }) 82 | 83 | x = hr.X 84 | y = hr.Y + 1 85 | g.uibuf.Set(x, y, termbox.Cell{ 86 | Fg: termbox.ColorWhite | termbox.AttrBold, 87 | Bg: termbox.ColorRed, 88 | Ch: 'h', 89 | }) 90 | 91 | // vertical ---------------------- 92 | vr := r 93 | vr.Y += (r.Height - 1) / 2 94 | vr.Height = 1 95 | vr.Width = 5 96 | g.uibuf.Fill(vr, termbox.Cell{ 97 | Fg: termbox.ColorWhite, 98 | Bg: termbox.ColorRed, 99 | Ch: '-', 100 | }) 101 | 102 | x = vr.X + 2 103 | y = vr.Y 104 | g.uibuf.Set(x, y, termbox.Cell{ 105 | Fg: termbox.ColorWhite | termbox.AttrBold, 106 | Bg: termbox.ColorRed, 107 | Ch: 'v', 108 | }) 109 | } 110 | 111 | func (v view_op_mode) select_name(ch rune) *view_tree { 112 | g := v.godit 113 | sel := (*view_tree)(nil) 114 | name := 0 115 | g.views.traverse(func(leaf *view_tree) { 116 | if name >= len(view_names) { 117 | return 118 | } 119 | if rune(view_names[name]) == ch { 120 | sel = leaf 121 | } 122 | name++ 123 | }) 124 | 125 | return sel 126 | } 127 | 128 | func (v view_op_mode) needs_cursor() bool { 129 | return true 130 | } 131 | 132 | func (v view_op_mode) on_key(ev *termbox.Event) { 133 | g := v.godit 134 | if ev.Ch != 0 { 135 | leaf := v.select_name(ev.Ch) 136 | if leaf != nil { 137 | g.active.leaf.deactivate() 138 | g.active = leaf 139 | g.active.leaf.activate() 140 | return 141 | } 142 | 143 | switch ev.Ch { 144 | case 'h': 145 | g.split_horizontally() 146 | return 147 | case 'v': 148 | g.split_vertically() 149 | return 150 | case 'k': 151 | g.kill_active_view() 152 | return 153 | } 154 | } 155 | 156 | switch ev.Key { 157 | case termbox.KeyCtrlN, termbox.KeyArrowDown: 158 | node := g.active.nearest_vsplit() 159 | if node != nil { 160 | node.step_resize(1) 161 | } 162 | return 163 | case termbox.KeyCtrlP, termbox.KeyArrowUp: 164 | node := g.active.nearest_vsplit() 165 | if node != nil { 166 | node.step_resize(-1) 167 | } 168 | return 169 | case termbox.KeyCtrlF, termbox.KeyArrowRight: 170 | node := g.active.nearest_hsplit() 171 | if node != nil { 172 | node.step_resize(1) 173 | } 174 | return 175 | case termbox.KeyCtrlB, termbox.KeyArrowLeft: 176 | node := g.active.nearest_hsplit() 177 | if node != nil { 178 | node.step_resize(-1) 179 | } 180 | return 181 | } 182 | 183 | g.set_overlay_mode(nil) 184 | } 185 | -------------------------------------------------------------------------------- /extended_mode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/nsf/termbox-go" 5 | "github.com/nsf/tulib" 6 | "strconv" 7 | ) 8 | 9 | //---------------------------------------------------------------------------- 10 | // extended mode 11 | //---------------------------------------------------------------------------- 12 | 13 | type extended_mode struct { 14 | stub_overlay_mode 15 | godit *godit 16 | } 17 | 18 | func init_extended_mode(godit *godit) extended_mode { 19 | e := extended_mode{godit: godit} 20 | e.godit.set_status("C-x") 21 | return e 22 | } 23 | 24 | func (e extended_mode) on_key(ev *termbox.Event) { 25 | g := e.godit 26 | v := g.active.leaf 27 | b := v.buf 28 | 29 | switch ev.Key { 30 | case termbox.KeyCtrlC: 31 | if g.has_unsaved_buffers() { 32 | g.set_overlay_mode(init_key_press_mode( 33 | g, 34 | map[rune]func(){ 35 | 'y': func() { 36 | g.quitflag = true 37 | }, 38 | 'n': func() {}, 39 | }, 40 | 0, 41 | "Modified buffers exist; exit anyway? (y or n)", 42 | )) 43 | return 44 | } else { 45 | g.quitflag = true 46 | } 47 | case termbox.KeyCtrlX: 48 | v.on_vcommand(vcommand_swap_cursor_and_mark, 0) 49 | case termbox.KeyCtrlW: 50 | g.set_overlay_mode(init_view_op_mode(g)) 51 | return 52 | case termbox.KeyCtrlA: 53 | v.on_vcommand(vcommand_autocompl_init, 0) 54 | case termbox.KeyCtrlU: 55 | v.on_vcommand(vcommand_region_to_upper, 0) 56 | case termbox.KeyCtrlL: 57 | v.on_vcommand(vcommand_region_to_lower, 0) 58 | case termbox.KeyCtrlF: 59 | g.set_overlay_mode(init_line_edit_mode(g, g.open_buffer_lemp())) 60 | return 61 | case termbox.KeyCtrlS: 62 | g.save_active_buffer(false) 63 | return 64 | case termbox.KeyCtrlSlash: 65 | g.active.leaf.on_vcommand(vcommand_redo, 0) 66 | g.set_overlay_mode(init_redo_mode(g)) 67 | return 68 | case termbox.KeyCtrlR: 69 | if !v.buf.is_mark_set() { 70 | v.ctx.set_status("The mark is not set now, so there is no region") 71 | break 72 | } 73 | g.set_overlay_mode(init_line_edit_mode(g, g.search_and_replace_lemp1())) 74 | return 75 | default: 76 | switch ev.Ch { 77 | case '0': 78 | g.kill_active_view() 79 | case '1': 80 | g.kill_all_views_but_active() 81 | case '2': 82 | g.split_vertically() 83 | case '3': 84 | g.split_horizontally() 85 | case 'o': 86 | sibling := g.active.sibling() 87 | if sibling != nil && sibling.leaf != nil { 88 | g.active.leaf.deactivate() 89 | g.active = sibling 90 | g.active.leaf.activate() 91 | } 92 | case 'b': 93 | g.set_overlay_mode(init_line_edit_mode(g, g.switch_buffer_lemp())) 94 | return 95 | case '(': 96 | g.set_status("Defining keyboard macro...") 97 | g.recording = true 98 | g.keymacros = g.keymacros[:0] 99 | case ')': 100 | g.stop_recording() 101 | case 'e': 102 | g.stop_recording() 103 | if len(g.keymacros) > 0 { 104 | g.set_overlay_mode(init_macro_repeat_mode(g)) 105 | return 106 | } 107 | case '>': 108 | g.set_overlay_mode(init_region_indent_mode(g, 1)) 109 | return 110 | case '<': 111 | g.set_overlay_mode(init_region_indent_mode(g, -1)) 112 | return 113 | case 'k': 114 | if !b.synced_with_disk() { 115 | g.set_overlay_mode(init_key_press_mode( 116 | g, 117 | map[rune]func(){ 118 | 'y': func() { 119 | g.kill_buffer(b) 120 | }, 121 | 'n': func() {}, 122 | }, 123 | 0, 124 | "Buffer "+b.name+" modified; kill anyway? (y or n)", 125 | )) 126 | return 127 | } else { 128 | g.kill_buffer(b) 129 | } 130 | case 'S': 131 | if ev.Mod&termbox.ModAlt != 0 { 132 | g.set_overlay_mode(init_line_edit_mode(g, 133 | g.save_as_buffer_lemp(true))) 134 | return 135 | } 136 | g.save_active_buffer(true) 137 | return 138 | case 's': 139 | if ev.Mod&termbox.ModAlt != 0 { 140 | g.set_overlay_mode(init_line_edit_mode(g, 141 | g.save_as_buffer_lemp(false))) 142 | return 143 | } 144 | case '=': 145 | var r rune 146 | if v.cursor.eol() { 147 | r = '\n' 148 | } else { 149 | r, _ = v.cursor.rune_under() 150 | } 151 | cursor_ex := make_cursor_location_ex(v.cursor) 152 | g.set_status("Char: %s (dec: %d, oct: %s, hex: %s), Cursor offset: %d bytes", 153 | strconv.QuoteRune(r), r, 154 | strconv.FormatInt(int64(r), 8), 155 | strconv.FormatInt(int64(r), 16), 156 | cursor_ex.abs_boffset) 157 | case '!': 158 | g.set_overlay_mode(init_line_edit_mode(g, g.filter_region_lemp())) 159 | return 160 | default: 161 | goto undefined 162 | } 163 | } 164 | 165 | g.set_overlay_mode(nil) 166 | return 167 | undefined: 168 | g.set_status("C-x %s is undefined", tulib.KeyToString(ev.Key, ev.Ch, ev.Mod)) 169 | g.set_overlay_mode(nil) 170 | } 171 | -------------------------------------------------------------------------------- /isearch_mode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "github.com/nsf/termbox-go" 6 | "unicode/utf8" 7 | ) 8 | 9 | var isearch_last_word = make([]byte, 0, 32) 10 | 11 | type isearch_mode struct { 12 | *line_edit_mode 13 | last_word []byte 14 | last_loc cursor_location 15 | 16 | backward bool 17 | failing bool 18 | wrapped bool 19 | 20 | prompt_isearch []byte 21 | prompt_failing []byte 22 | prompt_wrapped []byte 23 | } 24 | 25 | func init_isearch_mode(g *godit, backward bool) *isearch_mode { 26 | v := g.active.leaf 27 | m := new(isearch_mode) 28 | m.last_word = make([]byte, 0, 32) 29 | m.last_loc = v.cursor 30 | m.backward = backward 31 | m.prepare_prompts() 32 | cancel := func() { 33 | v.highlight_bytes = nil 34 | v.set_tags() 35 | v.dirty = dirty_everything 36 | } 37 | m.line_edit_mode = init_line_edit_mode(g, line_edit_mode_params{ 38 | on_apply: func(*buffer) { cancel() }, 39 | on_cancel: cancel, 40 | ac_decide: default_ac_decide, 41 | }) 42 | m.set_prompt(m.prompt_isearch) 43 | return m 44 | } 45 | 46 | func (m *isearch_mode) prepare_prompts() { 47 | if m.backward { 48 | m.prompt_isearch = []byte("I-search backward:") 49 | m.prompt_failing = []byte("Failing I-search backward:") 50 | m.prompt_wrapped = []byte("Wrapped I-search backward:") 51 | } else { 52 | m.prompt_isearch = []byte("I-search:") 53 | m.prompt_failing = []byte("Failing I-search:") 54 | m.prompt_wrapped = []byte("Wrapped I-search:") 55 | } 56 | } 57 | 58 | func (m *isearch_mode) set_prompt(prompt []byte) { 59 | m.prompt = prompt 60 | m.prompt_w = utf8.RuneCount(m.prompt) 61 | } 62 | 63 | func (m *isearch_mode) search(next bool) { 64 | v := m.godit.active.leaf 65 | v.finalize_action_group() 66 | v.last_vcommand = vcommand_move_cursor_forward 67 | 68 | var ( 69 | cursor cursor_location 70 | ok bool 71 | ) 72 | if m.backward { 73 | if !next { 74 | cursor, ok = m.last_loc.search_forward(m.last_word) 75 | if !ok || cursor != m.last_loc { 76 | cursor, ok = m.last_loc.search_backward(m.last_word) 77 | } 78 | } else { 79 | cursor, ok = m.last_loc.search_backward(m.last_word) 80 | } 81 | } else { 82 | if next && !m.wrapped { 83 | m.last_loc.boffset += len(m.last_word) 84 | } 85 | cursor, ok = m.last_loc.search_forward(m.last_word) 86 | } 87 | if !ok { 88 | v.set_tags() 89 | m.set_prompt(m.prompt_failing) 90 | m.failing = true 91 | m.wrapped = false 92 | } else { 93 | m.last_loc = cursor 94 | v.set_tags(view_tag{ 95 | beg_line: cursor.line_num, 96 | beg_offset: cursor.boffset, 97 | end_line: cursor.line_num, 98 | end_offset: cursor.boffset + len(m.last_word), 99 | fg: termbox.ColorCyan, 100 | bg: termbox.ColorMagenta, 101 | }) 102 | if !m.backward { 103 | cursor.boffset += len(m.last_word) 104 | } 105 | v.move_cursor_to(cursor) 106 | if m.wrapped { 107 | m.set_prompt(m.prompt_wrapped) 108 | m.wrapped = false 109 | } else { 110 | m.set_prompt(m.prompt_isearch) 111 | } 112 | m.failing = false 113 | } 114 | v.center_view_on_cursor() 115 | v.dirty = dirty_everything 116 | v.highlight_bytes = m.last_word 117 | } 118 | 119 | func (m *isearch_mode) restore_previous_isearch_maybe() { 120 | lw := m.godit.isearch_last_word 121 | if len(lw) == 0 { 122 | return 123 | } 124 | 125 | v := m.lineview 126 | c := v.cursor 127 | v.action_insert(c, clone_byte_slice(lw)) 128 | c.boffset += len(lw) 129 | v.move_cursor_to(c) 130 | v.dirty = dirty_everything 131 | v.finalize_action_group() 132 | } 133 | 134 | func (m *isearch_mode) wrap_location() cursor_location { 135 | v := m.godit.active.leaf 136 | if m.backward { 137 | return cursor_location{ 138 | line: v.buf.last_line, 139 | line_num: v.buf.lines_n, 140 | boffset: len(v.buf.last_line.data), 141 | } 142 | } 143 | 144 | return cursor_location{ 145 | line: v.buf.first_line, 146 | line_num: 1, 147 | boffset: 0, 148 | } 149 | } 150 | 151 | func (m *isearch_mode) advance_search() { 152 | if m.failing { 153 | m.last_loc = m.wrap_location() 154 | m.failing = false 155 | m.wrapped = true 156 | } 157 | 158 | if len(m.last_word) == 0 { 159 | m.restore_previous_isearch_maybe() 160 | } 161 | m.search(true) 162 | } 163 | 164 | func (m *isearch_mode) on_key(ev *termbox.Event) { 165 | switch ev.Key { 166 | case termbox.KeyCtrlR: 167 | if !m.backward { 168 | m.backward = true 169 | m.prepare_prompts() 170 | } 171 | m.advance_search() 172 | case termbox.KeyCtrlS: 173 | if m.backward { 174 | m.backward = false 175 | m.prepare_prompts() 176 | } 177 | m.advance_search() 178 | default: 179 | m.line_edit_mode.on_key(ev) 180 | } 181 | 182 | new_word := m.linebuf.first_line.data 183 | if bytes.Equal(new_word, m.last_word) { 184 | return 185 | } 186 | m.last_word = copy_byte_slice(m.last_word, new_word) 187 | m.godit.isearch_last_word = copy_byte_slice(m.godit.isearch_last_word, new_word) 188 | m.search(false) 189 | } 190 | -------------------------------------------------------------------------------- /view_tree.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/nsf/tulib" 5 | ) 6 | 7 | //---------------------------------------------------------------------------- 8 | // view_tree 9 | //---------------------------------------------------------------------------- 10 | 11 | type view_tree struct { 12 | // At the same time only one of these groups can be valid: 13 | // 1) 'left', 'right' and 'split' 14 | // 2) 'top', 'bottom' and 'split' 15 | // 3) 'leaf' 16 | parent *view_tree 17 | left *view_tree 18 | top *view_tree 19 | right *view_tree 20 | bottom *view_tree 21 | leaf *view 22 | split float32 23 | tulib.Rect // updated with 'resize' call 24 | } 25 | 26 | func new_view_tree_leaf(parent *view_tree, v *view) *view_tree { 27 | return &view_tree{ 28 | parent: parent, 29 | leaf: v, 30 | } 31 | } 32 | 33 | func (v *view_tree) split_vertically() { 34 | top := v.leaf 35 | bottom := new_view(top.ctx, top.buf) 36 | *v = view_tree{ 37 | parent: v.parent, 38 | top: new_view_tree_leaf(v, top), 39 | bottom: new_view_tree_leaf(v, bottom), 40 | split: 0.5, 41 | } 42 | } 43 | 44 | func (v *view_tree) split_horizontally() { 45 | left := v.leaf 46 | right := new_view(left.ctx, left.buf) 47 | *v = view_tree{ 48 | parent: v.parent, 49 | left: new_view_tree_leaf(v, left), 50 | right: new_view_tree_leaf(v, right), 51 | split: 0.5, 52 | } 53 | } 54 | 55 | func (v *view_tree) draw() { 56 | if v.leaf != nil { 57 | v.leaf.draw() 58 | return 59 | } 60 | 61 | if v.left != nil { 62 | v.left.draw() 63 | v.right.draw() 64 | } else { 65 | v.top.draw() 66 | v.bottom.draw() 67 | } 68 | } 69 | 70 | func (v *view_tree) resize(pos tulib.Rect) { 71 | v.Rect = pos 72 | if v.leaf != nil { 73 | v.leaf.resize(pos.Width, pos.Height) 74 | return 75 | } 76 | 77 | if v.left != nil { 78 | // horizontal split, use 'w' 79 | w := pos.Width 80 | if w > 0 { 81 | // reserve one line for splitter, if we have one line 82 | w-- 83 | } 84 | lw := int(float32(w) * v.split) 85 | rw := w - lw 86 | v.left.resize(tulib.Rect{pos.X, pos.Y, lw, pos.Height}) 87 | v.right.resize(tulib.Rect{pos.X + lw + 1, pos.Y, rw, pos.Height}) 88 | } else { 89 | // vertical split, use 'h', no need to reserve one line for 90 | // splitter, because splitters are part of the buffer's output 91 | // (their status bars act like a splitter) 92 | h := pos.Height 93 | th := int(float32(h) * v.split) 94 | bh := h - th 95 | v.top.resize(tulib.Rect{pos.X, pos.Y, pos.Width, th}) 96 | v.bottom.resize(tulib.Rect{pos.X, pos.Y + th, pos.Width, bh}) 97 | } 98 | } 99 | 100 | func (v *view_tree) traverse(cb func(*view_tree)) { 101 | if v.leaf != nil { 102 | cb(v) 103 | return 104 | } 105 | 106 | if v.left != nil { 107 | v.left.traverse(cb) 108 | v.right.traverse(cb) 109 | } else if v.top != nil { 110 | v.top.traverse(cb) 111 | v.bottom.traverse(cb) 112 | } 113 | } 114 | 115 | func (v *view_tree) nearest_vsplit() *view_tree { 116 | v = v.parent 117 | for v != nil { 118 | if v.top != nil { 119 | return v 120 | } 121 | v = v.parent 122 | } 123 | return nil 124 | } 125 | 126 | func (v *view_tree) nearest_hsplit() *view_tree { 127 | v = v.parent 128 | for v != nil { 129 | if v.left != nil { 130 | return v 131 | } 132 | v = v.parent 133 | } 134 | return nil 135 | } 136 | 137 | func (v *view_tree) one_step() float32 { 138 | if v.top != nil { 139 | return 1.0 / float32(v.Height) 140 | } else if v.left != nil { 141 | return 1.0 / float32(v.Width-1) 142 | } 143 | return 0.0 144 | } 145 | 146 | func (v *view_tree) normalize_split() { 147 | var off int 148 | if v.top != nil { 149 | off = int(float32(v.Height) * v.split) 150 | } else { 151 | off = int(float32(v.Width-1) * v.split) 152 | } 153 | v.split = float32(off) * v.one_step() 154 | } 155 | 156 | func (v *view_tree) step_resize(n int) { 157 | if v.Width <= 1 || v.Height <= 0 { 158 | // avoid division by zero, result is really bad 159 | return 160 | } 161 | 162 | one := v.one_step() 163 | v.normalize_split() 164 | v.split += one*float32(n) + (one * 0.5) 165 | if v.split > 1.0 { 166 | v.split = 1.0 167 | } 168 | if v.split < 0.0 { 169 | v.split = 0.0 170 | } 171 | v.resize(v.Rect) 172 | } 173 | 174 | func (v *view_tree) reparent() { 175 | if v.left != nil { 176 | v.left.parent = v 177 | v.right.parent = v 178 | } else if v.top != nil { 179 | v.top.parent = v 180 | v.bottom.parent = v 181 | } 182 | } 183 | 184 | func (v *view_tree) sibling() *view_tree { 185 | p := v.parent 186 | if p == nil { 187 | return nil 188 | } 189 | switch { 190 | case v == p.left: 191 | return p.right 192 | case v == p.right: 193 | return p.left 194 | case v == p.top: 195 | return p.bottom 196 | case v == p.bottom: 197 | return p.top 198 | } 199 | panic("unreachable") 200 | } 201 | 202 | func (v *view_tree) first_leaf_node() *view_tree { 203 | if v.left != nil { 204 | return v.left.first_leaf_node() 205 | } else if v.top != nil { 206 | return v.top.first_leaf_node() 207 | } else if v.leaf != nil { 208 | return v 209 | } 210 | panic("unreachable") 211 | } 212 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | --== Godit - a very religious text editor ==-- 2 | 3 | Screenshots: 4 | 5 | * https://nosmileface.dev/images/godit-linux1.png 6 | * https://nosmileface.dev/images/godit-linux2.png 7 | 8 | I call it religious, because there is a strong faith in the "one true way" 9 | of doing things. By that I mean things like: "the tab size is always an 10 | equivalent to 8 spaces/characters" or "each line ends with '\n' symbol and 11 | someone should end this '\r\n' madness" or "text files are always utf-8 12 | encoded". Most editors provide customizable options for these things, but 13 | godit takes a different approach in that area and has no settings at all. So, 14 | that concludes the ideology behind godit. 15 | 16 | If you're interested in what godit feels like, it would be fair to say that it 17 | is an emacsish lightweight text editor. The godit uses many of the emacs key 18 | bindings and operates using a notion of "micromodes". It's easier to explain 19 | what a micromode is by a simple example. Let's take the keyboard macros feature 20 | from both emacs and godit. You can start recording a macro using `C-x (` key 21 | combination and then when you're ready to start repeating it, you do the 22 | following: `C-x e (e...)`. Not only `C-x e` ends the recording of a macro, it 23 | executes the macro once and enters a micromode, where typing `e` again, will 24 | repeat that action. But as soon as some other key was pressed you quit this 25 | micromode and everything is back to normal again. The idea of micromode is used 26 | in godit a lot. 27 | 28 | 29 | --== List of keybindings ==-- 30 | 31 | Basic things: 32 | C-g - Universal cancel button 33 | C-x C-c - Quit from the godit 34 | C-x C-s - Save file [prompt maybe] 35 | C-x S - Save file (raw) [prompt maybe] 36 | C-x M-s - Save file as [prompt] 37 | C-x M-S - Save file as (raw) [prompt] 38 | C-x C-f - Open file 39 | M-g - Go to line [prompt] 40 | C-/ - Undo 41 | C-x C-/ (C-/...) - Redo 42 | 43 | View/buffer operations: 44 | C-x C-w - View operations mode 45 | C-x 0 - Kill active view 46 | C-x 1 - Kill all views but active 47 | C-x 2 - Split active view vertically 48 | C-x 3 - Split active view horizontally 49 | C-x o - Make a sibling view active 50 | C-x b - Switch buffer in the active view [prompt] 51 | C-x k - Kill buffer in the active view 52 | 53 | View operations mode: 54 | v - Split active view vertically 55 | h - Split active view horizontally 56 | k - Kill active view 57 | C-f, - Expand/shrink active view to the right 58 | C-b, - Expand/shrink active view to the left 59 | C-n, - Expand/shrink active view to the bottom 60 | C-p, - Expand/shrink active view to the top 61 | 1, 2, 3, 4, ... - Select view 62 | 63 | Cursor/view movement and text editing: 64 | C-f, - Move cursor one character forward 65 | M-f - Move cursor one word forward 66 | C-b, - Move cursor one character backward 67 | M-b - Move cursor one word backward 68 | C-n, - Move cursor to the next line 69 | C-p, - Move cursor to the previous line 70 | C-e, - Move cursor to the end of line 71 | C-a, - Move cursor to the beginning of the line 72 | C-v, - Move view forward (half of the screen) 73 | M-v, - Move view backward (half of the screen) 74 | C-l - Center view on line containing cursor 75 | C-s - Search forward [interactive prompt] 76 | C-r - Search backward [interactive prompt] 77 | C-j - Insert a newline character and autoindent 78 | - Insert a newline character 79 | - Delete one character backwards 80 | C-d, - Delete one character in-place 81 | M-d - Kill word 82 | M- - Kill word backwards 83 | C-k - Kill line 84 | M-u - Convert the following word to upper case 85 | M-l - Convert the following word to lower case 86 | M-c - Capitalize the following word 87 | - Insert character 88 | 89 | Mark and region operations: 90 | C- - Set mark 91 | C-x C-x - Swap cursor and mark locations 92 | C-x > (>...) - Indent region (lines between the cursor and the mark) 93 | C-x < (<...) - Deindent region (lines between the cursor and the mark) 94 | C-x C-r - Search & replace (within region) [prompt] 95 | C-x C-u - Convert the region to upper case 96 | C-x C-l - Convert the region to lower case 97 | C-w - Kill region (between the cursor and the mark) 98 | M-w - Copy region (between the cursor and the mark) 99 | C-y - Yank (aka Paste) previously killed/copied text 100 | M-q - Fill region (lines between the cursor and the mark) [prompt] 101 | 102 | Advanced: 103 | M-/ - Local words autocompletion 104 | C-x C-a - Invoke buffer specific autocompletion menu [menu] 105 | C-x ( - Start keyboard macro recording 106 | C-x ) - Stop keyboard macro recording 107 | C-x e (e...) - Stop keyboard macro recording and execute it 108 | C-x = - Info about character under the cursor 109 | C-x ! - Filter region through an external command [prompt] 110 | 111 | 112 | --== Current development state==-- 113 | 114 | I'm still in process of designing some parts of it. Bits of functionality are 115 | missing, but frankly I write godit in godit already and I use godit for 116 | everything else on my system (EDITOR=godit). This README was written in godit 117 | from scratch, I write commit messages in godit, I write code in godit, I write 118 | configs and scripts in godit. The editor is definitely usable, but it is 119 | certain that some corner cases are not covered. Just try it, perhaps you would 120 | like it. Oh and I'm very picky about feature suggestions at the moment, 121 | suggest, but don't expect too much. 122 | -------------------------------------------------------------------------------- /action.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | //---------------------------------------------------------------------------- 8 | // action 9 | // 10 | // A single entity of undo/redo history. All changes to contents of a buffer 11 | // must be initiated by an action. 12 | //---------------------------------------------------------------------------- 13 | 14 | type action_type int 15 | 16 | const ( 17 | action_insert action_type = 1 18 | action_delete action_type = -1 19 | ) 20 | 21 | type action struct { 22 | what action_type 23 | data []byte 24 | cursor cursor_location 25 | lines []*line 26 | } 27 | 28 | func (a *action) apply(v *view) { 29 | a.do(v, a.what) 30 | } 31 | 32 | func (a *action) revert(v *view) { 33 | a.do(v, -a.what) 34 | } 35 | 36 | func (a *action) insert_line(line, prev *line, v *view) { 37 | bi := prev 38 | ai := prev.next 39 | 40 | // 'bi' is always a non-nil line 41 | bi.next = line 42 | line.prev = bi 43 | 44 | // 'ai' could be nil (means we're inserting a new last line) 45 | if ai == nil { 46 | v.buf.last_line = line 47 | } else { 48 | ai.prev = line 49 | } 50 | line.next = ai 51 | } 52 | 53 | func (a *action) delete_line(line *line, v *view) { 54 | bi := line.prev 55 | ai := line.next 56 | if ai != nil { 57 | ai.prev = bi 58 | } else { 59 | v.buf.last_line = bi 60 | } 61 | if bi != nil { 62 | bi.next = ai 63 | } else { 64 | v.buf.first_line = ai 65 | } 66 | line.data = line.data[:0] 67 | } 68 | 69 | func (a *action) insert(v *view) { 70 | var data_chunk []byte 71 | nline := 0 72 | offset := a.cursor.boffset 73 | line := a.cursor.line 74 | iter_lines(a.data, func(data []byte) { 75 | if data[0] == '\n' { 76 | v.buf.bytes_n++ 77 | v.buf.lines_n++ 78 | 79 | if offset < len(line.data) { 80 | // a case where we insert at the middle of the 81 | // line, need to save that chunk for later 82 | // insertion at the end of the operation 83 | data_chunk = line.data[offset:] 84 | line.data = line.data[:offset] 85 | } 86 | // insert a line 87 | a.insert_line(a.lines[nline], line, v) 88 | line = a.lines[nline] 89 | nline++ 90 | offset = 0 91 | } else { 92 | v.buf.bytes_n += len(data) 93 | 94 | // insert a chunk of data 95 | line.data = insert_bytes(line.data, offset, data) 96 | offset += len(data) 97 | } 98 | }) 99 | if data_chunk != nil { 100 | line.data = append(line.data, data_chunk...) 101 | } 102 | } 103 | 104 | func (a *action) delete(v *view) { 105 | nline := 0 106 | offset := a.cursor.boffset 107 | line := a.cursor.line 108 | iter_lines(a.data, func(data []byte) { 109 | if data[0] == '\n' { 110 | v.buf.bytes_n-- 111 | v.buf.lines_n-- 112 | 113 | // append the contents of the deleted line the current line 114 | line.data = append(line.data, a.lines[nline].data...) 115 | // delete a line 116 | a.delete_line(a.lines[nline], v) 117 | nline++ 118 | } else { 119 | v.buf.bytes_n -= len(data) 120 | 121 | // delete a chunk of data 122 | copy(line.data[offset:], line.data[offset+len(data):]) 123 | line.data = line.data[:len(line.data)-len(data)] 124 | } 125 | }) 126 | } 127 | 128 | func (a *action) do(v *view, what action_type) { 129 | switch what { 130 | case action_insert: 131 | a.insert(v) 132 | v.on_insert_adjust_top_line(a) 133 | v.buf.other_views(v, func(v *view) { 134 | v.on_insert(a) 135 | }) 136 | if v.buf.is_mark_set() { 137 | v.buf.mark.on_insert_adjust(a) 138 | } 139 | case action_delete: 140 | a.delete(v) 141 | v.on_delete_adjust_top_line(a) 142 | v.buf.other_views(v, func(v *view) { 143 | v.on_delete(a) 144 | }) 145 | if v.buf.is_mark_set() { 146 | v.buf.mark.on_delete_adjust(a) 147 | } 148 | } 149 | v.dirty = dirty_everything 150 | 151 | // any change to the buffer causes words cache invalidation 152 | v.buf.words_cache_valid = false 153 | } 154 | 155 | func (a *action) last_line() *line { 156 | return a.lines[len(a.lines)-1] 157 | } 158 | 159 | func (a *action) last_line_affection_len() int { 160 | i := bytes.LastIndex(a.data, []byte{'\n'}) 161 | if i == -1 { 162 | return len(a.data) 163 | } 164 | 165 | return len(a.data) - i - 1 166 | } 167 | 168 | func (a *action) first_line_affection_len() int { 169 | i := bytes.Index(a.data, []byte{'\n'}) 170 | if i == -1 { 171 | return len(a.data) 172 | } 173 | 174 | return i 175 | } 176 | 177 | // returns the range of deleted lines, the first and the last one 178 | func (a *action) deleted_lines() (int, int) { 179 | first := a.cursor.line_num + 1 180 | last := first + len(a.lines) - 1 181 | return first, last 182 | } 183 | 184 | func (a *action) try_merge(b *action) bool { 185 | if a.what != b.what { 186 | // can only merge actions of the same type 187 | return false 188 | } 189 | 190 | if a.cursor.line_num != b.cursor.line_num { 191 | return false 192 | } 193 | 194 | if a.cursor.boffset == b.cursor.boffset { 195 | pa, pb := a, b 196 | if a.what == action_insert { 197 | // on insertion merge as 'ba', on deletion as 'ab' 198 | pa, pb = pb, pa 199 | } 200 | pa.data = append(pa.data, pb.data...) 201 | pa.lines = append(pa.lines, pb.lines...) 202 | *a = *pa 203 | return true 204 | } 205 | 206 | // different boffsets, try to restore the sequence 207 | pa, pb := a, b 208 | if pb.cursor.boffset < pa.cursor.boffset { 209 | pa, pb = pb, pa 210 | } 211 | if pa.cursor.boffset+len(pa.data) == pb.cursor.boffset { 212 | pa.data = append(pa.data, pb.data...) 213 | pa.lines = append(pa.lines, pb.lines...) 214 | *a = *pa 215 | return true 216 | } 217 | return false 218 | } 219 | 220 | //---------------------------------------------------------------------------- 221 | // action group 222 | //---------------------------------------------------------------------------- 223 | 224 | type action_group struct { 225 | actions []action 226 | next *action_group 227 | prev *action_group 228 | before cursor_location 229 | after cursor_location 230 | } 231 | 232 | func (ag *action_group) append(a *action) { 233 | if len(ag.actions) != 0 { 234 | // Oh, we have something in the group already, let's try to 235 | // merge this action with the last one. 236 | last := &ag.actions[len(ag.actions)-1] 237 | if last.try_merge(a) { 238 | return 239 | } 240 | } 241 | ag.actions = append(ag.actions, *a) 242 | } 243 | 244 | // Valid only as long as no new actions were added to the action group. 245 | func (ag *action_group) last_action() *action { 246 | if len(ag.actions) == 0 { 247 | return nil 248 | } 249 | return &ag.actions[len(ag.actions)-1] 250 | } 251 | -------------------------------------------------------------------------------- /buffer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "unicode/utf8" 10 | ) 11 | 12 | //---------------------------------------------------------------------------- 13 | // line 14 | //---------------------------------------------------------------------------- 15 | 16 | type line struct { 17 | data []byte 18 | next *line 19 | prev *line 20 | } 21 | 22 | // Find a set of closest offsets for a given visual offset 23 | func (l *line) find_closest_offsets(voffset int) (bo, co, vo int) { 24 | data := l.data 25 | for len(data) > 0 { 26 | var vodif int 27 | r, rlen := utf8.DecodeRune(data) 28 | data = data[rlen:] 29 | vodif = rune_advance_len(r, vo) 30 | if vo+vodif > voffset { 31 | return 32 | } 33 | 34 | bo += rlen 35 | co += 1 36 | vo += vodif 37 | } 38 | return 39 | } 40 | 41 | //---------------------------------------------------------------------------- 42 | // buffer 43 | //---------------------------------------------------------------------------- 44 | 45 | type buffer struct { 46 | views []*view 47 | first_line *line 48 | last_line *line 49 | loc view_location 50 | lines_n int 51 | bytes_n int 52 | history *action_group 53 | on_disk *action_group 54 | mark cursor_location 55 | 56 | // absoulte path of the file, if it's empty string, then the file has no 57 | // on-disk representation 58 | path string 59 | 60 | // buffer name (displayed in the status line), must be unique, 61 | // uniqueness is maintained by godit methods 62 | name string 63 | 64 | // cache for local buffer autocompletion 65 | words_cache llrb_tree 66 | words_cache_valid bool 67 | } 68 | 69 | func new_empty_buffer() *buffer { 70 | b := new(buffer) 71 | l := new(line) 72 | l.next = nil 73 | l.prev = nil 74 | b.first_line = l 75 | b.last_line = l 76 | b.lines_n = 1 77 | b.loc = view_location{ 78 | top_line: l, 79 | top_line_num: 1, 80 | cursor: cursor_location{ 81 | line: l, 82 | line_num: 1, 83 | }, 84 | } 85 | b.init_history() 86 | return b 87 | } 88 | 89 | func new_buffer(r io.Reader) (*buffer, error) { 90 | var err error 91 | var prevline *line 92 | 93 | br := bufio.NewReader(r) 94 | l := new(line) 95 | b := new(buffer) 96 | b.loc = view_location{ 97 | top_line: l, 98 | top_line_num: 1, 99 | cursor: cursor_location{ 100 | line: l, 101 | line_num: 1, 102 | }, 103 | } 104 | b.lines_n = 1 105 | b.first_line = l 106 | for { 107 | l.data, err = br.ReadBytes('\n') 108 | if err != nil { 109 | // last line was read 110 | break 111 | } else { 112 | b.bytes_n += len(l.data) 113 | 114 | // cut off the '\n' character 115 | l.data = l.data[:len(l.data)-1] 116 | } 117 | 118 | b.lines_n++ 119 | l.next = new(line) 120 | l.prev = prevline 121 | prevline = l 122 | l = l.next 123 | } 124 | l.prev = prevline 125 | b.last_line = l 126 | 127 | // io.EOF is not an error 128 | if err == io.EOF { 129 | err = nil 130 | } 131 | 132 | // history 133 | b.init_history() 134 | return b, err 135 | } 136 | 137 | func (b *buffer) add_view(v *view) { 138 | b.views = append(b.views, v) 139 | } 140 | 141 | func (b *buffer) delete_view(v *view) { 142 | vi := -1 143 | for i, n := 0, len(b.views); i < n; i++ { 144 | if b.views[i] == v { 145 | vi = i 146 | break 147 | } 148 | } 149 | 150 | if vi != -1 { 151 | lasti := len(b.views) - 1 152 | b.views[vi], b.views[lasti] = b.views[lasti], b.views[vi] 153 | b.views = b.views[:lasti] 154 | } 155 | } 156 | 157 | func (b *buffer) other_views(v *view, cb func(*view)) { 158 | for _, ov := range b.views { 159 | if v == ov { 160 | continue 161 | } 162 | cb(ov) 163 | } 164 | } 165 | 166 | func (b *buffer) init_history() { 167 | // the trick here is that I set 'sentinel' as 'history', it is required 168 | // to maintain an invariant, where 'history' is a sentinel or is not 169 | // empty 170 | 171 | sentinel := new(action_group) 172 | first := new(action_group) 173 | sentinel.next = first 174 | first.prev = sentinel 175 | b.history = sentinel 176 | b.on_disk = sentinel 177 | } 178 | 179 | func (b *buffer) is_mark_set() bool { 180 | return b.mark.line != nil 181 | } 182 | 183 | func (b *buffer) dump_history() { 184 | cur := b.history 185 | for cur.prev != nil { 186 | cur = cur.prev 187 | } 188 | 189 | p := func(format string, args ...interface{}) { 190 | fmt.Fprintf(os.Stderr, format, args...) 191 | } 192 | 193 | i := 0 194 | for cur != nil { 195 | p("action group %d: %d actions\n", i, len(cur.actions)) 196 | for _, a := range cur.actions { 197 | switch a.what { 198 | case action_insert: 199 | p(" + insert") 200 | case action_delete: 201 | p(" - delete") 202 | } 203 | p(" (%2d,%2d):%q\n", a.cursor.line_num, 204 | a.cursor.boffset, string(a.data)) 205 | } 206 | cur = cur.next 207 | i++ 208 | } 209 | } 210 | 211 | func (b *buffer) save() error { 212 | return b.save_as(b.path) 213 | } 214 | 215 | func (b *buffer) save_as(filename string) error { 216 | r := b.reader() 217 | f, err := os.Create(filename) 218 | if err != nil { 219 | return err 220 | } 221 | defer f.Close() 222 | 223 | _, err = io.Copy(f, r) 224 | if err != nil { 225 | return err 226 | } 227 | 228 | b.on_disk = b.history 229 | for _, v := range b.views { 230 | v.dirty |= dirty_status 231 | } 232 | return nil 233 | } 234 | 235 | func (b *buffer) synced_with_disk() bool { 236 | return b.on_disk == b.history 237 | } 238 | 239 | func (b *buffer) reader() *buffer_reader { 240 | return new_buffer_reader(b) 241 | } 242 | 243 | func (b *buffer) contents() []byte { 244 | data, _ := ioutil.ReadAll(b.reader()) 245 | return data 246 | } 247 | 248 | func (b *buffer) refill_words_cache() { 249 | b.words_cache.clear() 250 | line := b.first_line 251 | for line != nil { 252 | iter_words(line.data, func(word []byte) { 253 | b.words_cache.insert_maybe(word) 254 | }) 255 | line = line.next 256 | } 257 | } 258 | 259 | func (b *buffer) update_words_cache() { 260 | if b.words_cache_valid { 261 | return 262 | } 263 | 264 | b.refill_words_cache() 265 | b.words_cache_valid = true 266 | } 267 | 268 | //---------------------------------------------------------------------------- 269 | // buffer_reader 270 | //---------------------------------------------------------------------------- 271 | 272 | type buffer_reader struct { 273 | buffer *buffer 274 | line *line 275 | offset int 276 | } 277 | 278 | func new_buffer_reader(buffer *buffer) *buffer_reader { 279 | br := new(buffer_reader) 280 | br.buffer = buffer 281 | br.line = buffer.first_line 282 | br.offset = 0 283 | return br 284 | } 285 | 286 | func (br *buffer_reader) Read(data []byte) (int, error) { 287 | nread := 0 288 | for len(data) > 0 { 289 | if br.line == nil { 290 | return nread, io.EOF 291 | } 292 | 293 | // how much can we read from current line 294 | can_read := len(br.line.data) - br.offset 295 | if len(data) <= can_read { 296 | // if this is all we need, return 297 | n := copy(data, br.line.data[br.offset:]) 298 | nread += n 299 | br.offset += n 300 | break 301 | } 302 | 303 | // otherwise try to read '\n' and jump to the next line 304 | n := copy(data, br.line.data[br.offset:]) 305 | nread += n 306 | data = data[n:] 307 | if len(data) > 0 && br.line != br.buffer.last_line { 308 | data[0] = '\n' 309 | data = data[1:] 310 | nread++ 311 | } 312 | 313 | br.line = br.line.next 314 | br.offset = 0 315 | } 316 | return nread, nil 317 | } 318 | -------------------------------------------------------------------------------- /cursor_location.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "unicode/utf8" 6 | ) 7 | 8 | //---------------------------------------------------------------------------- 9 | // cursor location 10 | //---------------------------------------------------------------------------- 11 | 12 | type cursor_location struct { 13 | line *line 14 | line_num int 15 | boffset int 16 | } 17 | 18 | func (c *cursor_location) rune_under() (rune, int) { 19 | return utf8.DecodeRune(c.line.data[c.boffset:]) 20 | } 21 | 22 | func (c *cursor_location) rune_before() (rune, int) { 23 | return utf8.DecodeLastRune(c.line.data[:c.boffset]) 24 | } 25 | 26 | func (c *cursor_location) first_line() bool { 27 | return c.line.prev == nil 28 | } 29 | 30 | func (c *cursor_location) last_line() bool { 31 | return c.line.next == nil 32 | } 33 | 34 | // end of line 35 | func (c *cursor_location) eol() bool { 36 | return c.boffset == len(c.line.data) 37 | } 38 | 39 | // beginning of line 40 | func (c *cursor_location) bol() bool { 41 | return c.boffset == 0 42 | } 43 | 44 | // returns the distance between two locations in bytes 45 | func (a cursor_location) distance(b cursor_location) int { 46 | s := 1 47 | if b.line_num < a.line_num { 48 | a, b = b, a 49 | s = -1 50 | } else if a.line_num == b.line_num && b.boffset < a.boffset { 51 | a, b = b, a 52 | s = -1 53 | } 54 | 55 | n := 0 56 | for a.line != b.line { 57 | n += len(a.line.data) - a.boffset + 1 58 | a.line = a.line.next 59 | a.boffset = 0 60 | } 61 | n += b.boffset - a.boffset 62 | return n * s 63 | } 64 | 65 | // Find a visual and a character offset for a given cursor 66 | func (c *cursor_location) voffset_coffset() (vo, co int) { 67 | data := c.line.data[:c.boffset] 68 | for len(data) > 0 { 69 | r, rlen := utf8.DecodeRune(data) 70 | data = data[rlen:] 71 | co += 1 72 | vo += rune_advance_len(r, vo) 73 | } 74 | return 75 | } 76 | 77 | // Find a visual offset for a given cursor 78 | func (c *cursor_location) voffset() (vo int) { 79 | data := c.line.data[:c.boffset] 80 | for len(data) > 0 { 81 | r, rlen := utf8.DecodeRune(data) 82 | data = data[rlen:] 83 | vo += rune_advance_len(r, vo) 84 | } 85 | return 86 | } 87 | 88 | func (c *cursor_location) coffset() (co int) { 89 | data := c.line.data[:c.boffset] 90 | for len(data) > 0 { 91 | _, rlen := utf8.DecodeRune(data) 92 | data = data[rlen:] 93 | co += 1 94 | } 95 | return 96 | } 97 | 98 | func (c *cursor_location) extract_bytes(n int) []byte { 99 | var buf bytes.Buffer 100 | offset := c.boffset 101 | line := c.line 102 | for n > 0 { 103 | switch { 104 | case offset < len(line.data): 105 | nb := len(line.data) - offset 106 | if n < nb { 107 | nb = n 108 | } 109 | buf.Write(line.data[offset : offset+nb]) 110 | n -= nb 111 | offset += nb 112 | case offset == len(line.data): 113 | buf.WriteByte('\n') 114 | offset = 0 115 | line = line.next 116 | n -= 1 117 | default: 118 | panic("unreachable") 119 | } 120 | } 121 | return buf.Bytes() 122 | } 123 | 124 | func (c *cursor_location) move_one_rune_forward() { 125 | if c.last_line() && c.eol() { 126 | return 127 | } 128 | 129 | if c.eol() { 130 | c.line = c.line.next 131 | c.line_num++ 132 | c.boffset = 0 133 | } else { 134 | _, rlen := c.rune_under() 135 | c.boffset += rlen 136 | } 137 | } 138 | 139 | func (c *cursor_location) move_one_rune_backward() { 140 | if c.first_line() && c.bol() { 141 | return 142 | } 143 | 144 | if c.bol() { 145 | c.line = c.line.prev 146 | c.line_num-- 147 | c.boffset = len(c.line.data) 148 | } else { 149 | _, rlen := c.rune_before() 150 | c.boffset -= rlen 151 | } 152 | } 153 | 154 | func (c *cursor_location) move_n_bytes_forward(buf []byte) { 155 | for len(buf) > 0 { 156 | _, rlen := utf8.DecodeRune(buf) 157 | buf = buf[rlen:] 158 | c.move_one_rune_forward() 159 | } 160 | } 161 | 162 | func (c *cursor_location) move_beginning_of_line() { 163 | c.boffset = 0 164 | } 165 | 166 | func (c *cursor_location) move_end_of_line() { 167 | c.boffset = len(c.line.data) 168 | } 169 | 170 | func (c *cursor_location) word_under_cursor() []byte { 171 | end, beg := *c, *c 172 | r, rlen := beg.rune_before() 173 | if r == utf8.RuneError { 174 | return nil 175 | } 176 | 177 | for is_word(r) && !beg.bol() { 178 | beg.boffset -= rlen 179 | r, rlen = beg.rune_before() 180 | } 181 | 182 | if beg.boffset == end.boffset { 183 | return nil 184 | } 185 | return c.line.data[beg.boffset:end.boffset] 186 | } 187 | 188 | // returns true if the move was successful, false if EOF reached. 189 | func (c *cursor_location) move_one_word_forward() bool { 190 | // move cursor forward until the first word rune is met 191 | for { 192 | if c.eol() { 193 | if c.last_line() { 194 | return false 195 | } else { 196 | c.line = c.line.next 197 | c.line_num++ 198 | c.boffset = 0 199 | continue 200 | } 201 | } 202 | 203 | r, rlen := c.rune_under() 204 | for !is_word(r) && !c.eol() { 205 | c.boffset += rlen 206 | r, rlen = c.rune_under() 207 | } 208 | 209 | if c.eol() { 210 | continue 211 | } 212 | break 213 | } 214 | 215 | // now the cursor is under the word rune, skip all of them 216 | r, rlen := c.rune_under() 217 | for is_word(r) && !c.eol() { 218 | c.boffset += rlen 219 | r, rlen = c.rune_under() 220 | } 221 | 222 | return true 223 | } 224 | 225 | // returns true if the move was successful, false if BOF reached. 226 | func (c *cursor_location) move_one_word_backward() bool { 227 | // move cursor backward while previous rune is not a word rune 228 | for { 229 | if c.bol() { 230 | if c.first_line() { 231 | return false 232 | } else { 233 | c.line = c.line.prev 234 | c.line_num-- 235 | c.boffset = len(c.line.data) 236 | continue 237 | } 238 | } 239 | 240 | r, rlen := c.rune_before() 241 | for !is_word(r) && !c.bol() { 242 | c.boffset -= rlen 243 | r, rlen = c.rune_before() 244 | } 245 | 246 | if c.bol() { 247 | continue 248 | } 249 | break 250 | } 251 | 252 | // now the rune behind the cursor is a word rune, while it's true, move 253 | // backwards 254 | r, rlen := c.rune_before() 255 | for is_word(r) && !c.bol() { 256 | c.boffset -= rlen 257 | r, rlen = c.rune_before() 258 | } 259 | 260 | return true 261 | } 262 | 263 | func (c *cursor_location) on_insert_adjust(a *action) { 264 | if a.cursor.line_num > c.line_num { 265 | return 266 | } 267 | if a.cursor.line_num < c.line_num { 268 | // inserted something above the cursor, adjust it 269 | c.line_num += len(a.lines) 270 | return 271 | } 272 | 273 | // insertion on the cursor line 274 | if a.cursor.boffset < c.boffset { 275 | // insertion before the cursor, move cursor along with insertion 276 | if len(a.lines) == 0 { 277 | // no lines were inserted, simply adjust the offset 278 | c.boffset += len(a.data) 279 | } else { 280 | // one or more lines were inserted, adjust cursor 281 | // respectively 282 | c.line = a.last_line() 283 | c.line_num += len(a.lines) 284 | c.boffset = a.last_line_affection_len() + 285 | c.boffset - a.cursor.boffset 286 | } 287 | } 288 | } 289 | 290 | func (c *cursor_location) on_delete_adjust(a *action) { 291 | if a.cursor.line_num > c.line_num { 292 | return 293 | } 294 | if a.cursor.line_num < c.line_num { 295 | // deletion above the cursor line, may touch the cursor location 296 | if len(a.lines) == 0 { 297 | // no lines were deleted, no things to adjust 298 | return 299 | } 300 | 301 | first, last := a.deleted_lines() 302 | if first <= c.line_num && c.line_num <= last { 303 | // deleted the cursor line, see how much it affects it 304 | n := 0 305 | if last == c.line_num { 306 | n = c.boffset - a.last_line_affection_len() 307 | if n < 0 { 308 | n = 0 309 | } 310 | } 311 | *c = a.cursor 312 | c.boffset += n 313 | } else { 314 | // phew.. no worries 315 | c.line_num -= len(a.lines) 316 | return 317 | } 318 | } 319 | 320 | // the last case is deletion on the cursor line, see what was deleted 321 | if a.cursor.boffset >= c.boffset { 322 | // deleted something after cursor, don't care 323 | return 324 | } 325 | 326 | n := c.boffset - (a.cursor.boffset + a.first_line_affection_len()) 327 | if n < 0 { 328 | n = 0 329 | } 330 | c.boffset = a.cursor.boffset + n 331 | } 332 | 333 | func (c cursor_location) search_forward(word []byte) (cursor_location, bool) { 334 | for c.line != nil { 335 | i := bytes.Index(c.line.data[c.boffset:], word) 336 | if i != -1 { 337 | c.boffset += i 338 | return c, true 339 | } 340 | 341 | c.line = c.line.next 342 | c.line_num++ 343 | c.boffset = 0 344 | } 345 | return c, false 346 | } 347 | 348 | func (c cursor_location) search_backward(word []byte) (cursor_location, bool) { 349 | for { 350 | i := bytes.LastIndex(c.line.data[:c.boffset], word) 351 | if i != -1 { 352 | c.boffset = i 353 | return c, true 354 | } 355 | 356 | c.line = c.line.prev 357 | if c.line == nil { 358 | break 359 | } 360 | c.line_num-- 361 | c.boffset = len(c.line.data) 362 | } 363 | return c, false 364 | } 365 | 366 | func swap_cursors_maybe(c1, c2 cursor_location) (r1, r2 cursor_location) { 367 | if c1.line_num == c2.line_num { 368 | if c1.boffset > c2.boffset { 369 | return c2, c1 370 | } else { 371 | return c1, c2 372 | } 373 | } 374 | 375 | if c1.line_num > c2.line_num { 376 | return c2, c1 377 | } 378 | return c1, c2 379 | } 380 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "github.com/nsf/tulib" 6 | "os" 7 | "path/filepath" 8 | "strconv" 9 | "strings" 10 | "unicode" 11 | "unicode/utf8" 12 | ) 13 | 14 | var invisible_rune_table = []rune{ 15 | '@', // 0 16 | 'A', // 1 17 | 'B', // 2 18 | 'C', // 3 19 | 'D', // 4 20 | 'E', // 5 21 | 'F', // 6 22 | 'G', // 7 23 | 'H', // 8 24 | 'I', // 9 25 | 'J', // 10 26 | 'K', // 11 27 | 'L', // 12 28 | 'M', // 13 29 | 'N', // 14 30 | 'O', // 15 31 | 'P', // 16 32 | 'Q', // 17 33 | 'R', // 18 34 | 'S', // 19 35 | 'T', // 20 36 | 'U', // 21 37 | 'V', // 22 38 | 'W', // 23 39 | 'X', // 24 40 | 'Y', // 25 41 | 'Z', // 26 42 | '[', // 27 43 | '\\', // 28 44 | ']', // 29 45 | '^', // 30 46 | '_', // 31 47 | } 48 | 49 | func make_godit_default_label_params() tulib.LabelParams { 50 | lp := tulib.DefaultLabelParams 51 | lp.Ellipsis = '~' 52 | return lp 53 | } 54 | 55 | var default_label_params = make_godit_default_label_params() 56 | 57 | // somewhat close to what wcwidth does, except rune_width doesn't return 0 or 58 | // -1, it's always 1 or 2 59 | func rune_width(r rune) int { 60 | if r >= 0x1100 && 61 | (r <= 0x115f || r == 0x2329 || r == 0x232a || 62 | (r >= 0x2e80 && r <= 0xa4cf && r != 0x303f) || 63 | (r >= 0xac00 && r <= 0xd7a3) || 64 | (r >= 0xf900 && r <= 0xfaff) || 65 | (r >= 0xfe30 && r <= 0xfe6f) || 66 | (r >= 0xff00 && r <= 0xff60) || 67 | (r >= 0xffe0 && r <= 0xffe6) || 68 | (r >= 0x20000 && r <= 0x2fffd) || 69 | (r >= 0x30000 && r <= 0x3fffd)) { 70 | return 2 71 | } 72 | return 1 73 | } 74 | 75 | func rune_advance_len(r rune, pos int) int { 76 | switch { 77 | case r == '\t': 78 | return tabstop_length - pos%tabstop_length 79 | case r < 32: 80 | // for invisible chars like ^R ^@ and such, two cells 81 | return 2 82 | } 83 | return rune_width(r) 84 | } 85 | 86 | func vlen(data []byte, pos int) int { 87 | origin := pos 88 | for len(data) > 0 { 89 | r, rlen := utf8.DecodeRune(data) 90 | data = data[rlen:] 91 | pos += rune_advance_len(r, pos) 92 | } 93 | return pos - origin 94 | } 95 | 96 | func iter_nonspace_words(data []byte, cb func(word []byte)) { 97 | for { 98 | for len(data) > 0 && is_space(data[0]) { 99 | data = data[1:] 100 | } 101 | 102 | if len(data) == 0 { 103 | return 104 | } 105 | 106 | i := 0 107 | for i < len(data) && !is_space(data[i]) { 108 | i += 1 109 | } 110 | cb(data[:i]) 111 | data = data[i:] 112 | } 113 | } 114 | 115 | func iter_words(data []byte, cb func(word []byte)) { 116 | for { 117 | if len(data) == 0 { 118 | return 119 | } 120 | 121 | r, rlen := utf8.DecodeRune(data) 122 | // skip non-word runes 123 | for !is_word(r) { 124 | data = data[rlen:] 125 | if len(data) == 0 { 126 | return 127 | } 128 | r, rlen = utf8.DecodeRune(data) 129 | } 130 | 131 | // must be on a word rune 132 | i := 0 133 | for is_word(r) && i < len(data) { 134 | i += rlen 135 | r, rlen = utf8.DecodeRune(data[i:]) 136 | } 137 | cb(data[:i]) 138 | data = data[i:] 139 | } 140 | } 141 | 142 | func iter_words_backward(data []byte, cb func(word []byte)) { 143 | for { 144 | if len(data) == 0 { 145 | return 146 | } 147 | 148 | r, rlen := utf8.DecodeLastRune(data) 149 | // skip non-word runes 150 | for !is_word(r) { 151 | data = data[:len(data)-rlen] 152 | if len(data) == 0 { 153 | return 154 | } 155 | r, rlen = utf8.DecodeLastRune(data) 156 | } 157 | 158 | // must be on a word rune 159 | i := len(data) 160 | for is_word(r) && i > 0 { 161 | i -= rlen 162 | r, rlen = utf8.DecodeLastRune(data[:i]) 163 | } 164 | cb(data[i:]) 165 | data = data[:i] 166 | } 167 | } 168 | 169 | func readdir_stat(dir string, f *os.File) ([]os.FileInfo, error) { 170 | names, err := f.Readdirnames(-1) 171 | if err != nil { 172 | return nil, err 173 | } 174 | 175 | fis := make([]os.FileInfo, 0, len(names)) 176 | for _, name := range names { 177 | fi, err := os.Stat(filepath.Join(dir, name)) 178 | if err != nil { 179 | continue 180 | } 181 | fis = append(fis, fi) 182 | } 183 | return fis, nil 184 | } 185 | 186 | func index_first_non_space(s []byte) int { 187 | for i := 0; i < len(s); i++ { 188 | if s[i] != '\t' && s[i] != ' ' { 189 | return i 190 | } 191 | } 192 | return len(s) 193 | } 194 | 195 | func index_last_non_space(s []byte) int { 196 | for i := len(s) - 1; i >= 0; i-- { 197 | if s[i] != '\t' && s[i] != ' ' { 198 | return i 199 | } 200 | } 201 | return -1 202 | } 203 | 204 | func abs_path(filename string) string { 205 | path, err := filepath.Abs(filename) 206 | if err != nil { 207 | panic(err) 208 | } 209 | return path 210 | } 211 | 212 | func grow_byte_slice(s []byte, desired_cap int) []byte { 213 | if cap(s) < desired_cap { 214 | ns := make([]byte, len(s), desired_cap) 215 | copy(ns, s) 216 | return ns 217 | } 218 | return s 219 | } 220 | 221 | func insert_bytes(s []byte, offset int, data []byte) []byte { 222 | n := len(s) + len(data) 223 | s = grow_byte_slice(s, n) 224 | s = s[:n] 225 | copy(s[offset+len(data):], s[offset:]) 226 | copy(s[offset:], data) 227 | return s 228 | } 229 | 230 | func copy_byte_slice(dst, src []byte) []byte { 231 | if cap(dst) < len(src) { 232 | dst = clone_byte_slice(src) 233 | } 234 | dst = dst[:len(src)] 235 | copy(dst, src) 236 | return dst 237 | } 238 | 239 | func clone_byte_slice(s []byte) []byte { 240 | c := make([]byte, len(s)) 241 | copy(c, s) 242 | return c 243 | } 244 | 245 | // assumes the same line and a.boffset < b.offset order 246 | func bytes_between(a, b cursor_location) []byte { 247 | return a.line.data[a.boffset:b.boffset] 248 | } 249 | 250 | func is_word(r rune) bool { 251 | return r == '_' || unicode.IsLetter(r) || unicode.IsNumber(r) 252 | } 253 | 254 | func is_space(b byte) bool { 255 | return b == ' ' || b == '\t' || b == '\n' 256 | } 257 | 258 | func find_place_for_rect(win, pref tulib.Rect) tulib.Rect { 259 | var vars [4]tulib.Rect 260 | 261 | vars[0] = pref.Intersection(win) 262 | if vars[0] == pref { 263 | // this is just a common path, everything fits in 264 | return pref 265 | } 266 | 267 | // If a rect doesn't fit in the window, try to select the most 268 | // optimal position amongst mirrored variants. 269 | 270 | // invert X 271 | vars[1] = pref 272 | vars[1].X = win.Width - pref.Width 273 | vars[1] = vars[1].Intersection(win) 274 | 275 | // invert Y 276 | vars[2] = pref 277 | vars[2].Y -= pref.Height + 1 278 | vars[2] = vars[2].Intersection(win) 279 | 280 | // invert X and Y 281 | vars[3] = pref 282 | vars[3].X = win.Width - pref.Width 283 | vars[3].Y -= pref.Height + 1 284 | vars[3] = vars[3].Intersection(win) 285 | 286 | optimal_i, optimal_w, optimal_h := 0, 0, 0 287 | // find optimal width 288 | for i := 0; i < 4; i++ { 289 | if vars[i].Width > optimal_w { 290 | optimal_w = vars[i].Width 291 | } 292 | } 293 | 294 | // find optimal height (amongst optimal widths) and its index 295 | for i := 0; i < 4; i++ { 296 | if vars[i].Width != optimal_w { 297 | continue 298 | } 299 | if vars[i].Height > optimal_h { 300 | optimal_h = vars[i].Height 301 | optimal_i = i 302 | } 303 | } 304 | return vars[optimal_i] 305 | } 306 | 307 | // Function will iterate 'data' contents, calling 'cb' on some data or on '\n', 308 | // but never both. For example, given this data: "\n123\n123\n\n", it will call 309 | // 'cb' 6 times: ['\n', '123', '\n', '123', '\n', '\n'] 310 | func iter_lines(data []byte, cb func([]byte)) { 311 | offset := 0 312 | for { 313 | if offset == len(data) { 314 | return 315 | } 316 | 317 | i := bytes.IndexByte(data[offset:], '\n') 318 | switch i { 319 | case -1: 320 | cb(data[offset:]) 321 | return 322 | case 0: 323 | cb(data[offset : offset+1]) 324 | offset++ 325 | continue 326 | } 327 | 328 | cb(data[offset : offset+i]) 329 | cb(data[offset+i : offset+i+1]) 330 | offset += i + 1 331 | } 332 | } 333 | 334 | var double_comma = []byte(",,") 335 | 336 | func split_double_csv(data []byte) (a, b []byte) { 337 | i := bytes.Index(data, double_comma) 338 | if i == -1 { 339 | return data, nil 340 | } 341 | 342 | return data[:i], data[i+2:] 343 | } 344 | 345 | type line_reader struct { 346 | data []byte 347 | offset int 348 | } 349 | 350 | func new_line_reader(data []byte) line_reader { 351 | return line_reader{data, 0} 352 | } 353 | 354 | func (l *line_reader) read_line() []byte { 355 | data := l.data[l.offset:] 356 | i := bytes.Index(data, []byte{'\n'}) 357 | if i == -1 { 358 | l.offset = len(l.data) 359 | return data 360 | } 361 | 362 | l.offset += i + 1 363 | return data[:i] 364 | } 365 | 366 | func atoi(data []byte) (int, error) { 367 | return strconv.Atoi(string(data)) 368 | } 369 | 370 | func substitute_home(path string) string { 371 | if !strings.HasPrefix(path, "~") { 372 | return path 373 | } 374 | home := os.Getenv("HOME") 375 | if home == "" { 376 | panic("HOME is not set") 377 | } 378 | return filepath.Join(home, path[1:]) 379 | } 380 | 381 | func substitute_symlinks(path string) string { 382 | if path == "" { 383 | return "" 384 | } 385 | after, err := filepath.EvalSymlinks(path) 386 | if err != nil { 387 | return path 388 | } 389 | 390 | if strings.HasSuffix(path, string(filepath.Separator)) { 391 | return after + string(filepath.Separator) 392 | } 393 | return after 394 | } 395 | 396 | func is_file_hidden(path string) bool { 397 | if path == "." || path == ".." { 398 | return true 399 | } 400 | 401 | if len(path) > 1 { 402 | if strings.HasPrefix(path, "./") { 403 | return false 404 | } 405 | if strings.HasPrefix(path, "..") { 406 | return false 407 | } 408 | if strings.HasPrefix(path, ".") { 409 | return true 410 | } 411 | } 412 | return false 413 | } 414 | -------------------------------------------------------------------------------- /autocomplete.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "github.com/nsf/termbox-go" 6 | "github.com/nsf/tulib" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "sort" 11 | "strconv" 12 | "strings" 13 | "unicode/utf8" 14 | ) 15 | 16 | //---------------------------------------------------------------------------- 17 | // extended cursor location (includes absolute bytes offset) 18 | //---------------------------------------------------------------------------- 19 | 20 | type cursor_location_ex struct { 21 | cursor_location 22 | abs_boffset int 23 | } 24 | 25 | func make_cursor_location_ex(cursor cursor_location) cursor_location_ex { 26 | off := cursor.boffset 27 | line := cursor.line.prev 28 | for line != nil { 29 | off += len(line.data) + 1 // plus one is for '\n' 30 | line = line.prev 31 | } 32 | return cursor_location_ex{ 33 | cursor_location: cursor, 34 | abs_boffset: off, 35 | } 36 | } 37 | 38 | //---------------------------------------------------------------------------- 39 | // autocompletion 40 | //---------------------------------------------------------------------------- 41 | 42 | const ac_max_filtered = 200 43 | const ac_ui_max_lines = 14 44 | 45 | type ac_proposal struct { 46 | display []byte 47 | content []byte 48 | } 49 | 50 | type ( 51 | ac_func func(view *view) ([]ac_proposal, int) 52 | ac_decide_func func(view *view) ac_func 53 | ) 54 | 55 | type autocompl struct { 56 | // data 57 | origin cursor_location 58 | current cursor_location 59 | proposals []ac_proposal 60 | filtered []ac_proposal 61 | 62 | // ui 63 | cursor int 64 | view int 65 | tmpbuf bytes.Buffer 66 | } 67 | 68 | // Creates a new autocompletion object and makes a query for ac proposals, may 69 | // take a while. 70 | func new_autocompl(f ac_func, view *view) *autocompl { 71 | var charsback int 72 | ac := new(autocompl) 73 | ac.filtered = make([]ac_proposal, 0, ac_max_filtered) 74 | ac.proposals, charsback = f(view) 75 | if len(ac.proposals) == 0 { 76 | return nil 77 | } 78 | 79 | if charsback > 0 { 80 | origin := view.cursor 81 | 82 | // adjust origin if we have positive 'charsback' 83 | for charsback > 0 { 84 | view.move_cursor_backward() 85 | charsback-- 86 | } 87 | 88 | // delete region between the origin and the new cursor position 89 | view.action_delete(view.cursor, view.cursor.distance(origin)) 90 | view.finalize_action_group() 91 | } 92 | ac.origin = view.cursor 93 | ac.current = view.cursor 94 | 95 | // insert the common part of all the autocompletion proposals 96 | common := ac.common() 97 | if len(common) > 0 { 98 | c := view.cursor 99 | view.action_insert(c, common) 100 | c.boffset += len(common) 101 | view.move_cursor_to(c) 102 | view.finalize_action_group() 103 | ac.update(view.cursor) 104 | } 105 | return ac 106 | } 107 | 108 | func (ac *autocompl) common() []byte { 109 | common := ac.proposals[0].content 110 | common_n := len(common) 111 | for _, p := range ac.proposals { 112 | if len(p.content) < common_n { 113 | common_n = len(p.content) 114 | } 115 | 116 | for i := 0; i < common_n; i++ { 117 | if common[i] != p.content[i] { 118 | common_n = i 119 | break 120 | } 121 | } 122 | } 123 | 124 | return clone_byte_slice(common[:common_n]) 125 | } 126 | 127 | func (ac *autocompl) actual_proposals() []ac_proposal { 128 | if ac.origin.boffset != ac.current.boffset { 129 | return ac.filtered 130 | } 131 | return ac.proposals 132 | } 133 | 134 | // Returns 'true' if update was successful, 'false' if autocompletion should be 135 | // discarded. 136 | func (ac *autocompl) update(current cursor_location) bool { 137 | if ac.origin.line_num != current.line_num { 138 | return false 139 | } 140 | if ac.origin.boffset > current.boffset { 141 | return false 142 | } 143 | 144 | if ac.current.boffset == current.boffset { 145 | // false update, skip it 146 | return true 147 | } 148 | 149 | ac.current = current 150 | if ac.current.boffset == ac.origin.boffset { 151 | // simply discard filtered stuff 152 | return true 153 | } 154 | 155 | ac.filtered = ac.filtered[:0] 156 | filter := bytes_between(ac.origin, ac.current) 157 | j := 0 158 | for i := 0; i < ac_max_filtered; i++ { 159 | if j >= len(ac.proposals) { 160 | break 161 | } 162 | if bytes.HasPrefix(ac.proposals[j].content, filter) { 163 | ac.filtered = append(ac.filtered, ac.proposals[j]) 164 | } else { 165 | i-- 166 | } 167 | j++ 168 | } 169 | if len(ac.filtered) == 0 { 170 | // no filtered stuff, cancel autocompletion 171 | return false 172 | } 173 | return true 174 | } 175 | 176 | func (ac *autocompl) move_cursor_down() { 177 | if ac.cursor >= len(ac.actual_proposals())-1 { 178 | return 179 | } 180 | ac.cursor++ 181 | } 182 | 183 | func (ac *autocompl) move_cursor_up() { 184 | if ac.cursor <= 0 { 185 | return 186 | } 187 | ac.cursor-- 188 | } 189 | 190 | func (ac *autocompl) desired_height() int { 191 | proposals := ac.actual_proposals() 192 | minh := 0 193 | for i := 0; i < ac_ui_max_lines; i++ { 194 | n := ac.view + i 195 | if n >= len(proposals) { 196 | break 197 | } 198 | minh++ 199 | } 200 | return minh 201 | } 202 | 203 | func (ac *autocompl) desired_width(height int) int { 204 | proposals := ac.actual_proposals() 205 | minw := 0 206 | for i := 0; i < height; i++ { 207 | n := ac.view + i 208 | line_len := utf8.RuneCount(proposals[n].display) 209 | if line_len > minw { 210 | minw = line_len 211 | } 212 | } 213 | return minw + 2 214 | } 215 | 216 | func (ac *autocompl) adjust_view(height int) { 217 | if ac.cursor < ac.view { 218 | ac.view = ac.cursor 219 | } 220 | 221 | if ac.cursor >= ac.view+height { 222 | ac.view = ac.cursor - height + 1 223 | } 224 | } 225 | 226 | func (ac *autocompl) validate_cursor() { 227 | if ac.cursor >= len(ac.actual_proposals()) { 228 | ac.cursor = 0 229 | ac.view = 0 230 | } 231 | } 232 | 233 | // -1 if no need to make a slider 234 | func (ac *autocompl) slider_pos_and_rune(height int) (int, rune) { 235 | proposals := ac.actual_proposals() 236 | if len(proposals) == height { 237 | return -1, 0 238 | } 239 | max := len(proposals) - height 240 | if ac.view == max { 241 | return height - 1, '▄' 242 | } 243 | 244 | var r rune 245 | progress := int((float32(ac.view) / float32(max)) * float32(height*2)) 246 | if progress&1 != 0 { 247 | r = '▄' 248 | } else { 249 | r = '▀' 250 | } 251 | return progress / 2, r 252 | } 253 | 254 | func (ac *autocompl) draw_onto(buf *tulib.Buffer, x, y int) { 255 | ac.validate_cursor() 256 | 257 | h := ac.desired_height() 258 | dst := find_place_for_rect(buf.Rect, tulib.Rect{x, y + 1, 1, h}) 259 | ac.adjust_view(dst.Height) 260 | w := ac.desired_width(dst.Height) 261 | dst = find_place_for_rect(buf.Rect, tulib.Rect{x, y + 1, w, h}) 262 | 263 | slider_i, slider_r := ac.slider_pos_and_rune(dst.Height) 264 | lp := default_label_params 265 | 266 | r := dst 267 | r.Width-- 268 | r.Height = 1 269 | for i := 0; i < dst.Height; i++ { 270 | lp.Fg = termbox.ColorBlack 271 | lp.Bg = termbox.ColorWhite 272 | 273 | n := ac.view + i 274 | if n == ac.cursor { 275 | lp.Fg = termbox.ColorWhite 276 | lp.Bg = termbox.ColorBlue 277 | } 278 | buf.Fill(r, termbox.Cell{ 279 | Fg: lp.Fg, 280 | Bg: lp.Bg, 281 | Ch: ' ', 282 | }) 283 | buf.DrawLabel(r, &lp, ac.actual_proposals()[n].display) 284 | 285 | sr := ' ' 286 | if i == slider_i { 287 | sr = slider_r 288 | } 289 | buf.Set(r.X+r.Width, r.Y, termbox.Cell{ 290 | Fg: termbox.ColorWhite, 291 | Bg: termbox.ColorBlue, 292 | Ch: sr, 293 | }) 294 | r.Y++ 295 | } 296 | } 297 | 298 | func (ac *autocompl) finalize(view *view) { 299 | d := ac.origin.distance(ac.current) 300 | if d < 0 { 301 | panic("something went really wrong, oops..") 302 | } 303 | data := clone_byte_slice(ac.actual_proposals()[ac.cursor].content[d:]) 304 | view.action_insert(ac.current, data) 305 | ac.current.boffset += len(data) 306 | view.move_cursor_to(ac.current) 307 | } 308 | 309 | //---------------------------------------------------------------------------- 310 | // local buffer autocompletion 311 | //---------------------------------------------------------------------------- 312 | 313 | func local_ac(view *view) ([]ac_proposal, int) { 314 | var dups llrb_tree 315 | var others llrb_tree 316 | proposals := make([]ac_proposal, 0, 100) 317 | prefix := view.cursor.word_under_cursor() 318 | 319 | // update word caches 320 | view.other_buffers(func(buf *buffer) { 321 | buf.update_words_cache() 322 | }) 323 | 324 | collect := func(ignorecase bool) { 325 | words := view.collect_words([][]byte(nil), &dups, ignorecase) 326 | for _, word := range words { 327 | proposals = append(proposals, ac_proposal{ 328 | display: word, 329 | content: word, 330 | }) 331 | } 332 | 333 | lprefix := prefix 334 | if ignorecase { 335 | lprefix = bytes.ToLower(prefix) 336 | } 337 | view.other_buffers(func(buf *buffer) { 338 | buf.words_cache.walk(func(word []byte) { 339 | lword := word 340 | if ignorecase { 341 | lword = bytes.ToLower(word) 342 | } 343 | if bytes.HasPrefix(lword, lprefix) { 344 | ok := dups.insert_maybe(word) 345 | if !ok { 346 | return 347 | } 348 | others.insert_maybe(word) 349 | } 350 | }) 351 | }) 352 | others.walk(func(word []byte) { 353 | proposals = append(proposals, ac_proposal{ 354 | display: word, 355 | content: word, 356 | }) 357 | }) 358 | others.clear() 359 | } 360 | collect(false) 361 | if len(proposals) == 0 { 362 | collect(true) 363 | } 364 | 365 | if prefix != nil { 366 | return proposals, utf8.RuneCount(prefix) 367 | } 368 | return proposals, 0 369 | } 370 | 371 | //---------------------------------------------------------------------------- 372 | // gocode autocompletion 373 | //---------------------------------------------------------------------------- 374 | 375 | func gocode_ac(view *view) ([]ac_proposal, int) { 376 | cursor_ex := make_cursor_location_ex(view.cursor) 377 | var out bytes.Buffer 378 | gocode := exec.Command("gocode", "-f=godit", "autocomplete", 379 | view.buf.path, strconv.Itoa(cursor_ex.abs_boffset)) 380 | gocode.Stdin = view.buf.reader() 381 | gocode.Stdout = &out 382 | 383 | err := gocode.Run() 384 | if err != nil { 385 | return nil, 0 386 | } 387 | 388 | lr := new_line_reader(out.Bytes()) 389 | charsback_str, proposals_n_str := split_double_csv(lr.read_line()) 390 | charsback, err := atoi(charsback_str) 391 | if err != nil { 392 | return nil, 0 393 | } 394 | proposals_n, err := atoi(proposals_n_str) 395 | if err != nil { 396 | return nil, 0 397 | } 398 | 399 | proposals := make([]ac_proposal, proposals_n) 400 | for i := 0; i < proposals_n; i++ { 401 | d, c := split_double_csv(lr.read_line()) 402 | proposals[i].display = d 403 | proposals[i].content = c 404 | } 405 | return proposals, charsback 406 | } 407 | 408 | //---------------------------------------------------------------------------- 409 | // buffer autocompletion 410 | //---------------------------------------------------------------------------- 411 | 412 | func make_godit_buffer_ac_decide(godit *godit) ac_decide_func { 413 | return func(view *view) ac_func { 414 | return make_godit_buffer_ac(godit) 415 | } 416 | } 417 | 418 | func make_godit_buffer_ac(godit *godit) ac_func { 419 | return func(view *view) ([]ac_proposal, int) { 420 | prefix := string(view.buf.contents()[:view.cursor.boffset]) 421 | proposals := make([]ac_proposal, 0, 20) 422 | for _, buf := range godit.buffers { 423 | if strings.HasPrefix(buf.name, prefix) { 424 | display := make([]byte, len(buf.name), len(buf.name)+5) 425 | content := display 426 | copy(display, buf.name) 427 | if !buf.synced_with_disk() { 428 | display = display[:len(display)+5] 429 | copy(display[len(content):], " (**)") 430 | } 431 | proposals = append(proposals, ac_proposal{ 432 | display: display, 433 | content: content, 434 | }) 435 | } 436 | } 437 | 438 | return proposals, view.cursor_coffset 439 | } 440 | } 441 | 442 | //---------------------------------------------------------------------------- 443 | // file system autocompletion 444 | //---------------------------------------------------------------------------- 445 | 446 | func filesystem_line_ac_decide(view *view) ac_func { 447 | return filesystem_line_ac 448 | } 449 | 450 | type filesystem_slice []os.FileInfo 451 | 452 | func (s filesystem_slice) Len() int { return len(s) } 453 | func (s filesystem_slice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 454 | func (s filesystem_slice) Less(i, j int) bool { 455 | idir := s[i].IsDir() 456 | jdir := s[j].IsDir() 457 | if idir != jdir { 458 | if idir { 459 | return true 460 | } 461 | return false 462 | } 463 | 464 | return s[i].Name() < s[j].Name() 465 | } 466 | 467 | func filesystem_line_ac(view *view) ([]ac_proposal, int) { 468 | var dirfd *os.File 469 | var err error 470 | path := string(view.buf.contents()[:view.cursor.boffset]) 471 | path = substitute_home(path) 472 | path = substitute_symlinks(path) 473 | dir, partfile := filepath.Split(path) 474 | if dir == "" { 475 | dirfd, err = os.Open(".") 476 | } else { 477 | dirfd, err = os.Open(dir) 478 | } 479 | if err != nil { 480 | return nil, 0 481 | } 482 | fis, err := readdir_stat(dir, dirfd) 483 | if err != nil { 484 | // can we recover something from here? 485 | return nil, 0 486 | } 487 | sort.Sort(filesystem_slice(fis)) 488 | proposals := make([]ac_proposal, 0, 20) 489 | match_files := func(ignorecase bool) { 490 | if ignorecase { 491 | partfile = strings.ToLower(partfile) 492 | } 493 | for _, fi := range fis { 494 | name := fi.Name() 495 | if is_file_hidden(name) { 496 | continue 497 | } 498 | tmpname := name 499 | if ignorecase { 500 | tmpname = strings.ToLower(tmpname) 501 | } 502 | if strings.HasPrefix(tmpname, partfile) { 503 | suffix := "" 504 | if fi.IsDir() { 505 | suffix = string(filepath.Separator) 506 | } 507 | proposals = append(proposals, ac_proposal{ 508 | display: []byte(dir + name + suffix), 509 | content: []byte(dir + name + suffix), 510 | }) 511 | } 512 | } 513 | } 514 | match_files(false) 515 | if len(proposals) == 0 { 516 | match_files(true) 517 | } 518 | return proposals, view.cursor_coffset 519 | } 520 | -------------------------------------------------------------------------------- /godit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/nsf/termbox-go" 7 | "github.com/nsf/tulib" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strconv" 12 | ) 13 | 14 | const ( 15 | tabstop_length = 8 16 | view_vertical_threshold = 5 17 | view_horizontal_threshold = 10 18 | ) 19 | 20 | // this is a structure which represents a key press, used for keyboard macros 21 | type key_event struct { 22 | mod termbox.Modifier 23 | _ [1]byte 24 | key termbox.Key 25 | ch rune 26 | } 27 | 28 | func create_key_event(ev *termbox.Event) key_event { 29 | return key_event{ 30 | mod: ev.Mod, 31 | key: ev.Key, 32 | ch: ev.Ch, 33 | } 34 | } 35 | 36 | func (k key_event) to_termbox_event() termbox.Event { 37 | return termbox.Event{ 38 | Type: termbox.EventKey, 39 | Mod: k.mod, 40 | Key: k.key, 41 | Ch: k.ch, 42 | } 43 | } 44 | 45 | //---------------------------------------------------------------------------- 46 | // godit 47 | // 48 | // Main top-level structure, that handles views composition, status bar and 49 | // input messaging. Also it's the spot where keyboard macros are implemented. 50 | //---------------------------------------------------------------------------- 51 | 52 | type godit struct { 53 | uibuf tulib.Buffer 54 | active *view_tree // this one is always a leaf node 55 | views *view_tree // a root node 56 | buffers []*buffer 57 | lastcmdclass vcommand_class 58 | statusbuf bytes.Buffer 59 | quitflag bool 60 | overlay overlay_mode 61 | termbox_event chan termbox.Event 62 | keymacros []key_event 63 | recording bool 64 | killbuffer []byte 65 | isearch_last_word []byte 66 | s_and_r_last_word []byte 67 | s_and_r_last_repl []byte 68 | } 69 | 70 | func new_godit(filenames []string) *godit { 71 | g := new(godit) 72 | g.buffers = make([]*buffer, 0, 20) 73 | for _, filename := range filenames { 74 | g.new_buffer_from_file(filename) 75 | } 76 | if len(g.buffers) == 0 { 77 | buf := new_empty_buffer() 78 | buf.name = g.buffer_name("unnamed") 79 | g.buffers = append(g.buffers, buf) 80 | } 81 | g.views = new_view_tree_leaf(nil, new_view(g.view_context(), g.buffers[0])) 82 | g.active = g.views 83 | g.keymacros = make([]key_event, 0, 50) 84 | g.isearch_last_word = make([]byte, 0, 32) 85 | return g 86 | } 87 | 88 | func (g *godit) kill_buffer(buf *buffer) { 89 | var replacement *buffer 90 | views := make([]*view, len(buf.views)) 91 | copy(views, buf.views) 92 | 93 | // find replacement buffer 94 | if len(views) > 0 { 95 | for _, gbuf := range g.buffers { 96 | if gbuf == buf { 97 | continue 98 | } 99 | replacement = gbuf 100 | break 101 | } 102 | if replacement == nil { 103 | replacement = new_empty_buffer() 104 | replacement.name = g.buffer_name("unnamed") 105 | g.buffers = append(g.buffers, replacement) 106 | } 107 | } 108 | 109 | // replace the buffer we're killing with replacement one for 110 | // all the views 111 | for _, v := range views { 112 | v.attach(replacement) 113 | } 114 | 115 | // remove buffer from the list 116 | bi := -1 117 | for i, n := 0, len(g.buffers); i < n; i++ { 118 | if g.buffers[i] == buf { 119 | bi = i 120 | break 121 | } 122 | } 123 | 124 | if bi == -1 { 125 | panic("removing non-existent buffer") 126 | } 127 | 128 | copy(g.buffers[bi:], g.buffers[bi+1:]) 129 | g.buffers = g.buffers[:len(g.buffers)-1] 130 | } 131 | 132 | func (g *godit) find_buffer_by_full_path(path string) *buffer { 133 | for _, buf := range g.buffers { 134 | if buf.path == path { 135 | return buf 136 | } 137 | } 138 | return nil 139 | } 140 | 141 | func (g *godit) open_buffers_from_pattern(pattern string) { 142 | matches, err := filepath.Glob(pattern) 143 | if err != nil { 144 | panic(err) 145 | } 146 | 147 | var buf *buffer 148 | for _, match := range matches { 149 | buf, _ = g.new_buffer_from_file(match) 150 | } 151 | if buf == nil { 152 | buf, _ = g.new_buffer_from_file(pattern) 153 | } 154 | if buf == nil { 155 | buf = new_empty_buffer() 156 | buf.name = g.buffer_name("unnamed") 157 | } 158 | g.active.leaf.attach(buf) 159 | } 160 | 161 | func (g *godit) buffer_name_exists(name string) bool { 162 | for _, buf := range g.buffers { 163 | if buf.name == name { 164 | return true 165 | } 166 | } 167 | return false 168 | } 169 | 170 | func (g *godit) buffer_name(name string) string { 171 | if !g.buffer_name_exists(name) { 172 | return name 173 | } 174 | 175 | for i := 2; i < 9999; i++ { 176 | candidate := name + " <" + strconv.Itoa(i) + ">" 177 | if !g.buffer_name_exists(candidate) { 178 | return candidate 179 | } 180 | } 181 | panic("too many buffers opened with the same name") 182 | } 183 | 184 | func (g *godit) new_buffer_from_file(filename string) (*buffer, error) { 185 | fullpath := abs_path(filename) 186 | buf := g.find_buffer_by_full_path(fullpath) 187 | if buf != nil { 188 | return buf, nil 189 | } 190 | 191 | _, err := os.Stat(fullpath) 192 | if err != nil { 193 | // assume the file is just not there 194 | g.set_status("(New file)") 195 | buf = new_empty_buffer() 196 | } else { 197 | f, err := os.Open(fullpath) 198 | if err != nil { 199 | g.set_status(err.Error()) 200 | return nil, err 201 | } 202 | defer f.Close() 203 | buf, err = new_buffer(f) 204 | if err != nil { 205 | g.set_status(err.Error()) 206 | return nil, err 207 | } 208 | buf.path = fullpath 209 | } 210 | 211 | buf.name = g.buffer_name(filename) 212 | g.buffers = append(g.buffers, buf) 213 | return buf, nil 214 | } 215 | 216 | func (g *godit) set_status(format string, args ...interface{}) { 217 | g.statusbuf.Reset() 218 | fmt.Fprintf(&g.statusbuf, format, args...) 219 | } 220 | 221 | func (g *godit) split_horizontally() { 222 | if g.active.Width == 0 { 223 | return 224 | } 225 | g.active.split_horizontally() 226 | g.active = g.active.left 227 | g.resize() 228 | } 229 | 230 | func (g *godit) split_vertically() { 231 | if g.active.Height == 0 { 232 | return 233 | } 234 | g.active.split_vertically() 235 | g.active = g.active.top 236 | g.resize() 237 | } 238 | 239 | func (g *godit) kill_active_view() { 240 | p := g.active.parent 241 | if p == nil { 242 | return 243 | } 244 | 245 | pp := p.parent 246 | sib := g.active.sibling() 247 | g.active.leaf.deactivate() 248 | g.active.leaf.detach() 249 | 250 | *p = *sib 251 | p.parent = pp 252 | p.reparent() 253 | 254 | g.active = p.first_leaf_node() 255 | g.active.leaf.activate() 256 | g.resize() 257 | } 258 | 259 | func (g *godit) kill_all_views_but_active() { 260 | g.views.traverse(func(v *view_tree) { 261 | if v == g.active { 262 | return 263 | } 264 | if v.leaf != nil { 265 | v.leaf.detach() 266 | } 267 | }) 268 | g.views = g.active 269 | g.views.parent = nil 270 | g.resize() 271 | } 272 | 273 | // Call it manually only when views layout has changed. 274 | func (g *godit) resize() { 275 | g.uibuf = tulib.TermboxBuffer() 276 | views_area := g.uibuf.Rect 277 | views_area.Height -= 1 // reserve space for command line 278 | g.views.resize(views_area) 279 | } 280 | 281 | func (g *godit) draw_autocompl() { 282 | view := g.active.leaf 283 | x, y := g.active.X, g.active.Y 284 | if view.ac == nil { 285 | return 286 | } 287 | 288 | proposals := view.ac.actual_proposals() 289 | if len(proposals) > 0 { 290 | cx, cy := view.cursor_position_for(view.ac.origin) 291 | view.ac.draw_onto(&g.uibuf, x+cx, y+cy) 292 | } 293 | } 294 | 295 | func (g *godit) draw() { 296 | var overlay_needs_cursor bool 297 | if g.overlay != nil { 298 | overlay_needs_cursor = g.overlay.needs_cursor() 299 | } 300 | 301 | // draw everything 302 | g.views.draw() 303 | g.composite_recursively(g.views) 304 | g.draw_status() 305 | 306 | // draw overlay if any 307 | if g.overlay != nil { 308 | g.overlay.draw() 309 | } 310 | 311 | // draw autocompletion 312 | if !overlay_needs_cursor { 313 | g.draw_autocompl() 314 | } 315 | 316 | // update cursor position 317 | var cx, cy int 318 | if overlay_needs_cursor { 319 | // this can be true, only when g.overlay != nil, see above 320 | cx, cy = g.overlay.cursor_position() 321 | } else { 322 | cx, cy = g.cursor_position() 323 | } 324 | termbox.SetCursor(cx, cy) 325 | } 326 | 327 | func (g *godit) draw_status() { 328 | lp := default_label_params 329 | r := g.uibuf.Rect 330 | r.Y = r.Height - 1 331 | r.Height = 1 332 | g.uibuf.Fill(r, termbox.Cell{Fg: lp.Fg, Bg: lp.Bg, Ch: ' '}) 333 | g.uibuf.DrawLabel(r, &lp, g.statusbuf.Bytes()) 334 | } 335 | 336 | func (g *godit) composite_recursively(v *view_tree) { 337 | if v.leaf != nil { 338 | g.uibuf.Blit(v.Rect, 0, 0, &v.leaf.uibuf) 339 | return 340 | } 341 | 342 | if v.left != nil { 343 | g.composite_recursively(v.left) 344 | g.composite_recursively(v.right) 345 | splitter := v.right.Rect 346 | splitter.X -= 1 347 | splitter.Width = 1 348 | splitter.Height -= 1 349 | g.uibuf.Fill(splitter, termbox.Cell{ 350 | Fg: termbox.AttrReverse, 351 | Bg: termbox.AttrReverse, 352 | Ch: '|', 353 | }) 354 | } else { 355 | g.composite_recursively(v.top) 356 | g.composite_recursively(v.bottom) 357 | } 358 | } 359 | 360 | func (g *godit) cursor_position() (int, int) { 361 | x, y := g.active.leaf.cursor_position() 362 | return g.active.X + x, g.active.Y + y 363 | } 364 | 365 | func (g *godit) on_sys_key(ev *termbox.Event) { 366 | switch ev.Key { 367 | case termbox.KeyCtrlG: 368 | v := g.active.leaf 369 | v.ac = nil 370 | g.set_overlay_mode(nil) 371 | g.set_status("Quit") 372 | case termbox.KeyCtrlZ: 373 | suspend(g) 374 | } 375 | } 376 | 377 | func (g *godit) on_alt_key(ev *termbox.Event) bool { 378 | switch ev.Ch { 379 | case 'g': 380 | g.set_overlay_mode(init_line_edit_mode(g, g.goto_line_lemp())) 381 | return true 382 | case '/': 383 | g.set_overlay_mode(init_autocomplete_mode(g)) 384 | return true 385 | case 'q': 386 | g.set_overlay_mode(init_fill_region_mode(g)) 387 | return true 388 | } 389 | return false 390 | } 391 | 392 | func (g *godit) on_key(ev *termbox.Event) { 393 | v := g.active.leaf 394 | switch ev.Key { 395 | case termbox.KeyCtrlX: 396 | g.set_overlay_mode(init_extended_mode(g)) 397 | case termbox.KeyCtrlS: 398 | g.set_overlay_mode(init_isearch_mode(g, false)) 399 | case termbox.KeyCtrlR: 400 | g.set_overlay_mode(init_isearch_mode(g, true)) 401 | default: 402 | if ev.Mod&termbox.ModAlt != 0 && g.on_alt_key(ev) { 403 | break 404 | } 405 | v.on_key(ev) 406 | } 407 | } 408 | 409 | func (g *godit) main_loop() { 410 | g.termbox_event = make(chan termbox.Event, 20) 411 | go func() { 412 | for { 413 | g.termbox_event <- termbox.PollEvent() 414 | } 415 | }() 416 | for { 417 | select { 418 | case ev := <-g.termbox_event: 419 | ok := g.handle_event(&ev) 420 | if !ok { 421 | return 422 | } 423 | g.consume_more_events() 424 | g.draw() 425 | termbox.Flush() 426 | } 427 | } 428 | } 429 | 430 | func (g *godit) consume_more_events() bool { 431 | for { 432 | select { 433 | case ev := <-g.termbox_event: 434 | ok := g.handle_event(&ev) 435 | if !ok { 436 | return false 437 | } 438 | default: 439 | return true 440 | } 441 | } 442 | panic("unreachable") 443 | } 444 | 445 | func (g *godit) handle_event(ev *termbox.Event) bool { 446 | switch ev.Type { 447 | case termbox.EventKey: 448 | if g.recording { 449 | g.keymacros = append(g.keymacros, create_key_event(ev)) 450 | } 451 | g.set_status("") // reset status on every key event 452 | g.on_sys_key(ev) 453 | if g.overlay != nil { 454 | g.overlay.on_key(ev) 455 | } else { 456 | g.on_key(ev) 457 | } 458 | 459 | if g.quitflag { 460 | return false 461 | } 462 | case termbox.EventResize: 463 | termbox.Clear(termbox.ColorDefault, termbox.ColorDefault) 464 | g.resize() 465 | if g.overlay != nil { 466 | g.overlay.on_resize(ev) 467 | } 468 | case termbox.EventError: 469 | panic(ev.Err) 470 | } 471 | 472 | // just dump the current view location from the view to the buffer 473 | // after each event, it's cheap and does what it needs to be done 474 | v := g.active.leaf 475 | v.buf.loc = v.view_location 476 | return true 477 | } 478 | 479 | func (g *godit) set_overlay_mode(m overlay_mode) { 480 | if g.overlay != nil { 481 | g.overlay.exit() 482 | } 483 | g.overlay = m 484 | } 485 | 486 | // used by extended mode only 487 | func (g *godit) save_active_buffer(raw bool) { 488 | v := g.active.leaf 489 | b := v.buf 490 | 491 | if b.path != "" { 492 | if b.synced_with_disk() { 493 | g.set_status("(No changes need to be saved)") 494 | g.set_overlay_mode(nil) 495 | return 496 | } 497 | 498 | v.presave_cleanup(raw) 499 | err := b.save() 500 | if err != nil { 501 | g.set_status(err.Error()) 502 | } else { 503 | g.set_status("Wrote %s", b.path) 504 | } 505 | g.set_overlay_mode(nil) 506 | return 507 | } 508 | 509 | g.set_overlay_mode(init_line_edit_mode(g, g.save_as_buffer_lemp(raw))) 510 | } 511 | 512 | // "lemp" stands for "line edit mode params" 513 | func (g *godit) switch_buffer_lemp() line_edit_mode_params { 514 | return line_edit_mode_params{ 515 | ac_decide: make_godit_buffer_ac_decide(g), 516 | prompt: "Buffer:", 517 | init_autocompl: true, 518 | 519 | on_apply: func(buf *buffer) { 520 | bufname := string(buf.contents()) 521 | for _, buf := range g.buffers { 522 | if buf.name == bufname { 523 | g.active.leaf.attach(buf) 524 | return 525 | } 526 | } 527 | g.set_status("(Buffer with this name doesn't exist)") 528 | }, 529 | } 530 | } 531 | 532 | // "lemp" stands for "line edit mode params" 533 | func (g *godit) open_buffer_lemp() line_edit_mode_params { 534 | return line_edit_mode_params{ 535 | ac_decide: filesystem_line_ac_decide, 536 | prompt: "Find file:", 537 | 538 | on_apply: func(buf *buffer) { 539 | pattern := string(buf.contents()) 540 | if pattern == "" { 541 | g.set_status("(Nothing to open)") 542 | return 543 | } 544 | g.open_buffers_from_pattern(pattern) 545 | }, 546 | } 547 | } 548 | 549 | // "lemp" stands for "line edit mode params" 550 | func (g *godit) save_as_buffer_lemp(raw bool) line_edit_mode_params { 551 | v := g.active.leaf 552 | b := v.buf 553 | return line_edit_mode_params{ 554 | ac_decide: filesystem_line_ac_decide, 555 | prompt: "File to save in:", 556 | initial_content: b.name, 557 | 558 | on_apply: func(linebuf *buffer) { 559 | v.presave_cleanup(raw) 560 | name := string(linebuf.contents()) 561 | fullpath := abs_path(name) 562 | err := b.save_as(fullpath) 563 | if err != nil { 564 | g.set_status(err.Error()) 565 | } else { 566 | b.name = "" 567 | b.name = g.buffer_name(name) 568 | b.path = fullpath 569 | v.dirty |= dirty_status 570 | g.set_status("Wrote %s", b.path) 571 | } 572 | }, 573 | } 574 | } 575 | 576 | // "lemp" stands for "line edit mode params" 577 | func (g *godit) filter_region_lemp() line_edit_mode_params { 578 | v := g.active.leaf 579 | return line_edit_mode_params{ 580 | ac_decide: filesystem_line_ac_decide, 581 | prompt: "Filter region through:", 582 | on_apply: func(linebuf *buffer) { 583 | v.finalize_action_group() 584 | cmdstr := string(linebuf.contents()) 585 | v.region_to(func(data []byte) []byte { 586 | // TODO: not portable 587 | cmd := exec.Command("/bin/sh", "-c", cmdstr) 588 | in, err := cmd.StdinPipe() 589 | if err != nil { 590 | return clone_byte_slice(data) 591 | } 592 | 593 | in.Write(data) 594 | in.Close() 595 | 596 | out, err := cmd.Output() 597 | if err != nil { 598 | return clone_byte_slice(data) 599 | } 600 | return out 601 | }) 602 | v.finalize_action_group() 603 | }, 604 | } 605 | } 606 | 607 | // "lemp" stands for "line edit mode params" 608 | func (g *godit) goto_line_lemp() line_edit_mode_params { 609 | v := g.active.leaf 610 | return line_edit_mode_params{ 611 | prompt: "Goto line:", 612 | on_apply: func(buf *buffer) { 613 | numstr := string(buf.contents()) 614 | num, err := strconv.Atoi(numstr) 615 | if err != nil { 616 | g.set_status(err.Error()) 617 | return 618 | } 619 | v.on_vcommand(vcommand_move_cursor_to_line, rune(num)) 620 | }, 621 | } 622 | } 623 | 624 | // "lemp" stands for "line edit mode params" 625 | func (g *godit) search_and_replace_lemp1() line_edit_mode_params { 626 | var prompt string 627 | if len(g.s_and_r_last_word) != 0 { 628 | prompt = fmt.Sprintf("Replace string [%s]:", g.s_and_r_last_word) 629 | } else { 630 | prompt = "Replace string:" 631 | } 632 | return line_edit_mode_params{ 633 | prompt: prompt, 634 | on_apply: func(buf *buffer) { 635 | var word []byte 636 | contents := buf.contents() 637 | if len(contents) == 0 { 638 | if len(g.s_and_r_last_word) != 0 { 639 | word = g.s_and_r_last_word 640 | } 641 | } else { 642 | word = contents 643 | } 644 | if word == nil { 645 | g.set_status("Nothing to replace") 646 | return 647 | } 648 | g.set_overlay_mode(init_line_edit_mode(g, g.search_and_replace_lemp2(word))) 649 | }, 650 | } 651 | } 652 | 653 | // "lemp" stands for "line edit mode params" 654 | func (g *godit) search_and_replace_lemp2(word []byte) line_edit_mode_params { 655 | var prompt string 656 | if len(g.s_and_r_last_repl) != 0 { 657 | prompt = fmt.Sprintf("Replace string %s with [%s]:", word, g.s_and_r_last_repl) 658 | } else { 659 | prompt = fmt.Sprintf("Replace string %s with:", word) 660 | } 661 | v := g.active.leaf 662 | return line_edit_mode_params{ 663 | prompt: prompt, 664 | on_apply: func(buf *buffer) { 665 | var repl []byte 666 | contents := buf.contents() 667 | if len(contents) == 0 { 668 | if len(g.s_and_r_last_repl) != 0 { 669 | repl = g.s_and_r_last_repl 670 | } 671 | } else { 672 | repl = contents 673 | } 674 | v.finalize_action_group() 675 | v.last_vcommand = vcommand_none 676 | g.active.leaf.search_and_replace(word, repl) 677 | v.finalize_action_group() 678 | g.s_and_r_last_word = word 679 | g.s_and_r_last_repl = repl 680 | }, 681 | } 682 | } 683 | 684 | func (g *godit) stop_recording() { 685 | if !g.recording { 686 | g.set_status("Not defining keyboard macro") 687 | return 688 | } 689 | 690 | // clean up the current key combo: "C-x )" 691 | g.recording = false 692 | g.keymacros = g.keymacros[:len(g.keymacros)-2] 693 | if len(g.keymacros) == 0 { 694 | g.set_status("Ignore empty macro") 695 | } else { 696 | g.set_status("Keyboard macro defined") 697 | } 698 | } 699 | 700 | func (g *godit) replay_macro() { 701 | for _, keyev := range g.keymacros { 702 | ev := keyev.to_termbox_event() 703 | g.handle_event(&ev) 704 | } 705 | } 706 | 707 | func (g *godit) view_context() view_context { 708 | return view_context{ 709 | set_status: func(f string, args ...interface{}) { 710 | g.set_status(f, args...) 711 | }, 712 | kill_buffer: &g.killbuffer, 713 | buffers: &g.buffers, 714 | } 715 | } 716 | 717 | func (g *godit) has_unsaved_buffers() bool { 718 | for _, buf := range g.buffers { 719 | if !buf.synced_with_disk() { 720 | return true 721 | } 722 | } 723 | return false 724 | } 725 | 726 | func main() { 727 | err := termbox.Init() 728 | if err != nil { 729 | panic(err) 730 | } 731 | defer termbox.Close() 732 | termbox.SetInputMode(termbox.InputAlt) 733 | 734 | godit := new_godit(os.Args[1:]) 735 | godit.resize() 736 | godit.draw() 737 | termbox.SetCursor(godit.cursor_position()) 738 | termbox.Flush() 739 | godit.main_loop() 740 | } 741 | -------------------------------------------------------------------------------- /view.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/nsf/termbox-go" 7 | "github.com/nsf/tulib" 8 | "os" 9 | "strings" 10 | "unicode/utf8" 11 | ) 12 | 13 | //---------------------------------------------------------------------------- 14 | // dirty flag 15 | //---------------------------------------------------------------------------- 16 | 17 | type dirty_flag int 18 | 19 | const ( 20 | dirty_contents dirty_flag = (1 << iota) 21 | dirty_status 22 | 23 | dirty_everything = dirty_contents | dirty_status 24 | ) 25 | 26 | //---------------------------------------------------------------------------- 27 | // view location 28 | // 29 | // This structure represents a view location in the buffer. It needs to be 30 | // separated from the view, because it's also being saved by the buffer (in case 31 | // if at the moment buffer has no views attached to it). 32 | //---------------------------------------------------------------------------- 33 | 34 | type view_location struct { 35 | cursor cursor_location 36 | top_line *line 37 | top_line_num int 38 | 39 | // Various cursor offsets from the beginning of the line: 40 | // 1. in characters 41 | // 2. in visual cells 42 | // An example would be the '\t' character, which gives 1 character 43 | // offset, but 'tabstop_length' visual cells offset. 44 | cursor_coffset int 45 | cursor_voffset int 46 | 47 | // This offset is different from these three above, because it's the 48 | // amount of visual cells you need to skip, before starting to show the 49 | // contents of the cursor line. The value stays as long as the cursor is 50 | // within the same line. When cursor jumps from one line to another, the 51 | // value is recalculated. The logic behind this variable is somewhat 52 | // close to the one behind the 'top_line' variable. 53 | line_voffset int 54 | 55 | // this one is used for choosing the best location while traversing 56 | // vertically, every time 'cursor_voffset' changes due to horizontal 57 | // movement, this one must be changed as well 58 | last_cursor_voffset int 59 | } 60 | 61 | //---------------------------------------------------------------------------- 62 | // byte_range 63 | //---------------------------------------------------------------------------- 64 | 65 | type byte_range struct { 66 | begin int 67 | end int 68 | } 69 | 70 | func (r byte_range) includes(offset int) bool { 71 | return r.begin <= offset && r.end > offset 72 | } 73 | 74 | const hl_fg = termbox.ColorCyan 75 | const hl_bg = termbox.ColorBlue 76 | 77 | //---------------------------------------------------------------------------- 78 | // view tags 79 | //---------------------------------------------------------------------------- 80 | 81 | type view_tag struct { 82 | beg_line int 83 | beg_offset int 84 | end_line int 85 | end_offset int 86 | fg termbox.Attribute 87 | bg termbox.Attribute 88 | } 89 | 90 | func (t *view_tag) includes(line, offset int) bool { 91 | if line < t.beg_line || line > t.end_line { 92 | return false 93 | } 94 | if line == t.beg_line && offset < t.beg_offset { 95 | return false 96 | } 97 | if line == t.end_line && offset >= t.end_offset { 98 | return false 99 | } 100 | return true 101 | } 102 | 103 | var default_view_tag = view_tag{ 104 | fg: termbox.ColorDefault, 105 | bg: termbox.ColorDefault, 106 | } 107 | 108 | //---------------------------------------------------------------------------- 109 | // view context 110 | //---------------------------------------------------------------------------- 111 | 112 | type view_context struct { 113 | set_status func(format string, args ...interface{}) 114 | kill_buffer *[]byte 115 | buffers *[]*buffer 116 | } 117 | 118 | //---------------------------------------------------------------------------- 119 | // default autocompletion type decision function 120 | //---------------------------------------------------------------------------- 121 | 122 | func default_ac_decide(view *view) ac_func { 123 | if strings.HasSuffix(view.buf.path, ".go") { 124 | return gocode_ac 125 | } 126 | return local_ac 127 | } 128 | 129 | //---------------------------------------------------------------------------- 130 | // view 131 | // 132 | // Think of it as a window. It draws contents from a portion of a buffer into 133 | // 'uibuf' and maintains things like cursor position. 134 | //---------------------------------------------------------------------------- 135 | 136 | type view struct { 137 | view_location 138 | ctx view_context 139 | tmpbuf bytes.Buffer // temporary buffer for status bar text 140 | buf *buffer // currently displayed buffer 141 | uibuf tulib.Buffer 142 | dirty dirty_flag 143 | oneline bool 144 | ac *autocompl 145 | last_vcommand vcommand 146 | ac_decide ac_decide_func 147 | highlight_bytes []byte 148 | highlight_ranges []byte_range 149 | tags []view_tag 150 | } 151 | 152 | func new_view(ctx view_context, buf *buffer) *view { 153 | v := new(view) 154 | v.ctx = ctx 155 | v.uibuf = tulib.NewBuffer(1, 1) 156 | v.attach(buf) 157 | v.ac_decide = default_ac_decide 158 | v.highlight_ranges = make([]byte_range, 0, 10) 159 | v.tags = make([]view_tag, 0, 10) 160 | return v 161 | } 162 | 163 | func (v *view) activate() { 164 | v.last_vcommand = vcommand_none 165 | } 166 | 167 | func (v *view) deactivate() { 168 | // on deactivation discard autocompl 169 | v.ac = nil 170 | } 171 | 172 | func (v *view) attach(b *buffer) { 173 | if v.buf == b { 174 | return 175 | } 176 | 177 | v.ac = nil 178 | if v.buf != nil { 179 | v.detach() 180 | } 181 | v.buf = b 182 | v.view_location = b.loc 183 | b.add_view(v) 184 | v.dirty = dirty_everything 185 | } 186 | 187 | func (v *view) detach() { 188 | v.buf.delete_view(v) 189 | v.buf = nil 190 | } 191 | 192 | func (v *view) init_autocompl() { 193 | if v.ac_decide == nil { 194 | return 195 | } 196 | 197 | ac_func := v.ac_decide(v) 198 | if ac_func == nil { 199 | return 200 | } 201 | 202 | v.ac = new_autocompl(ac_func, v) 203 | if v.ac != nil && len(v.ac.actual_proposals()) == 1 { 204 | v.ac.finalize(v) 205 | v.ac = nil 206 | } 207 | } 208 | 209 | // Resize the 'v.uibuf', adjusting things accordingly. 210 | func (v *view) resize(w, h int) { 211 | v.uibuf.Resize(w, h) 212 | v.adjust_line_voffset() 213 | v.adjust_top_line() 214 | v.dirty = dirty_everything 215 | } 216 | 217 | func (v *view) height() int { 218 | if !v.oneline { 219 | return v.uibuf.Height - 1 220 | } 221 | return v.uibuf.Height 222 | } 223 | 224 | func (v *view) vertical_threshold() int { 225 | max_v_threshold := (v.height() - 1) / 2 226 | if view_vertical_threshold > max_v_threshold { 227 | return max_v_threshold 228 | } 229 | return view_vertical_threshold 230 | } 231 | 232 | func (v *view) horizontal_threshold() int { 233 | max_h_threshold := (v.width() - 1) / 2 234 | if view_horizontal_threshold > max_h_threshold { 235 | return max_h_threshold 236 | } 237 | return view_horizontal_threshold 238 | } 239 | 240 | func (v *view) width() int { 241 | // TODO: perhaps if I want to draw line numbers, I will hack it there 242 | return v.uibuf.Width 243 | } 244 | 245 | func (v *view) draw_line(line *line, line_num, coff, line_voffset int) { 246 | x := 0 247 | tabstop := 0 248 | bx := 0 249 | data := line.data 250 | 251 | if len(v.highlight_bytes) > 0 { 252 | v.find_highlight_ranges_for_line(data) 253 | } 254 | for { 255 | rx := x - line_voffset 256 | if len(data) == 0 { 257 | break 258 | } 259 | 260 | if x == tabstop { 261 | tabstop += tabstop_length 262 | } 263 | 264 | if rx >= v.uibuf.Width { 265 | last := coff + v.uibuf.Width - 1 266 | v.uibuf.Cells[last] = termbox.Cell{ 267 | Ch: '>', 268 | Fg: termbox.ColorDefault, 269 | Bg: termbox.ColorDefault, 270 | } 271 | break 272 | } 273 | 274 | r, rlen := utf8.DecodeRune(data) 275 | switch { 276 | case r == '\t': 277 | // fill with spaces to the next tabstop 278 | for ; x < tabstop; x++ { 279 | rx := x - line_voffset 280 | if rx >= v.uibuf.Width { 281 | break 282 | } 283 | 284 | if rx >= 0 { 285 | v.uibuf.Cells[coff+rx] = v.make_cell( 286 | line_num, bx, ' ') 287 | } 288 | } 289 | case r < 32: 290 | // invisible chars like ^R or ^@ 291 | if rx >= 0 { 292 | v.uibuf.Cells[coff+rx] = termbox.Cell{ 293 | Ch: '^', 294 | Fg: termbox.ColorRed, 295 | Bg: termbox.ColorDefault, 296 | } 297 | } 298 | x++ 299 | rx = x - line_voffset 300 | if rx >= v.uibuf.Width { 301 | break 302 | } 303 | if rx >= 0 { 304 | v.uibuf.Cells[coff+rx] = termbox.Cell{ 305 | Ch: invisible_rune_table[r], 306 | Fg: termbox.ColorRed, 307 | Bg: termbox.ColorDefault, 308 | } 309 | } 310 | x++ 311 | default: 312 | if rx >= 0 { 313 | v.uibuf.Cells[coff+rx] = v.make_cell( 314 | line_num, bx, r) 315 | } 316 | x += rune_width(r) 317 | } 318 | data = data[rlen:] 319 | bx += rlen 320 | } 321 | 322 | if line_voffset != 0 { 323 | v.uibuf.Cells[coff] = termbox.Cell{ 324 | Ch: '<', 325 | Fg: termbox.ColorDefault, 326 | Bg: termbox.ColorDefault, 327 | } 328 | } 329 | } 330 | 331 | func (v *view) draw_contents() { 332 | if len(v.highlight_bytes) == 0 { 333 | v.highlight_ranges = v.highlight_ranges[:0] 334 | } 335 | 336 | // clear the buffer 337 | v.uibuf.Fill(v.uibuf.Rect, termbox.Cell{ 338 | Ch: ' ', 339 | Fg: termbox.ColorDefault, 340 | Bg: termbox.ColorDefault, 341 | }) 342 | 343 | if v.uibuf.Width == 0 || v.uibuf.Height == 0 { 344 | return 345 | } 346 | 347 | // draw lines 348 | line := v.top_line 349 | coff := 0 350 | for y, h := 0, v.height(); y < h; y++ { 351 | if line == nil { 352 | break 353 | } 354 | 355 | if line == v.cursor.line { 356 | // special case, cursor line 357 | v.draw_line(line, v.top_line_num+y, coff, v.line_voffset) 358 | } else { 359 | v.draw_line(line, v.top_line_num+y, coff, 0) 360 | } 361 | 362 | coff += v.uibuf.Width 363 | line = line.next 364 | } 365 | } 366 | 367 | func (v *view) draw_status() { 368 | if v.oneline { 369 | return 370 | } 371 | 372 | // fill background with '-' 373 | lp := default_label_params 374 | lp.Bg = termbox.AttrReverse 375 | lp.Fg = termbox.AttrReverse | termbox.AttrBold 376 | v.uibuf.Fill(tulib.Rect{0, v.height(), v.uibuf.Width, 1}, termbox.Cell{ 377 | Fg: termbox.AttrReverse, 378 | Bg: termbox.AttrReverse, 379 | Ch: '-', 380 | }) 381 | 382 | // on disk sync status 383 | if !v.buf.synced_with_disk() { 384 | cell := termbox.Cell{ 385 | Fg: termbox.AttrReverse, 386 | Bg: termbox.AttrReverse, 387 | Ch: '*', 388 | } 389 | v.uibuf.Set(1, v.height(), cell) 390 | v.uibuf.Set(2, v.height(), cell) 391 | } 392 | 393 | // filename 394 | fmt.Fprintf(&v.tmpbuf, " %s ", v.buf.name) 395 | v.uibuf.DrawLabel(tulib.Rect{5, v.height(), v.uibuf.Width, 1}, 396 | &lp, v.tmpbuf.Bytes()) 397 | namel := v.tmpbuf.Len() 398 | lp.Fg = termbox.AttrReverse 399 | v.tmpbuf.Reset() 400 | fmt.Fprintf(&v.tmpbuf, "(%d, %d) ", v.cursor.line_num, v.cursor_voffset) 401 | v.uibuf.DrawLabel(tulib.Rect{5 + namel, v.height(), v.uibuf.Width, 1}, 402 | &lp, v.tmpbuf.Bytes()) 403 | v.tmpbuf.Reset() 404 | } 405 | 406 | // Draw the current view to the 'v.uibuf'. 407 | func (v *view) draw() { 408 | if v.dirty&dirty_contents != 0 { 409 | v.dirty &^= dirty_contents 410 | v.draw_contents() 411 | } 412 | 413 | if v.dirty&dirty_status != 0 { 414 | v.dirty &^= dirty_status 415 | v.draw_status() 416 | } 417 | } 418 | 419 | // Center view on the cursor. 420 | func (v *view) center_view_on_cursor() { 421 | v.top_line = v.cursor.line 422 | v.top_line_num = v.cursor.line_num 423 | v.move_top_line_n_times(-v.height() / 2) 424 | v.dirty = dirty_everything 425 | } 426 | 427 | func (v *view) move_cursor_to_line(n int) { 428 | v.move_cursor_beginning_of_file() 429 | v.move_cursor_line_n_times(n - 1) 430 | v.center_view_on_cursor() 431 | } 432 | 433 | // Move top line 'n' times forward or backward. 434 | func (v *view) move_top_line_n_times(n int) { 435 | if n == 0 { 436 | return 437 | } 438 | 439 | top := v.top_line 440 | for top.prev != nil && n < 0 { 441 | top = top.prev 442 | v.top_line_num-- 443 | n++ 444 | } 445 | for top.next != nil && n > 0 { 446 | top = top.next 447 | v.top_line_num++ 448 | n-- 449 | } 450 | v.top_line = top 451 | } 452 | 453 | // Move cursor line 'n' times forward or backward. 454 | func (v *view) move_cursor_line_n_times(n int) { 455 | if n == 0 { 456 | return 457 | } 458 | 459 | cursor := v.cursor.line 460 | for cursor.prev != nil && n < 0 { 461 | cursor = cursor.prev 462 | v.cursor.line_num-- 463 | n++ 464 | } 465 | for cursor.next != nil && n > 0 { 466 | cursor = cursor.next 467 | v.cursor.line_num++ 468 | n-- 469 | } 470 | v.cursor.line = cursor 471 | } 472 | 473 | // When 'top_line' was changed, call this function to possibly adjust the 474 | // 'cursor_line'. 475 | func (v *view) adjust_cursor_line() { 476 | vt := v.vertical_threshold() 477 | cursor := v.cursor.line 478 | co := v.cursor.line_num - v.top_line_num 479 | h := v.height() 480 | 481 | if cursor.next != nil && co < vt { 482 | v.move_cursor_line_n_times(vt - co) 483 | } 484 | 485 | if cursor.prev != nil && co >= h-vt { 486 | v.move_cursor_line_n_times((h - vt) - co - 1) 487 | } 488 | 489 | if cursor != v.cursor.line { 490 | cursor = v.cursor.line 491 | bo, co, vo := cursor.find_closest_offsets(v.last_cursor_voffset) 492 | v.cursor.boffset = bo 493 | v.cursor_coffset = co 494 | v.cursor_voffset = vo 495 | v.line_voffset = 0 496 | v.adjust_line_voffset() 497 | v.dirty = dirty_everything 498 | } 499 | } 500 | 501 | // When 'cursor_line' was changed, call this function to possibly adjust the 502 | // 'top_line'. 503 | func (v *view) adjust_top_line() { 504 | vt := v.vertical_threshold() 505 | top := v.top_line 506 | co := v.cursor.line_num - v.top_line_num 507 | h := v.height() 508 | 509 | if top.next != nil && co >= h-vt { 510 | v.move_top_line_n_times(co - (h - vt) + 1) 511 | v.dirty = dirty_everything 512 | } 513 | 514 | if top.prev != nil && co < vt { 515 | v.move_top_line_n_times(co - vt) 516 | v.dirty = dirty_everything 517 | } 518 | } 519 | 520 | // When 'cursor_voffset' was changed usually > 0, then call this function to 521 | // possibly adjust 'line_voffset'. 522 | func (v *view) adjust_line_voffset() { 523 | ht := v.horizontal_threshold() 524 | w := v.uibuf.Width 525 | vo := v.line_voffset 526 | cvo := v.cursor_voffset 527 | threshold := w - 1 528 | if vo != 0 { 529 | threshold = w - ht 530 | } 531 | 532 | if cvo-vo >= threshold { 533 | vo = cvo + (ht - w + 1) 534 | } 535 | 536 | if vo != 0 && cvo-vo < ht { 537 | vo = cvo - ht 538 | if vo < 0 { 539 | vo = 0 540 | } 541 | } 542 | 543 | if v.line_voffset != vo { 544 | v.line_voffset = vo 545 | v.dirty = dirty_everything 546 | } 547 | } 548 | 549 | func (v *view) cursor_position() (int, int) { 550 | y := v.cursor.line_num - v.top_line_num 551 | x := v.cursor_voffset - v.line_voffset 552 | return x, y 553 | } 554 | 555 | func (v *view) cursor_position_for(cursor cursor_location) (int, int) { 556 | y := cursor.line_num - v.top_line_num 557 | x := cursor.voffset() - v.line_voffset 558 | return x, y 559 | } 560 | 561 | // Move cursor to the 'boffset' position in the 'line'. Obviously 'line' must be 562 | // from the attached buffer. If 'boffset' < 0, use 'last_cursor_voffset'. Keep 563 | // in mind that there is no need to maintain connections between lines (e.g. for 564 | // moving from a deleted line to another line). 565 | func (v *view) move_cursor_to(c cursor_location) { 566 | v.dirty |= dirty_status 567 | if c.boffset < 0 { 568 | bo, co, vo := c.line.find_closest_offsets(v.last_cursor_voffset) 569 | v.cursor.boffset = bo 570 | v.cursor_coffset = co 571 | v.cursor_voffset = vo 572 | } else { 573 | vo, co := c.voffset_coffset() 574 | v.cursor.boffset = c.boffset 575 | v.cursor_coffset = co 576 | v.cursor_voffset = vo 577 | } 578 | 579 | if c.boffset >= 0 { 580 | v.last_cursor_voffset = v.cursor_voffset 581 | } 582 | 583 | if c.line != v.cursor.line { 584 | if v.line_voffset != 0 { 585 | v.dirty = dirty_everything 586 | } 587 | v.line_voffset = 0 588 | } 589 | v.cursor.line = c.line 590 | v.cursor.line_num = c.line_num 591 | v.adjust_line_voffset() 592 | v.adjust_top_line() 593 | 594 | if v.ac != nil { 595 | // update autocompletion on every cursor move 596 | ok := v.ac.update(v.cursor) 597 | if !ok { 598 | v.ac = nil 599 | } 600 | } 601 | } 602 | 603 | // Move cursor one character forward. 604 | func (v *view) move_cursor_forward() { 605 | c := v.cursor 606 | if c.last_line() && c.eol() { 607 | v.ctx.set_status("End of buffer") 608 | return 609 | } 610 | 611 | c.move_one_rune_forward() 612 | v.move_cursor_to(c) 613 | } 614 | 615 | // Move cursor one character backward. 616 | func (v *view) move_cursor_backward() { 617 | c := v.cursor 618 | if c.first_line() && c.bol() { 619 | v.ctx.set_status("Beginning of buffer") 620 | return 621 | } 622 | 623 | c.move_one_rune_backward() 624 | v.move_cursor_to(c) 625 | } 626 | 627 | // Move cursor to the next line. 628 | func (v *view) move_cursor_next_line() { 629 | c := v.cursor 630 | if !c.last_line() { 631 | c = cursor_location{c.line.next, c.line_num + 1, -1} 632 | v.move_cursor_to(c) 633 | } else { 634 | v.ctx.set_status("End of buffer") 635 | } 636 | } 637 | 638 | // Move cursor to the previous line. 639 | func (v *view) move_cursor_prev_line() { 640 | c := v.cursor 641 | if !c.first_line() { 642 | c = cursor_location{c.line.prev, c.line_num - 1, -1} 643 | v.move_cursor_to(c) 644 | } else { 645 | v.ctx.set_status("Beginning of buffer") 646 | } 647 | } 648 | 649 | // Move cursor to the beginning of the line. 650 | func (v *view) move_cursor_beginning_of_line() { 651 | c := v.cursor 652 | c.move_beginning_of_line() 653 | v.move_cursor_to(c) 654 | } 655 | 656 | // Move cursor to the end of the line. 657 | func (v *view) move_cursor_end_of_line() { 658 | c := v.cursor 659 | c.move_end_of_line() 660 | v.move_cursor_to(c) 661 | } 662 | 663 | // Move cursor to the beginning of the file (buffer). 664 | func (v *view) move_cursor_beginning_of_file() { 665 | c := cursor_location{v.buf.first_line, 1, 0} 666 | v.move_cursor_to(c) 667 | } 668 | 669 | // Move cursor to the end of the file (buffer). 670 | func (v *view) move_cursor_end_of_file() { 671 | c := cursor_location{v.buf.last_line, v.buf.lines_n, len(v.buf.last_line.data)} 672 | v.move_cursor_to(c) 673 | } 674 | 675 | // Move cursor to the end of the next (or current) word. 676 | func (v *view) move_cursor_word_forward() { 677 | c := v.cursor 678 | ok := c.move_one_word_forward() 679 | v.move_cursor_to(c) 680 | if !ok { 681 | v.ctx.set_status("End of buffer") 682 | } 683 | } 684 | 685 | func (v *view) move_cursor_word_backward() { 686 | c := v.cursor 687 | ok := c.move_one_word_backward() 688 | v.move_cursor_to(c) 689 | if !ok { 690 | v.ctx.set_status("Beginning of buffer") 691 | } 692 | } 693 | 694 | // Move view 'n' lines forward or backward. 695 | func (v *view) move_view_n_lines(n int) { 696 | prevtop := v.top_line_num 697 | v.move_top_line_n_times(n) 698 | if prevtop != v.top_line_num { 699 | v.adjust_cursor_line() 700 | v.dirty = dirty_everything 701 | } 702 | } 703 | 704 | // Check if it's possible to move view 'n' lines forward or backward. 705 | func (v *view) can_move_top_line_n_times(n int) bool { 706 | if n == 0 { 707 | return true 708 | } 709 | 710 | top := v.top_line 711 | for top.prev != nil && n < 0 { 712 | top = top.prev 713 | n++ 714 | } 715 | for top.next != nil && n > 0 { 716 | top = top.next 717 | n-- 718 | } 719 | 720 | if n != 0 { 721 | return false 722 | } 723 | return true 724 | } 725 | 726 | // Move view 'n' lines forward or backward only if it's possible. 727 | func (v *view) maybe_move_view_n_lines(n int) { 728 | if v.can_move_top_line_n_times(n) { 729 | v.move_view_n_lines(n) 730 | } 731 | } 732 | 733 | func (v *view) maybe_next_action_group() { 734 | b := v.buf 735 | if b.history.next == nil { 736 | // no need to move 737 | return 738 | } 739 | 740 | prev := b.history 741 | b.history = b.history.next 742 | b.history.prev = prev 743 | b.history.next = nil 744 | b.history.actions = nil 745 | b.history.before = v.cursor 746 | } 747 | 748 | func (v *view) finalize_action_group() { 749 | b := v.buf 750 | // finalize only if we're at the tip of the undo history, this function 751 | // will be called mainly after each cursor movement and actions alike 752 | // (that are supposed to finalize action group) 753 | if b.history.next == nil { 754 | b.history.next = new(action_group) 755 | b.history.after = v.cursor 756 | } 757 | } 758 | 759 | func (v *view) undo() { 760 | b := v.buf 761 | if b.history.prev == nil { 762 | // we're at the sentinel, no more things to undo 763 | v.ctx.set_status("No further undo information") 764 | return 765 | } 766 | 767 | // undo action causes finalization, always 768 | v.finalize_action_group() 769 | 770 | // undo invariant tells us 'len(b.history.actions) != 0' in case if this is 771 | // not a sentinel, revert the actions in the current action group 772 | for i := len(b.history.actions) - 1; i >= 0; i-- { 773 | a := &b.history.actions[i] 774 | a.revert(v) 775 | } 776 | v.move_cursor_to(b.history.before) 777 | v.last_cursor_voffset = v.cursor_voffset 778 | b.history = b.history.prev 779 | v.ctx.set_status("Undo!") 780 | } 781 | 782 | func (v *view) redo() { 783 | b := v.buf 784 | if b.history.next == nil { 785 | // open group, obviously, can't move forward 786 | v.ctx.set_status("No further redo information") 787 | return 788 | } 789 | if len(b.history.next.actions) == 0 { 790 | // last finalized group, moving to the next group breaks the 791 | // invariant and doesn't make sense (nothing to redo) 792 | v.ctx.set_status("No further redo information") 793 | return 794 | } 795 | 796 | // move one entry forward, and redo all its actions 797 | b.history = b.history.next 798 | for i := range b.history.actions { 799 | a := &b.history.actions[i] 800 | a.apply(v) 801 | } 802 | v.move_cursor_to(b.history.after) 803 | v.last_cursor_voffset = v.cursor_voffset 804 | v.ctx.set_status("Redo!") 805 | } 806 | 807 | func (v *view) action_insert(c cursor_location, data []byte) { 808 | if v.oneline { 809 | data = bytes.Replace(data, []byte{'\n'}, nil, -1) 810 | } 811 | 812 | v.maybe_next_action_group() 813 | a := action{ 814 | what: action_insert, 815 | data: data, 816 | cursor: c, 817 | lines: make([]*line, bytes.Count(data, []byte{'\n'})), 818 | } 819 | for i := range a.lines { 820 | a.lines[i] = new(line) 821 | } 822 | a.apply(v) 823 | v.buf.history.append(&a) 824 | } 825 | 826 | func (v *view) action_delete(c cursor_location, nbytes int) { 827 | v.maybe_next_action_group() 828 | d := c.extract_bytes(nbytes) 829 | a := action{ 830 | what: action_delete, 831 | data: d, 832 | cursor: c, 833 | lines: make([]*line, bytes.Count(d, []byte{'\n'})), 834 | } 835 | for i := range a.lines { 836 | a.lines[i] = c.line.next 837 | c.line = c.line.next 838 | } 839 | a.apply(v) 840 | v.buf.history.append(&a) 841 | } 842 | 843 | // Insert a rune 'r' at the current cursor position, advance cursor one character forward. 844 | func (v *view) insert_rune(r rune) { 845 | var data [utf8.UTFMax]byte 846 | l := utf8.EncodeRune(data[:], r) 847 | c := v.cursor 848 | if r == '\n' || r == '\r' { 849 | v.action_insert(c, []byte{'\n'}) 850 | prev := c.line 851 | c.line = c.line.next 852 | c.line_num++ 853 | c.boffset = 0 854 | 855 | if r == '\n' { 856 | i := index_first_non_space(prev.data) 857 | if i > 0 { 858 | autoindent := clone_byte_slice(prev.data[:i]) 859 | v.action_insert(c, autoindent) 860 | c.boffset += len(autoindent) 861 | } 862 | } 863 | } else { 864 | v.action_insert(c, data[:l]) 865 | c.boffset += l 866 | } 867 | v.move_cursor_to(c) 868 | v.dirty = dirty_everything 869 | } 870 | 871 | // If at the beginning of the line, move contents of the current line to the end 872 | // of the previous line. Otherwise, erase one character backward. 873 | func (v *view) delete_rune_backward() { 874 | c := v.cursor 875 | if c.bol() { 876 | if c.first_line() { 877 | // beginning of the file 878 | v.ctx.set_status("Beginning of buffer") 879 | return 880 | } 881 | c.line = c.line.prev 882 | c.line_num-- 883 | c.boffset = len(c.line.data) 884 | v.action_delete(c, 1) 885 | v.move_cursor_to(c) 886 | v.dirty = dirty_everything 887 | return 888 | } 889 | 890 | _, rlen := c.rune_before() 891 | c.boffset -= rlen 892 | v.action_delete(c, rlen) 893 | v.move_cursor_to(c) 894 | v.dirty = dirty_everything 895 | } 896 | 897 | // If at the EOL, move contents of the next line to the end of the current line, 898 | // erasing the next line after that. Otherwise, delete one character under the 899 | // cursor. 900 | func (v *view) delete_rune() { 901 | c := v.cursor 902 | if c.eol() { 903 | if c.last_line() { 904 | // end of the file 905 | v.ctx.set_status("End of buffer") 906 | return 907 | } 908 | v.action_delete(c, 1) 909 | v.dirty = dirty_everything 910 | return 911 | } 912 | 913 | _, rlen := c.rune_under() 914 | v.action_delete(c, rlen) 915 | v.dirty = dirty_everything 916 | } 917 | 918 | // If not at the EOL, remove contents of the current line from the cursor to the 919 | // end. Otherwise behave like 'delete'. 920 | func (v *view) kill_line() { 921 | c := v.cursor 922 | if !c.eol() { 923 | // kill data from the cursor to the EOL 924 | len := len(c.line.data) - c.boffset 925 | v.append_to_kill_buffer(c, len) 926 | v.action_delete(c, len) 927 | v.dirty = dirty_everything 928 | return 929 | } 930 | v.append_to_kill_buffer(c, 1) 931 | v.delete_rune() 932 | } 933 | 934 | func (v *view) kill_word() { 935 | c1 := v.cursor 936 | c2 := c1 937 | c2.move_one_word_forward() 938 | d := c1.distance(c2) 939 | if d > 0 { 940 | v.append_to_kill_buffer(c1, d) 941 | v.action_delete(c1, d) 942 | } 943 | } 944 | 945 | func (v *view) kill_word_backward() { 946 | c2 := v.cursor 947 | c1 := c2 948 | c1.move_one_word_backward() 949 | d := c1.distance(c2) 950 | if d > 0 { 951 | v.prepend_to_kill_buffer(c1, d) 952 | v.action_delete(c1, d) 953 | v.move_cursor_to(c1) 954 | } 955 | } 956 | 957 | func (v *view) kill_region() { 958 | if !v.buf.is_mark_set() { 959 | v.ctx.set_status("The mark is not set now, so there is no region") 960 | return 961 | } 962 | 963 | c1 := v.cursor 964 | c2 := v.buf.mark 965 | d := c1.distance(c2) 966 | switch { 967 | case d == 0: 968 | return 969 | case d < 0: 970 | d = -d 971 | v.append_to_kill_buffer(c2, d) 972 | v.action_delete(c2, d) 973 | v.move_cursor_to(c2) 974 | default: 975 | v.append_to_kill_buffer(c1, d) 976 | v.action_delete(c1, d) 977 | } 978 | } 979 | 980 | func (v *view) set_mark() { 981 | v.buf.mark = v.cursor 982 | v.ctx.set_status("Mark set") 983 | } 984 | 985 | func (v *view) swap_cursor_and_mark() { 986 | if v.buf.is_mark_set() { 987 | m := v.buf.mark 988 | v.buf.mark = v.cursor 989 | v.move_cursor_to(m) 990 | } 991 | } 992 | 993 | func (v *view) on_insert_adjust_top_line(a *action) { 994 | if a.cursor.line_num < v.top_line_num && len(a.lines) > 0 { 995 | // inserted one or more lines above the view 996 | v.top_line_num += len(a.lines) 997 | v.dirty |= dirty_status 998 | } 999 | } 1000 | 1001 | func (v *view) on_delete_adjust_top_line(a *action) { 1002 | if a.cursor.line_num < v.top_line_num { 1003 | // deletion above the top line 1004 | if len(a.lines) == 0 { 1005 | return 1006 | } 1007 | 1008 | topnum := v.top_line_num 1009 | first, last := a.deleted_lines() 1010 | if first <= topnum && topnum <= last { 1011 | // deleted the top line, adjust the pointers 1012 | if a.cursor.line.next != nil { 1013 | v.top_line = a.cursor.line.next 1014 | v.top_line_num = a.cursor.line_num + 1 1015 | } else { 1016 | v.top_line = a.cursor.line 1017 | v.top_line_num = a.cursor.line_num 1018 | } 1019 | v.dirty = dirty_everything 1020 | } else { 1021 | // no need to worry 1022 | v.top_line_num -= len(a.lines) 1023 | v.dirty |= dirty_status 1024 | } 1025 | } 1026 | } 1027 | 1028 | func (v *view) on_insert(a *action) { 1029 | v.on_insert_adjust_top_line(a) 1030 | if v.top_line_num+v.height() <= a.cursor.line_num { 1031 | // inserted something below the view, don't care 1032 | return 1033 | } 1034 | if a.cursor.line_num < v.top_line_num { 1035 | // inserted something above the top line 1036 | if len(a.lines) > 0 { 1037 | // inserted one or more lines, adjust line numbers 1038 | v.cursor.line_num += len(a.lines) 1039 | v.dirty |= dirty_status 1040 | } 1041 | return 1042 | } 1043 | c := v.cursor 1044 | c.on_insert_adjust(a) 1045 | v.move_cursor_to(c) 1046 | v.last_cursor_voffset = v.cursor_voffset 1047 | v.dirty = dirty_everything 1048 | } 1049 | 1050 | func (v *view) on_delete(a *action) { 1051 | v.on_delete_adjust_top_line(a) 1052 | if v.top_line_num+v.height() <= a.cursor.line_num { 1053 | // deleted something below the view, don't care 1054 | return 1055 | } 1056 | if a.cursor.line_num < v.top_line_num { 1057 | // deletion above the top line 1058 | if len(a.lines) == 0 { 1059 | return 1060 | } 1061 | 1062 | _, last := a.deleted_lines() 1063 | if last < v.top_line_num { 1064 | // no need to worry 1065 | v.cursor.line_num -= len(a.lines) 1066 | v.dirty |= dirty_status 1067 | return 1068 | } 1069 | } 1070 | c := v.cursor 1071 | c.on_delete_adjust(a) 1072 | v.move_cursor_to(c) 1073 | v.last_cursor_voffset = v.cursor_voffset 1074 | v.dirty = dirty_everything 1075 | } 1076 | 1077 | func (v *view) on_vcommand(cmd vcommand, arg rune) { 1078 | last_class := v.last_vcommand.class() 1079 | if cmd.class() != last_class || last_class == vcommand_class_misc { 1080 | v.finalize_action_group() 1081 | } 1082 | 1083 | switch cmd { 1084 | case vcommand_move_cursor_forward: 1085 | v.move_cursor_forward() 1086 | case vcommand_move_cursor_backward: 1087 | v.move_cursor_backward() 1088 | case vcommand_move_cursor_word_forward: 1089 | v.move_cursor_word_forward() 1090 | case vcommand_move_cursor_word_backward: 1091 | v.move_cursor_word_backward() 1092 | case vcommand_move_cursor_next_line: 1093 | v.move_cursor_next_line() 1094 | case vcommand_move_cursor_prev_line: 1095 | v.move_cursor_prev_line() 1096 | case vcommand_move_cursor_beginning_of_line: 1097 | v.move_cursor_beginning_of_line() 1098 | case vcommand_move_cursor_end_of_line: 1099 | v.move_cursor_end_of_line() 1100 | case vcommand_move_cursor_beginning_of_file: 1101 | v.move_cursor_beginning_of_file() 1102 | case vcommand_move_cursor_end_of_file: 1103 | v.move_cursor_end_of_file() 1104 | case vcommand_move_cursor_to_line: 1105 | v.move_cursor_to_line(int(arg)) 1106 | case vcommand_move_view_half_forward: 1107 | v.maybe_move_view_n_lines(v.height() / 2) 1108 | case vcommand_move_view_half_backward: 1109 | v.move_view_n_lines(-v.height() / 2) 1110 | case vcommand_set_mark: 1111 | v.set_mark() 1112 | case vcommand_swap_cursor_and_mark: 1113 | v.swap_cursor_and_mark() 1114 | case vcommand_recenter: 1115 | v.center_view_on_cursor() 1116 | case vcommand_insert_rune: 1117 | v.insert_rune(arg) 1118 | case vcommand_yank: 1119 | v.yank() 1120 | case vcommand_delete_rune_backward: 1121 | v.delete_rune_backward() 1122 | case vcommand_delete_rune: 1123 | v.delete_rune() 1124 | case vcommand_kill_line: 1125 | v.kill_line() 1126 | case vcommand_kill_word: 1127 | v.kill_word() 1128 | case vcommand_kill_word_backward: 1129 | v.kill_word_backward() 1130 | case vcommand_kill_region: 1131 | v.kill_region() 1132 | case vcommand_copy_region: 1133 | v.copy_region() 1134 | case vcommand_undo: 1135 | v.undo() 1136 | case vcommand_redo: 1137 | v.redo() 1138 | case vcommand_autocompl_init: 1139 | v.init_autocompl() 1140 | case vcommand_autocompl_finalize: 1141 | v.ac.finalize(v) 1142 | v.ac = nil 1143 | case vcommand_autocompl_move_cursor_up: 1144 | v.ac.move_cursor_up() 1145 | case vcommand_autocompl_move_cursor_down: 1146 | v.ac.move_cursor_down() 1147 | case vcommand_indent_region: 1148 | v.indent_region() 1149 | case vcommand_deindent_region: 1150 | v.deindent_region() 1151 | case vcommand_region_to_upper: 1152 | v.region_to(bytes.ToUpper) 1153 | case vcommand_region_to_lower: 1154 | v.region_to(bytes.ToLower) 1155 | case vcommand_word_to_upper: 1156 | v.word_to(bytes.ToUpper) 1157 | case vcommand_word_to_title: 1158 | v.word_to(func(s []byte) []byte { 1159 | return bytes.Title(bytes.ToLower(s)) 1160 | }) 1161 | case vcommand_word_to_lower: 1162 | v.word_to(bytes.ToLower) 1163 | } 1164 | 1165 | v.last_vcommand = cmd 1166 | } 1167 | 1168 | func (v *view) on_key(ev *termbox.Event) { 1169 | switch ev.Key { 1170 | case termbox.KeyCtrlF, termbox.KeyArrowRight: 1171 | v.on_vcommand(vcommand_move_cursor_forward, 0) 1172 | case termbox.KeyCtrlB, termbox.KeyArrowLeft: 1173 | v.on_vcommand(vcommand_move_cursor_backward, 0) 1174 | case termbox.KeyCtrlN, termbox.KeyArrowDown: 1175 | if v.ac != nil { 1176 | v.on_vcommand(vcommand_autocompl_move_cursor_down, 0) 1177 | break 1178 | } 1179 | v.on_vcommand(vcommand_move_cursor_next_line, 0) 1180 | case termbox.KeyCtrlP, termbox.KeyArrowUp: 1181 | if v.ac != nil { 1182 | v.on_vcommand(vcommand_autocompl_move_cursor_up, 0) 1183 | break 1184 | } 1185 | v.on_vcommand(vcommand_move_cursor_prev_line, 0) 1186 | case termbox.KeyCtrlE, termbox.KeyEnd: 1187 | v.on_vcommand(vcommand_move_cursor_end_of_line, 0) 1188 | case termbox.KeyCtrlA, termbox.KeyHome: 1189 | v.on_vcommand(vcommand_move_cursor_beginning_of_line, 0) 1190 | case termbox.KeyCtrlV, termbox.KeyPgdn: 1191 | v.on_vcommand(vcommand_move_view_half_forward, 0) 1192 | case termbox.KeyCtrlL: 1193 | v.on_vcommand(vcommand_recenter, 0) 1194 | case termbox.KeyCtrlSlash: 1195 | v.on_vcommand(vcommand_undo, 0) 1196 | case termbox.KeySpace: 1197 | v.on_vcommand(vcommand_insert_rune, ' ') 1198 | case termbox.KeyEnter, termbox.KeyCtrlJ: 1199 | c := '\n' 1200 | if ev.Key == termbox.KeyEnter { 1201 | // we use '\r' for , because it doesn't cause 1202 | // autoindent 1203 | c = '\r' 1204 | } 1205 | if v.ac != nil { 1206 | v.on_vcommand(vcommand_autocompl_finalize, 0) 1207 | } else { 1208 | v.on_vcommand(vcommand_insert_rune, c) 1209 | } 1210 | case termbox.KeyBackspace, termbox.KeyBackspace2: 1211 | if ev.Mod&termbox.ModAlt != 0 { 1212 | v.on_vcommand(vcommand_kill_word_backward, 0) 1213 | } else { 1214 | v.on_vcommand(vcommand_delete_rune_backward, 0) 1215 | } 1216 | case termbox.KeyDelete, termbox.KeyCtrlD: 1217 | v.on_vcommand(vcommand_delete_rune, 0) 1218 | case termbox.KeyCtrlK: 1219 | v.on_vcommand(vcommand_kill_line, 0) 1220 | case termbox.KeyPgup: 1221 | v.on_vcommand(vcommand_move_view_half_backward, 0) 1222 | case termbox.KeyTab: 1223 | v.on_vcommand(vcommand_insert_rune, '\t') 1224 | case termbox.KeyCtrlSpace: 1225 | if ev.Ch == 0 { 1226 | v.set_mark() 1227 | } 1228 | case termbox.KeyCtrlW: 1229 | v.on_vcommand(vcommand_kill_region, 0) 1230 | case termbox.KeyCtrlY: 1231 | v.on_vcommand(vcommand_yank, 0) 1232 | } 1233 | 1234 | if ev.Mod&termbox.ModAlt != 0 { 1235 | switch ev.Ch { 1236 | case 'v': 1237 | v.on_vcommand(vcommand_move_view_half_backward, 0) 1238 | case '<': 1239 | v.on_vcommand(vcommand_move_cursor_beginning_of_file, 0) 1240 | case '>': 1241 | v.on_vcommand(vcommand_move_cursor_end_of_file, 0) 1242 | case 'f': 1243 | v.on_vcommand(vcommand_move_cursor_word_forward, 0) 1244 | case 'b': 1245 | v.on_vcommand(vcommand_move_cursor_word_backward, 0) 1246 | case 'd': 1247 | v.on_vcommand(vcommand_kill_word, 0) 1248 | case 'w': 1249 | v.on_vcommand(vcommand_copy_region, 0) 1250 | case 'u': 1251 | v.on_vcommand(vcommand_word_to_upper, 0) 1252 | case 'l': 1253 | v.on_vcommand(vcommand_word_to_lower, 0) 1254 | case 'c': 1255 | v.on_vcommand(vcommand_word_to_title, 0) 1256 | } 1257 | } else if ev.Ch != 0 { 1258 | v.on_vcommand(vcommand_insert_rune, ev.Ch) 1259 | } 1260 | } 1261 | 1262 | func (v *view) dump_info() { 1263 | p := func(format string, args ...interface{}) { 1264 | fmt.Fprintf(os.Stderr, format, args...) 1265 | } 1266 | 1267 | p("Top line num: %d\n", v.top_line_num) 1268 | } 1269 | 1270 | func (v *view) find_highlight_ranges_for_line(data []byte) { 1271 | v.highlight_ranges = v.highlight_ranges[:0] 1272 | offset := 0 1273 | for { 1274 | i := bytes.Index(data, v.highlight_bytes) 1275 | if i == -1 { 1276 | return 1277 | } 1278 | 1279 | v.highlight_ranges = append(v.highlight_ranges, byte_range{ 1280 | begin: offset + i, 1281 | end: offset + i + len(v.highlight_bytes), 1282 | }) 1283 | data = data[i+len(v.highlight_bytes):] 1284 | offset += i + len(v.highlight_bytes) 1285 | } 1286 | } 1287 | 1288 | func (v *view) in_one_of_highlight_ranges(offset int) bool { 1289 | for _, r := range v.highlight_ranges { 1290 | if r.includes(offset) { 1291 | return true 1292 | } 1293 | } 1294 | return false 1295 | } 1296 | 1297 | func (v *view) tag(line, offset int) *view_tag { 1298 | for i := range v.tags { 1299 | t := &v.tags[i] 1300 | if t.includes(line, offset) { 1301 | return t 1302 | } 1303 | } 1304 | return &default_view_tag 1305 | } 1306 | 1307 | func (v *view) make_cell(line, offset int, ch rune) termbox.Cell { 1308 | tag := v.tag(line, offset) 1309 | if tag != &default_view_tag { 1310 | return termbox.Cell{ 1311 | Ch: ch, 1312 | Fg: tag.fg, 1313 | Bg: tag.bg, 1314 | } 1315 | } 1316 | 1317 | cell := termbox.Cell{ 1318 | Ch: ch, 1319 | Fg: tag.fg, 1320 | Bg: tag.bg, 1321 | } 1322 | if v.in_one_of_highlight_ranges(offset) { 1323 | cell.Fg = hl_fg 1324 | cell.Bg = hl_bg 1325 | } 1326 | return cell 1327 | } 1328 | 1329 | func (v *view) cleanup_trailing_whitespaces() { 1330 | cursor := cursor_location{ 1331 | line: v.buf.first_line, 1332 | line_num: 1, 1333 | boffset: 0, 1334 | } 1335 | 1336 | for cursor.line != nil { 1337 | len := len(cursor.line.data) 1338 | i := index_last_non_space(cursor.line.data) 1339 | if i == -1 && len > 0 { 1340 | // the whole string is whitespace 1341 | v.action_delete(cursor, len) 1342 | } 1343 | if i != -1 && i != len-1 { 1344 | // some whitespace at the end 1345 | cursor.boffset = i + 1 1346 | v.action_delete(cursor, len-cursor.boffset) 1347 | } 1348 | cursor.line = cursor.line.next 1349 | cursor.line_num++ 1350 | cursor.boffset = 0 1351 | } 1352 | 1353 | // adjust cursor after changes possibly 1354 | cursor = v.cursor 1355 | if cursor.boffset > len(cursor.line.data) { 1356 | cursor.boffset = len(cursor.line.data) 1357 | v.move_cursor_to(cursor) 1358 | } 1359 | } 1360 | 1361 | func (v *view) cleanup_trailing_newlines() { 1362 | cursor := cursor_location{ 1363 | line: v.buf.last_line, 1364 | line_num: v.buf.lines_n, 1365 | boffset: 0, 1366 | } 1367 | 1368 | for len(cursor.line.data) == 0 { 1369 | prev := cursor.line.prev 1370 | if prev == nil { 1371 | // beginning of the file, stop 1372 | break 1373 | } 1374 | 1375 | if len(prev.data) > 0 { 1376 | // previous line is not empty, leave one empty line at 1377 | // the end (trailing EOL) 1378 | break 1379 | } 1380 | 1381 | // adjust view cursor just in case 1382 | if v.cursor.line_num == cursor.line_num { 1383 | v.move_cursor_prev_line() 1384 | } 1385 | 1386 | cursor.line = prev 1387 | cursor.line_num-- 1388 | cursor.boffset = 0 1389 | v.action_delete(cursor, 1) 1390 | } 1391 | } 1392 | 1393 | func (v *view) ensure_trailing_eol() { 1394 | cursor := cursor_location{ 1395 | line: v.buf.last_line, 1396 | line_num: v.buf.lines_n, 1397 | boffset: len(v.buf.last_line.data), 1398 | } 1399 | if len(v.buf.last_line.data) > 0 { 1400 | v.action_insert(cursor, []byte{'\n'}) 1401 | } 1402 | } 1403 | 1404 | func (v *view) presave_cleanup(raw bool) { 1405 | v.finalize_action_group() 1406 | v.last_vcommand = vcommand_none 1407 | if !raw { 1408 | v.cleanup_trailing_whitespaces() 1409 | v.cleanup_trailing_newlines() 1410 | v.ensure_trailing_eol() 1411 | v.finalize_action_group() 1412 | } 1413 | } 1414 | 1415 | func (v *view) append_to_kill_buffer(cursor cursor_location, nbytes int) { 1416 | kb := *v.ctx.kill_buffer 1417 | 1418 | switch v.last_vcommand { 1419 | case vcommand_kill_word, vcommand_kill_word_backward, vcommand_kill_region, vcommand_kill_line: 1420 | default: 1421 | kb = kb[:0] 1422 | } 1423 | 1424 | kb = append(kb, cursor.extract_bytes(nbytes)...) 1425 | *v.ctx.kill_buffer = kb 1426 | } 1427 | 1428 | func (v *view) prepend_to_kill_buffer(cursor cursor_location, nbytes int) { 1429 | kb := *v.ctx.kill_buffer 1430 | 1431 | switch v.last_vcommand { 1432 | case vcommand_kill_word, vcommand_kill_word_backward, vcommand_kill_region, vcommand_kill_line: 1433 | default: 1434 | kb = kb[:0] 1435 | } 1436 | 1437 | kb = append(cursor.extract_bytes(nbytes), kb...) 1438 | *v.ctx.kill_buffer = kb 1439 | } 1440 | 1441 | func (v *view) yank() { 1442 | buf := *v.ctx.kill_buffer 1443 | cursor := v.cursor 1444 | 1445 | if len(buf) == 0 { 1446 | return 1447 | } 1448 | cbuf := clone_byte_slice(buf) 1449 | v.action_insert(cursor, cbuf) 1450 | cursor.move_n_bytes_forward(buf) 1451 | v.move_cursor_to(cursor) 1452 | } 1453 | 1454 | // shameless copy & paste from kill_region 1455 | func (v *view) copy_region() { 1456 | if !v.buf.is_mark_set() { 1457 | v.ctx.set_status("The mark is not set now, so there is no region") 1458 | return 1459 | } 1460 | 1461 | c1 := v.cursor 1462 | c2 := v.buf.mark 1463 | d := c1.distance(c2) 1464 | switch { 1465 | case d == 0: 1466 | return 1467 | case d < 0: 1468 | d = -d 1469 | v.append_to_kill_buffer(c2, d) 1470 | default: 1471 | v.append_to_kill_buffer(c1, d) 1472 | } 1473 | } 1474 | 1475 | func (v *view) region_to(filter func([]byte) []byte) { 1476 | if !v.buf.is_mark_set() { 1477 | v.ctx.set_status("The mark is not set now, so there is no region") 1478 | return 1479 | } 1480 | v.filter_text(v.cursor, v.buf.mark, filter) 1481 | } 1482 | 1483 | func (v *view) set_tags(tags ...view_tag) { 1484 | v.tags = v.tags[:0] 1485 | if len(tags) == 0 { 1486 | return 1487 | } 1488 | if cap(v.tags) < cap(tags) { 1489 | v.tags = tags 1490 | return 1491 | } 1492 | v.tags = v.tags[:len(tags)] 1493 | copy(v.tags, tags) 1494 | } 1495 | 1496 | func (v *view) region() (beg, end cursor_location) { 1497 | beg = v.cursor 1498 | end = v.cursor 1499 | if v.buf.is_mark_set() { 1500 | end = v.buf.mark 1501 | } 1502 | beg, end = swap_cursors_maybe(beg, end) 1503 | return 1504 | } 1505 | 1506 | func (v *view) line_region() (beg, end cursor_location) { 1507 | beg = v.cursor 1508 | end = v.cursor 1509 | if v.buf.is_mark_set() { 1510 | end = v.buf.mark 1511 | } 1512 | beg, end = swap_cursors_maybe(beg, end) 1513 | beg.boffset = 0 1514 | end.boffset = len(end.line.data) 1515 | return 1516 | } 1517 | 1518 | func (v *view) indent_line(line cursor_location) { 1519 | line.boffset = 0 1520 | v.action_insert(line, []byte{'\t'}) 1521 | if v.cursor.line == line.line { 1522 | cursor := v.cursor 1523 | cursor.boffset += 1 1524 | v.move_cursor_to(cursor) 1525 | } 1526 | } 1527 | 1528 | func (v *view) deindent_line(line cursor_location) { 1529 | line.boffset = 0 1530 | if r, _ := line.rune_under(); r == '\t' { 1531 | v.action_delete(line, 1) 1532 | if v.cursor.line == line.line && v.cursor.boffset > 0 { 1533 | cursor := v.cursor 1534 | cursor.boffset -= 1 1535 | v.move_cursor_to(cursor) 1536 | } 1537 | } 1538 | } 1539 | 1540 | func (v *view) indent_region() { 1541 | beg, end := v.line_region() 1542 | for beg.line != end.line { 1543 | v.indent_line(beg) 1544 | beg.line = beg.line.next 1545 | beg.line_num++ 1546 | } 1547 | v.indent_line(end) 1548 | } 1549 | 1550 | func (v *view) deindent_region() { 1551 | beg, end := v.line_region() 1552 | for beg.line != end.line { 1553 | v.deindent_line(beg) 1554 | beg.line = beg.line.next 1555 | beg.line_num++ 1556 | } 1557 | v.deindent_line(end) 1558 | } 1559 | 1560 | func (v *view) word_to(filter func([]byte) []byte) { 1561 | c1, c2 := v.cursor, v.cursor 1562 | c2.move_one_word_forward() 1563 | v.filter_text(c1, c2, filter) 1564 | c1.move_one_word_forward() 1565 | v.move_cursor_to(c1) 1566 | } 1567 | 1568 | // Filter _must_ return a new slice and shouldn't touch contents of the 1569 | // argument, perfect filter examples are: bytes.Title, bytes.ToUpper, 1570 | // bytes.ToLower 1571 | func (v *view) filter_text(from, to cursor_location, filter func([]byte) []byte) { 1572 | c1, c2 := swap_cursors_maybe(from, to) 1573 | d := c1.distance(c2) 1574 | v.action_delete(c1, d) 1575 | data := filter(v.buf.history.last_action().data) 1576 | v.action_insert(c1, data) 1577 | if v.cursor != c1 { 1578 | c1.move_n_bytes_forward(data) 1579 | v.move_cursor_to(c1) 1580 | } 1581 | } 1582 | 1583 | func fill_region_filt(data []byte, maxv int, prefix []byte) []byte { 1584 | var buf, out bytes.Buffer 1585 | indent := data[:index_first_non_space(data)] 1586 | indent_vlen := vlen(indent, 0) 1587 | prefix_vlen := vlen(prefix, indent_vlen) 1588 | offset := 0 1589 | for { 1590 | // for each line 1591 | // 1. skip whitespace 1592 | offset += index_first_non_space(data[offset:]) 1593 | // 2. skip prefix 1594 | if bytes.HasPrefix(data[offset:], prefix) { 1595 | offset += len(prefix) 1596 | } 1597 | // 3. skip more whitespace 1598 | offset += index_first_non_space(data[offset:]) 1599 | // append line to the buffer without \n 1600 | i := bytes.Index(data[offset:], []byte("\n")) 1601 | if i == -1 { 1602 | iter_nonspace_words(data[offset:], func(word []byte) { 1603 | buf.Write(word) 1604 | buf.WriteString(" ") 1605 | }) 1606 | break 1607 | } else { 1608 | iter_nonspace_words(data[offset:offset+i], func(word []byte) { 1609 | buf.Write(word) 1610 | buf.WriteString(" ") 1611 | }) 1612 | offset += i + 1 1613 | } 1614 | } 1615 | // just in case if there were unnecessary space at the end, clean it up 1616 | if buf.Len() > 0 && buf.Bytes()[buf.Len()-1] == ' ' { 1617 | buf.Truncate(buf.Len() - 1) 1618 | } 1619 | 1620 | offset = 0 1621 | for { 1622 | data := buf.Bytes()[offset:] 1623 | out.Write(indent) 1624 | if len(prefix) > 0 { 1625 | out.Write(prefix) 1626 | out.WriteString(" ") 1627 | } 1628 | 1629 | v := indent_vlen + prefix_vlen + 1 1630 | lastspacei := -1 1631 | i := 0 1632 | for i < len(data) { 1633 | r, rlen := utf8.DecodeRune(data[i:]) 1634 | if r == ' ' { 1635 | // if the rune is a space and we still haven't found one 1636 | // or we're still before maxv, update the index of the 1637 | // last space before maxv 1638 | if lastspacei == -1 || v < maxv { 1639 | lastspacei = i 1640 | } 1641 | } 1642 | 1643 | // advance v and i 1644 | v += rune_advance_len(r, v) 1645 | i += rlen 1646 | 1647 | if lastspacei != -1 && v >= maxv { 1648 | // we've seen last space and now we're past maxv, break 1649 | break 1650 | } 1651 | } 1652 | 1653 | if i >= len(data) { 1654 | out.Write(data) 1655 | break 1656 | } else { 1657 | out.Write(data[:lastspacei]) 1658 | out.WriteString("\n") 1659 | offset += lastspacei + 1 1660 | } 1661 | } 1662 | return out.Bytes() 1663 | } 1664 | 1665 | func (v *view) fill_region(maxv int, prefix []byte) { 1666 | filt := func(data []byte) []byte { 1667 | return fill_region_filt(data, maxv, prefix) 1668 | } 1669 | beg, end := v.line_region() 1670 | v.filter_text(beg, end, filt) 1671 | } 1672 | 1673 | func (v *view) collect_words(slice [][]byte, dups *llrb_tree, ignorecase bool) [][]byte { 1674 | append_word_full := func(prefix, word []byte, clone bool) { 1675 | lword := word 1676 | lprefix := prefix 1677 | if ignorecase { 1678 | lword = bytes.ToLower(word) 1679 | lprefix = bytes.ToLower(prefix) 1680 | } 1681 | 1682 | if !bytes.HasPrefix(lword, lprefix) { 1683 | return 1684 | } 1685 | ok := dups.insert_maybe(word) 1686 | if ok { 1687 | if clone { 1688 | slice = append(slice, clone_byte_slice(word)) 1689 | } else { 1690 | slice = append(slice, word) 1691 | } 1692 | } 1693 | } 1694 | 1695 | prefix := v.cursor.word_under_cursor() 1696 | if prefix != nil { 1697 | dups.insert_maybe(prefix) 1698 | } 1699 | 1700 | append_word := func(word []byte) { 1701 | append_word_full(prefix, word, false) 1702 | } 1703 | append_word_clone := func(word []byte) { 1704 | append_word_full(prefix, word, true) 1705 | } 1706 | 1707 | line := v.cursor.line 1708 | iter_words_backward(line.data[:v.cursor.boffset], append_word_clone) 1709 | line = line.prev 1710 | for line != nil { 1711 | iter_words_backward(line.data, append_word) 1712 | line = line.prev 1713 | } 1714 | 1715 | line = v.cursor.line 1716 | iter_words(line.data[v.cursor.boffset:], append_word_clone) 1717 | line = line.next 1718 | for line != nil { 1719 | iter_words(line.data, append_word) 1720 | line = line.next 1721 | } 1722 | return slice 1723 | } 1724 | 1725 | func (v *view) search_and_replace(word, repl []byte) { 1726 | // assumes mark is set 1727 | c1, c2 := swap_cursors_maybe(v.cursor, v.buf.mark) 1728 | cur := cursor_location{ 1729 | line: c1.line, 1730 | line_num: c1.line_num, 1731 | boffset: c1.boffset, 1732 | } 1733 | for { 1734 | var end int 1735 | if cur.line == c2.line { 1736 | end = c2.boffset 1737 | } else { 1738 | end = len(cur.line.data) 1739 | } 1740 | 1741 | i := bytes.Index(cur.line.data[cur.boffset:end], word) 1742 | if i != -1 { 1743 | // match on this line, replace it 1744 | cur.boffset += i 1745 | v.action_delete(cur, len(word)) 1746 | 1747 | // It is safe to use the original 'repl' here, but be 1748 | // very careful with that, it may change. 'repl' comes 1749 | // from 'godit.s_and_r_last_repl', if someone decides 1750 | // to make it mutable, then 'repl' must be copied 1751 | // somewhere in this func. 1752 | v.action_insert(cur, repl) 1753 | 1754 | // special correction if we're on the same line as 'c2' 1755 | if cur.line == c2.line { 1756 | c2.boffset += len(repl) - len(word) 1757 | } 1758 | 1759 | if cur.line == v.cursor.line && cur.boffset < v.cursor.boffset { 1760 | c := v.cursor 1761 | c.boffset += len(repl) - len(word) 1762 | v.move_cursor_to(c) 1763 | } 1764 | 1765 | // continue with the same line 1766 | cur.boffset += len(repl) 1767 | continue 1768 | } 1769 | 1770 | // nothing on this line found, terminate or continue to the next line 1771 | if cur.line == c2.line { 1772 | break 1773 | } 1774 | 1775 | cur.line = cur.line.next 1776 | cur.line_num++ 1777 | cur.boffset = 0 1778 | } 1779 | 1780 | v.ctx.set_status("Replaced %s with %s", word, repl) 1781 | } 1782 | 1783 | func (v *view) other_buffers(cb func(buf *buffer)) { 1784 | bufs := *v.ctx.buffers 1785 | for _, buf := range bufs { 1786 | if buf == v.buf { 1787 | continue 1788 | } 1789 | cb(buf) 1790 | } 1791 | } 1792 | 1793 | //---------------------------------------------------------------------------- 1794 | // view commands 1795 | //---------------------------------------------------------------------------- 1796 | 1797 | type vcommand_class int 1798 | 1799 | const ( 1800 | vcommand_class_none vcommand_class = iota 1801 | vcommand_class_movement 1802 | vcommand_class_insertion 1803 | vcommand_class_deletion 1804 | vcommand_class_history 1805 | vcommand_class_misc 1806 | ) 1807 | 1808 | type vcommand int 1809 | 1810 | const ( 1811 | vcommand_none vcommand = iota 1812 | 1813 | // movement commands (finalize undo action group) 1814 | _vcommand_movement_beg 1815 | vcommand_move_cursor_forward 1816 | vcommand_move_cursor_backward 1817 | vcommand_move_cursor_word_forward 1818 | vcommand_move_cursor_word_backward 1819 | vcommand_move_cursor_next_line 1820 | vcommand_move_cursor_prev_line 1821 | vcommand_move_cursor_beginning_of_line 1822 | vcommand_move_cursor_end_of_line 1823 | vcommand_move_cursor_beginning_of_file 1824 | vcommand_move_cursor_end_of_file 1825 | vcommand_move_cursor_to_line 1826 | vcommand_move_view_half_forward 1827 | vcommand_move_view_half_backward 1828 | vcommand_set_mark 1829 | vcommand_swap_cursor_and_mark 1830 | vcommand_recenter 1831 | _vcommand_movement_end 1832 | 1833 | // insertion commands 1834 | _vcommand_insertion_beg 1835 | vcommand_insert_rune 1836 | vcommand_yank 1837 | _vcommand_insertion_end 1838 | 1839 | // deletion commands 1840 | _vcommand_deletion_beg 1841 | vcommand_delete_rune_backward 1842 | vcommand_delete_rune 1843 | vcommand_kill_line 1844 | vcommand_kill_word 1845 | vcommand_kill_word_backward 1846 | vcommand_kill_region 1847 | _vcommand_deletion_end 1848 | 1849 | // history commands (undo/redo) 1850 | _vcommand_history_beg 1851 | vcommand_undo 1852 | vcommand_redo 1853 | _vcommand_history_end 1854 | 1855 | // misc commands 1856 | _vcommand_misc_beg 1857 | vcommand_indent_region 1858 | vcommand_deindent_region 1859 | vcommand_copy_region 1860 | vcommand_region_to_upper 1861 | vcommand_region_to_lower 1862 | vcommand_word_to_upper 1863 | vcommand_word_to_title 1864 | vcommand_word_to_lower 1865 | vcommand_autocompl_init 1866 | vcommand_autocompl_move_cursor_up 1867 | vcommand_autocompl_move_cursor_down 1868 | vcommand_autocompl_finalize 1869 | _vcommand_misc_end 1870 | ) 1871 | 1872 | func (c vcommand) class() vcommand_class { 1873 | switch { 1874 | case c > _vcommand_movement_beg && c < _vcommand_movement_end: 1875 | return vcommand_class_movement 1876 | case c > _vcommand_insertion_beg && c < _vcommand_insertion_end: 1877 | return vcommand_class_insertion 1878 | case c > _vcommand_deletion_beg && c < _vcommand_deletion_end: 1879 | return vcommand_class_deletion 1880 | case c > _vcommand_history_beg && c < _vcommand_history_end: 1881 | return vcommand_class_history 1882 | case c > _vcommand_misc_beg && c < _vcommand_misc_end: 1883 | return vcommand_class_misc 1884 | } 1885 | return vcommand_class_none 1886 | } 1887 | --------------------------------------------------------------------------------