diff --git a/internal/action/actions.go b/internal/action/actions.go index 72081718..bacb04ad 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -560,10 +560,22 @@ func (h *BufPane) OutdentSelection() bool { // InsertTab inserts a tab or spaces func (h *BufPane) InsertTab() bool { - indent := h.Buf.IndentString(util.IntOpt(h.Buf.Settings["tabsize"])) - tabBytes := len(indent) - bytesUntilIndent := tabBytes - (h.Cursor.GetVisualX() % tabBytes) - h.Buf.Insert(h.Cursor.Loc, indent[:bytesUntilIndent]) + b := h.Buf + if b.HasSuggestions { + b.CycleAutocomplete(true) + return true + } + + l := b.LineBytes(h.Cursor.Y) + l = util.SliceStart(l, h.Cursor.X) + hasComplete := b.Autocomplete(buffer.BufferComplete) + if !hasComplete { + indent := b.IndentString(util.IntOpt(b.Settings["tabsize"])) + tabBytes := len(indent) + bytesUntilIndent := tabBytes - (h.Cursor.GetVisualX() % tabBytes) + b.Insert(h.Cursor.Loc, indent[:bytesUntilIndent]) + return true + } return true } diff --git a/internal/action/bufpane.go b/internal/action/bufpane.go index 39b6c145..669e2de4 100644 --- a/internal/action/bufpane.go +++ b/internal/action/bufpane.go @@ -227,6 +227,9 @@ func (h *BufPane) HandleEvent(event tcell.Event) { func (h *BufPane) DoKeyEvent(e Event) bool { if action, ok := BufKeyBindings[e]; ok { estr := BufKeyStrings[e] + if estr != "InsertTab" { + h.Buf.HasSuggestions = false + } for _, s := range MultiActions { if s == estr { cursors := h.Buf.GetCursors() diff --git a/internal/action/infopane.go b/internal/action/infopane.go index 1ab7c281..cc0ef609 100644 --- a/internal/action/infopane.go +++ b/internal/action/infopane.go @@ -157,12 +157,17 @@ var InfoOverrides = map[string]InfoKeyAction{ "QuitAll": (*InfoPane).QuitAll, } +// CursorUp cycles history up func (h *InfoPane) CursorUp() { h.UpHistory(h.History[h.PromptType]) } + +// CursorDown cycles history down func (h *InfoPane) CursorDown() { h.DownHistory(h.History[h.PromptType]) } + +// InsertTab begins autocompletion func (h *InfoPane) InsertTab() { b := h.Buf if b.HasSuggestions { @@ -187,22 +192,32 @@ func (h *InfoPane) InsertTab() { } } } + +// CycleBack cycles back in the autocomplete suggestion list func (h *InfoPane) CycleBack() { if h.Buf.HasSuggestions { h.Buf.CycleAutocomplete(false) } } + +// InsertNewline completes the prompt func (h *InfoPane) InsertNewline() { if !h.HasYN { h.DonePrompt(false) } } + +// Quit cancels the prompt func (h *InfoPane) Quit() { h.DonePrompt(true) } + +// QuitAll cancels the prompt func (h *InfoPane) QuitAll() { h.DonePrompt(true) } + +// Escape cancels the prompt func (h *InfoPane) Escape() { h.DonePrompt(true) } diff --git a/internal/buffer/autocomplete.go b/internal/buffer/autocomplete.go index d143f329..1e5a7fc3 100644 --- a/internal/buffer/autocomplete.go +++ b/internal/buffer/autocomplete.go @@ -16,7 +16,7 @@ import ( // cursor location // It returns a list of string suggestions which will be inserted at // the current cursor location if selected as well as a list of -// suggestion names which can be displayed in a autocomplete box or +// suggestion names which can be displayed in an autocomplete box or // other UI element type Completer func(*Buffer) ([]string, []string) @@ -24,15 +24,18 @@ func (b *Buffer) GetSuggestions() { } -func (b *Buffer) Autocomplete(c Completer) { +// Autocomplete starts the autocomplete process +func (b *Buffer) Autocomplete(c Completer) bool { b.Completions, b.Suggestions = c(b) if len(b.Completions) != len(b.Suggestions) || len(b.Completions) == 0 { - return + return false } b.CurSuggestion = -1 b.CycleAutocomplete(true) + return true } +// CycleAutocomplete moves to the next suggestion func (b *Buffer) CycleAutocomplete(forward bool) { prevSuggestion := b.CurSuggestion @@ -53,7 +56,7 @@ func (b *Buffer) CycleAutocomplete(forward bool) { if prevSuggestion < len(b.Suggestions) && prevSuggestion >= 0 { start = end.Move(-utf8.RuneCountInString(b.Completions[prevSuggestion]), b) } else { - end = start.Move(1, b) + // end = start.Move(1, b) } b.Replace(start, end, b.Completions[b.CurSuggestion]) @@ -62,6 +65,27 @@ func (b *Buffer) CycleAutocomplete(forward bool) { } } +// GetWord gets the most recent word separated by any separator +// (whitespace, punctuation, any non alphanumeric character) +func GetWord(b *Buffer) ([]byte, int) { + c := b.GetActiveCursor() + l := b.LineBytes(c.Y) + l = util.SliceStart(l, c.X) + + if c.X == 0 || util.IsWhitespace(b.RuneAt(c.Loc)) { + return []byte{}, -1 + } + + if util.IsNonAlphaNumeric(b.RuneAt(c.Loc)) { + return []byte{}, c.X + } + + args := bytes.FieldsFunc(l, util.IsNonAlphaNumeric) + input := args[len(args)-1] + return input, c.X - utf8.RuneCount(input) +} + +// GetArg gets the most recent word (separated by ' ' only) func GetArg(b *Buffer) (string, int) { c := b.GetActiveCursor() l := b.LineBytes(c.Y) @@ -128,3 +152,55 @@ func FileComplete(b *Buffer) ([]string, []string) { return completions, suggestions } + +// BufferComplete autocompletes based on previous words in the buffer +func BufferComplete(b *Buffer) ([]string, []string) { + c := b.GetActiveCursor() + input, argstart := GetWord(b) + + if argstart == -1 { + return []string{}, []string{} + } + + inputLen := utf8.RuneCount(input) + + suggestionsSet := make(map[string]struct{}) + + var suggestions []string + for i := c.Y; i >= 0; i-- { + l := b.LineBytes(i) + words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric) + for _, w := range words { + if bytes.HasPrefix(w, input) && utf8.RuneCount(w) > inputLen { + strw := string(w) + if _, ok := suggestionsSet[strw]; !ok { + suggestionsSet[strw] = struct{}{} + suggestions = append(suggestions, strw) + } + } + } + } + for i := c.Y + 1; i < b.LinesNum(); i++ { + l := b.LineBytes(i) + words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric) + for _, w := range words { + if bytes.HasPrefix(w, input) && utf8.RuneCount(w) > inputLen { + strw := string(w) + if _, ok := suggestionsSet[strw]; !ok { + suggestionsSet[strw] = struct{}{} + suggestions = append(suggestions, strw) + } + } + } + } + if len(suggestions) > 1 { + suggestions = append(suggestions, string(input)) + } + + completions := make([]string, len(suggestions)) + for i := range suggestions { + completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart) + } + + return completions, suggestions +} diff --git a/internal/display/statusline.go b/internal/display/statusline.go index beede173..6731c2c8 100644 --- a/internal/display/statusline.go +++ b/internal/display/statusline.go @@ -69,6 +69,43 @@ func (s *StatusLine) Display() { // We'll draw the line at the lowest line in the window y := s.win.Height + s.win.Y - 1 + b := s.win.Buf + if b.HasSuggestions && len(b.Suggestions) > 1 { + statusLineStyle := config.DefStyle.Reverse(true) + if style, ok := config.Colorscheme["statusline"]; ok { + statusLineStyle = style + } + keymenuOffset := 0 + if config.GetGlobalOption("keymenu").(bool) { + keymenuOffset = len(keydisplay) + } + x := 0 + for j, sug := range b.Suggestions { + style := statusLineStyle + if b.CurSuggestion == j { + style = style.Reverse(true) + } + for _, r := range sug { + screen.Screen.SetContent(x, y-keymenuOffset, r, nil, style) + x++ + if x >= s.win.Width { + return + } + } + screen.Screen.SetContent(x, y-keymenuOffset, ' ', nil, statusLineStyle) + x++ + if x >= s.win.Width { + return + } + } + + for x < s.win.Width { + screen.Screen.SetContent(x, y-keymenuOffset, ' ', nil, statusLineStyle) + x++ + } + return + } + formatter := func(match []byte) []byte { name := match[2 : len(match)-1] if bytes.HasPrefix(name, []byte("opt")) { diff --git a/internal/util/util.go b/internal/util/util.go index b7a7bb31..bdcf11ee 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" "time" + "unicode" "unicode/utf8" "github.com/blang/semver" @@ -403,3 +404,7 @@ func Clamp(val, min, max int) int { } return val } + +func IsNonAlphaNumeric(c rune) bool { + return !unicode.IsLetter(c) && !unicode.IsNumber(c) +}