package main import ( "bytes" "github.com/gdamore/tcell/termbox" "github.com/nsf/tulib" "os" "os/exec" "path/filepath" "sort" "strconv" "strings" "unicode/utf8" ) //---------------------------------------------------------------------------- // extended cursor location (includes absolute bytes offset) //---------------------------------------------------------------------------- type cursor_location_ex struct { cursor_location abs_boffset int } func make_cursor_location_ex(cursor cursor_location) cursor_location_ex { off := cursor.boffset line := cursor.line.prev for line != nil { off += len(line.data) + 1 // plus one is for '\n' line = line.prev } return cursor_location_ex{ cursor_location: cursor, abs_boffset: off, } } //---------------------------------------------------------------------------- // autocompletion //---------------------------------------------------------------------------- const ac_max_filtered = 200 const ac_ui_max_lines = 14 type ac_proposal struct { display []byte content []byte } type ( ac_func func(view *view) ([]ac_proposal, int) ac_decide_func func(view *view) ac_func ) type autocompl struct { // data origin cursor_location current cursor_location proposals []ac_proposal filtered []ac_proposal // ui cursor int view int tmpbuf bytes.Buffer } // Creates a new autocompletion object and makes a query for ac proposals, may // take a while. func new_autocompl(f ac_func, view *view) *autocompl { var charsback int ac := new(autocompl) ac.filtered = make([]ac_proposal, 0, ac_max_filtered) ac.proposals, charsback = f(view) if len(ac.proposals) == 0 { return nil } if charsback > 0 { origin := view.cursor // adjust origin if we have positive 'charsback' for charsback > 0 { view.move_cursor_backward() charsback-- } // delete region between the origin and the new cursor position view.action_delete(view.cursor, view.cursor.distance(origin)) view.finalize_action_group() } ac.origin = view.cursor ac.current = view.cursor // insert the common part of all the autocompletion proposals common := ac.common() if len(common) > 0 { c := view.cursor view.action_insert(c, common) c.boffset += len(common) view.move_cursor_to(c) view.finalize_action_group() ac.update(view.cursor) } return ac } func (ac *autocompl) common() []byte { common := ac.proposals[0].content common_n := len(common) for _, p := range ac.proposals { if len(p.content) < common_n { common_n = len(p.content) } for i := 0; i < common_n; i++ { if common[i] != p.content[i] { common_n = i break } } } return clone_byte_slice(common[:common_n]) } func (ac *autocompl) actual_proposals() []ac_proposal { if ac.origin.boffset != ac.current.boffset { return ac.filtered } return ac.proposals } // Returns 'true' if update was successful, 'false' if autocompletion should be // discarded. func (ac *autocompl) update(current cursor_location) bool { if ac.origin.line_num != current.line_num { return false } if ac.origin.boffset > current.boffset { return false } if ac.current.boffset == current.boffset { // false update, skip it return true } ac.current = current if ac.current.boffset == ac.origin.boffset { // simply discard filtered stuff return true } ac.filtered = ac.filtered[:0] filter := bytes_between(ac.origin, ac.current) j := 0 for i := 0; i < ac_max_filtered; i++ { if j >= len(ac.proposals) { break } if bytes.HasPrefix(ac.proposals[j].content, filter) { ac.filtered = append(ac.filtered, ac.proposals[j]) } else { i-- } j++ } if len(ac.filtered) == 0 { // no filtered stuff, cancel autocompletion return false } return true } func (ac *autocompl) move_cursor_down() { if ac.cursor >= len(ac.actual_proposals())-1 { return } ac.cursor++ } func (ac *autocompl) move_cursor_up() { if ac.cursor <= 0 { return } ac.cursor-- } func (ac *autocompl) tab(view *view) { // finalize first, to capture chosen directory/file. ac.finalize(view) // then init again, to recurse into the chosen directory/file. view.init_autocompl() } func (ac *autocompl) desired_height() int { proposals := ac.actual_proposals() minh := 0 for i := 0; i < ac_ui_max_lines; i++ { n := ac.view + i if n >= len(proposals) { break } minh++ } return minh } func (ac *autocompl) desired_width(height int) int { proposals := ac.actual_proposals() minw := 0 for i := 0; i < height; i++ { n := ac.view + i line_len := utf8.RuneCount(proposals[n].display) if line_len > minw { minw = line_len } } return minw + 2 } func (ac *autocompl) adjust_view(height int) { if ac.cursor < ac.view { ac.view = ac.cursor } if ac.cursor >= ac.view+height { ac.view = ac.cursor - height + 1 } } func (ac *autocompl) validate_cursor() { if ac.cursor >= len(ac.actual_proposals()) { ac.cursor = 0 ac.view = 0 } } // -1 if no need to make a slider func (ac *autocompl) slider_pos_and_rune(height int) (int, rune) { proposals := ac.actual_proposals() if len(proposals) == height { return -1, 0 } max := len(proposals) - height if ac.view == max { return height - 1, '▄' } var r rune progress := int((float32(ac.view) / float32(max)) * float32(height*2)) if progress&1 != 0 { r = '▄' } else { r = '▀' } return progress / 2, r } func (ac *autocompl) draw_onto(buf *tulib.Buffer, x, y int) { ac.validate_cursor() h := ac.desired_height() dst := find_place_for_rect(buf.Rect, tulib.Rect{x, y + 1, 1, h}) ac.adjust_view(dst.Height) w := ac.desired_width(dst.Height) dst = find_place_for_rect(buf.Rect, tulib.Rect{x, y + 1, w, h}) slider_i, slider_r := ac.slider_pos_and_rune(dst.Height) lp := default_label_params r := dst r.Width-- r.Height = 1 for i := 0; i < dst.Height; i++ { lp.Fg = termbox.ColorBlack lp.Bg = termbox.ColorWhite n := ac.view + i if n == ac.cursor { lp.Fg = termbox.ColorWhite lp.Bg = termbox.ColorBlue } buf.Fill(r, termbox.Cell{ Fg: lp.Fg, Bg: lp.Bg, Ch: ' ', }) buf.DrawLabel(r, &lp, ac.actual_proposals()[n].display) sr := ' ' if i == slider_i { sr = slider_r } buf.Set(r.X+r.Width, r.Y, termbox.Cell{ Fg: termbox.ColorWhite, Bg: termbox.ColorBlue, Ch: sr, }) r.Y++ } } func (ac *autocompl) finalize(view *view) { d := ac.origin.distance(ac.current) if d < 0 { panic("something went really wrong, oops..") } data := clone_byte_slice(ac.actual_proposals()[ac.cursor].content[d:]) view.action_insert(ac.current, data) ac.current.boffset += len(data) // ac.current is a cursor location. view.move_cursor_to(ac.current) } //---------------------------------------------------------------------------- // local buffer autocompletion //---------------------------------------------------------------------------- func local_ac(view *view) ([]ac_proposal, int) { var dups llrb_tree var others llrb_tree proposals := make([]ac_proposal, 0, 100) prefix := view.cursor.word_under_cursor() // update word caches view.other_buffers(func(buf *buffer) { buf.update_words_cache() }) collect := func(ignorecase bool) { words := view.collect_words([][]byte(nil), &dups, ignorecase) for _, word := range words { proposals = append(proposals, ac_proposal{ display: word, content: word, }) } lprefix := prefix if ignorecase { lprefix = bytes.ToLower(prefix) } view.other_buffers(func(buf *buffer) { buf.words_cache.walk(func(word []byte) { lword := word if ignorecase { lword = bytes.ToLower(word) } if bytes.HasPrefix(lword, lprefix) { ok := dups.insert_maybe(word) if !ok { return } others.insert_maybe(word) } }) }) others.walk(func(word []byte) { proposals = append(proposals, ac_proposal{ display: word, content: word, }) }) others.clear() } collect(false) if len(proposals) == 0 { collect(true) } if prefix != nil { return proposals, utf8.RuneCount(prefix) } return proposals, 0 } //---------------------------------------------------------------------------- // gocode autocompletion //---------------------------------------------------------------------------- func gocode_ac(view *view) ([]ac_proposal, int) { cursor_ex := make_cursor_location_ex(view.cursor) var out bytes.Buffer gocode := exec.Command("gocode", "-f=godit", "autocomplete", view.buf.path, strconv.Itoa(cursor_ex.abs_boffset)) gocode.Stdin = view.buf.reader() gocode.Stdout = &out err := gocode.Run() if err != nil { return nil, 0 } lr := new_line_reader(out.Bytes()) charsback_str, proposals_n_str := split_double_csv(lr.read_line()) charsback, err := atoi(charsback_str) if err != nil { return nil, 0 } proposals_n, err := atoi(proposals_n_str) if err != nil { return nil, 0 } proposals := make([]ac_proposal, proposals_n) for i := 0; i < proposals_n; i++ { d, c := split_double_csv(lr.read_line()) proposals[i].display = d proposals[i].content = c } return proposals, charsback } //---------------------------------------------------------------------------- // buffer autocompletion //---------------------------------------------------------------------------- func make_gemacs_buffer_ac_decide(gemacs *gemacs) ac_decide_func { return func(view *view) ac_func { return make_gemacs_buffer_ac(gemacs) } } func make_gemacs_buffer_ac(gemacs *gemacs) ac_func { return func(view *view) ([]ac_proposal, int) { prefix := string(view.buf.contents()[:view.cursor.boffset]) proposals := make([]ac_proposal, 0, 20) for _, buf := range gemacs.buffers { if strings.HasPrefix(buf.name, prefix) { display := make([]byte, len(buf.name), len(buf.name)+5) content := display copy(display, buf.name) if !buf.synced_with_disk() { display = display[:len(display)+5] copy(display[len(content):], " (**)") } proposals = append(proposals, ac_proposal{ display: display, content: content, }) } } return proposals, view.cursor_coffset } } //---------------------------------------------------------------------------- // file system autocompletion //---------------------------------------------------------------------------- func filesystem_line_ac_decide(view *view) ac_func { return filesystem_line_ac } type filesystem_slice []os.FileInfo func (s filesystem_slice) Len() int { return len(s) } func (s filesystem_slice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s filesystem_slice) Less(i, j int) bool { idir := s[i].IsDir() jdir := s[j].IsDir() if idir != jdir { if idir { return true } return false } return s[i].Name() < s[j].Name() } func filesystem_line_ac(view *view) ([]ac_proposal, int) { var dirfd *os.File var err error path := string(view.buf.contents()[:view.cursor.boffset]) path = substitute_home(path) path = substitute_symlinks(path) dir, partfile := filepath.Split(path) if dir == "" { dirfd, err = os.Open(".") } else { dirfd, err = os.Open(dir) } if err != nil { return nil, 0 } fis, err := readdir_stat(dir, dirfd) if err != nil { // can we recover something from here? return nil, 0 } sort.Sort(filesystem_slice(fis)) proposals := make([]ac_proposal, 0, 20) match_files := func(ignorecase bool) { if ignorecase { partfile = strings.ToLower(partfile) } for _, fi := range fis { name := fi.Name() if is_file_hidden(name) { continue } tmpname := name if ignorecase { tmpname = strings.ToLower(tmpname) } if strings.HasPrefix(tmpname, partfile) { suffix := "" if fi.IsDir() { suffix = string(filepath.Separator) } proposals = append(proposals, ac_proposal{ display: []byte(dir + name + suffix), content: []byte(dir + name + suffix), }) } } } match_files(false) if len(proposals) == 0 { match_files(true) } return proposals, view.cursor_coffset }