From a700b64a2811d3a99c3aa054df9538bed4b00e4b Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Wed, 16 Jul 2025 22:11:12 -0300 Subject: [PATCH] fix: hide completions tui when no results (#206) * fix: hide completions tui when no results * fix: gitignore * Revert "fix(tui): completions should not close on no results (#198)" This reverts commit 833eede1c10e1dcfacfcc6c6e529d3d8b7e7f838. * fix: completions Signed-off-by: Carlos Alexandro Becker * fix: accept * fix: improvements * chore(deps): update bubbles Signed-off-by: Carlos Alexandro Becker * fix: improvements * fix: accept --------- Signed-off-by: Carlos Alexandro Becker --- .gitignore | 1 + go.mod | 2 +- go.sum | 4 +- internal/tui/components/chat/editor/editor.go | 65 ++++++++++--------- .../tui/components/completions/completions.go | 50 +++++++++----- .../bubbles/v2/filepicker/filepicker.go | 6 +- .../bubbles/v2/textarea/textarea.go | 35 ++++++++++ vendor/modules.txt | 2 +- 8 files changed, 113 insertions(+), 52 deletions(-) diff --git a/.gitignore b/.gitignore index 2f16f744432d89e0a72fd6ea8e359678a64b6d42..1bd8e7f96d876b03ce3711854b5a050c1419b0e5 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ Thumbs.db manpages/ completions/ +!internal/tui/components/completions/ diff --git a/go.mod b/go.mod index d28bb8b1d169151a68aa35c84f585c81275ff675..d6e1572028e1105651910deb40dcbbdc81d96660 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/aymanbagabas/go-udiff v0.3.1 github.com/bmatcuk/doublestar/v4 v4.8.1 github.com/charlievieth/fastwalk v1.0.11 - github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198 + github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.1 github.com/charmbracelet/fang v0.3.1-0.20250711140230-d5ebb8c1d674 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe diff --git a/go.sum b/go.sum index fcda20ce8845005b128c6b8db337eb07b3ca8d8a..ba0beaca4845a75fbe2a8471ab040ac06bb02653 100644 --- a/go.sum +++ b/go.sum @@ -68,8 +68,8 @@ github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/charlievieth/fastwalk v1.0.11 h1:5sLT/q9+d9xMdpKExawLppqvXFZCVKf6JHnr2u/ufj8= github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI= -github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198 h1:CkMS9Ah9ac1Ego5JDC5NJyZyAAqu23Z+O0yDwsa3IxM= -github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw= +github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5 h1:GTcMIfDQJKyNKS+xVt7GkNIwz+tBuQtIuiP50WpzNgs= +github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw= github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250716142813-5d1379f56ba2 h1:Gj/vSk7h96TxUU/GSuwbYkr9H0ze+ElAQjcl25wB0+U= github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250716142813-5d1379f56ba2/go.mod h1:m240IQxo1/eDQ7klblSzOCAUyc3LddHcV3Rc/YEGAgw= github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= diff --git a/internal/tui/components/chat/editor/editor.go b/internal/tui/components/chat/editor/editor.go index 9a0f5d3d191d722f4be2e48a40b730b255bf01d1..51009536358cd849e28cd6c7550376340b58d530 100644 --- a/internal/tui/components/chat/editor/editor.go +++ b/internal/tui/components/chat/editor/editor.go @@ -171,6 +171,8 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.attachments = append(m.attachments, msg.Attachment) return m, nil + case completions.CompletionsOpenedMsg: + m.isCompletionsOpen = true case completions.CompletionsClosedMsg: m.isCompletionsOpen = false m.currentQuery = "" @@ -183,9 +185,6 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // If the selected item is a file, insert its path into the textarea value := m.textarea.Value() value = value[:m.completionsStartIndex] - if len(value) > 0 && value[len(value)-1] != ' ' { - value += " " - } value += item.Path m.textarea.SetValue(value) m.isCompletionsOpen = false @@ -199,37 +198,15 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyPressMsg: switch { // Completions - case msg.String() == "/" && !m.isCompletionsOpen: + case msg.String() == "/" && !m.isCompletionsOpen && + // only show if beginning of prompt, or if previous char is a space: + (len(m.textarea.Value()) == 0 || m.textarea.Value()[len(m.textarea.Value())-1] == ' '): m.isCompletionsOpen = true m.currentQuery = "" - cmds = append(cmds, m.startCompletions) m.completionsStartIndex = len(m.textarea.Value()) - case msg.String() == "space" && m.isCompletionsOpen: - m.isCompletionsOpen = false - m.currentQuery = "" - m.completionsStartIndex = 0 - cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{})) + cmds = append(cmds, m.startCompletions) case m.isCompletionsOpen && m.textarea.Cursor().X <= m.completionsStartIndex: cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{})) - case msg.String() == "backspace" && m.isCompletionsOpen: - if len(m.currentQuery) > 0 { - m.currentQuery = m.currentQuery[:len(m.currentQuery)-1] - cmds = append(cmds, util.CmdHandler(completions.FilterCompletionsMsg{ - Query: m.currentQuery, - })) - } else { - m.isCompletionsOpen = false - m.currentQuery = "" - m.completionsStartIndex = 0 - cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{})) - } - default: - if m.isCompletionsOpen { - m.currentQuery += msg.String() - cmds = append(cmds, util.CmdHandler(completions.FilterCompletionsMsg{ - Query: m.currentQuery, - })) - } } if key.Matches(msg, DeleteKeyMaps.AttachmentDeleteMode) { m.deleteMode = true @@ -281,6 +258,36 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.textarea, cmd = m.textarea.Update(msg) cmds = append(cmds, cmd) + + if m.textarea.Focused() { + kp, ok := msg.(tea.KeyPressMsg) + if ok { + if kp.String() == "space" || m.textarea.Value() == "" { + m.isCompletionsOpen = false + m.currentQuery = "" + m.completionsStartIndex = 0 + cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{})) + } else { + word := m.textarea.Word() + if strings.HasPrefix(word, "/") { + // XXX: wont' work if editing in the middle of the field. + m.completionsStartIndex = strings.LastIndex(m.textarea.Value(), word) + m.currentQuery = word[1:] + m.isCompletionsOpen = true + cmds = append(cmds, util.CmdHandler(completions.FilterCompletionsMsg{ + Query: m.currentQuery, + Reopen: m.isCompletionsOpen, + })) + } else { + m.isCompletionsOpen = false + m.currentQuery = "" + m.completionsStartIndex = 0 + cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{})) + } + } + } + } + return m, tea.Batch(cmds...) } diff --git a/internal/tui/components/completions/completions.go b/internal/tui/components/completions/completions.go index 02307c253c731541e0e09f177fd74f429c928287..bd30dc394d47ad80421e8c78d3a0f84730518a9c 100644 --- a/internal/tui/components/completions/completions.go +++ b/internal/tui/components/completions/completions.go @@ -1,6 +1,8 @@ package completions import ( + "strings" + "github.com/charmbracelet/bubbles/v2/key" tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/crush/internal/tui/components/core/list" @@ -23,11 +25,14 @@ type OpenCompletionsMsg struct { } type FilterCompletionsMsg struct { - Query string // The query to filter completions + Query string // The query to filter completions + Reopen bool } type CompletionsClosedMsg struct{} +type CompletionsOpenedMsg struct{} + type CloseCompletionsMsg struct{} type SelectCompletionMsg struct { @@ -126,11 +131,7 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case CloseCompletionsMsg: c.open = false - c.query = "" - return c, tea.Batch( - c.list.SetItems([]util.Model{}), - util.CmdHandler(CompletionsClosedMsg{}), - ) + return c, util.CmdHandler(CompletionsClosedMsg{}) case OpenCompletionsMsg: c.open = true c.query = "" @@ -143,21 +144,41 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { items = append(items, item) } c.height = max(min(c.height, len(items)), 1) // Ensure at least 1 item height - cmds := []tea.Cmd{ + return c, tea.Batch( c.list.SetSize(c.width, c.height), c.list.SetItems(items), - } - return c, tea.Batch(cmds...) + util.CmdHandler(CompletionsOpenedMsg{}), + ) case FilterCompletionsMsg: - c.query = msg.Query - if !c.open { - return c, nil // If completions are not open, do nothing + if !c.open && !msg.Reopen { + return c, nil + } + if msg.Query == c.query { + // PERF: if same query, don't need to filter again + return c, nil + } + if len(c.list.Items()) == 0 && + len(msg.Query) > len(c.query) && + strings.HasPrefix(msg.Query, c.query) { + // PERF: if c.query didn't match anything, + // AND msg.Query is longer than c.query, + // AND msg.Query is prefixed with c.query - which means + // that the user typed more chars after a 0 match, + // it won't match anything, so return earlier. + return c, nil } + c.query = msg.Query var cmds []tea.Cmd cmds = append(cmds, c.list.Filter(msg.Query)) itemsLen := len(c.list.Items()) c.height = max(min(maxCompletionsHeight, itemsLen), 1) cmds = append(cmds, c.list.SetSize(c.width, c.height)) + if itemsLen == 0 { + cmds = append(cmds, util.CmdHandler(CloseCompletionsMsg{})) + } else if msg.Reopen { + c.open = true + cmds = append(cmds, util.CmdHandler(CompletionsOpenedMsg{})) + } return c, tea.Batch(cmds...) } return c, nil @@ -165,12 +186,9 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // View implements Completions. func (c *completionsCmp) View() string { - if !c.open { + if !c.open || len(c.list.Items()) == 0 { return "" } - if len(c.list.Items()) == 0 { - return c.style().Render("No completions found") - } return c.style().Render(c.list.View()) } diff --git a/vendor/github.com/charmbracelet/bubbles/v2/filepicker/filepicker.go b/vendor/github.com/charmbracelet/bubbles/v2/filepicker/filepicker.go index 90e79d63bfcedfcd1153c7b82789502b52cad844..5a5e5b6578518463775af53efa1b655578d178b9 100644 --- a/vendor/github.com/charmbracelet/bubbles/v2/filepicker/filepicker.go +++ b/vendor/github.com/charmbracelet/bubbles/v2/filepicker/filepicker.go @@ -518,9 +518,9 @@ func (m Model) canSelect(file string) bool { } // HighlightedPath returns the path of the currently highlighted file or directory. -func (M Model) HighlightedPath() string { - if len(M.files) == 0 || M.selected < 0 || M.selected >= len(M.files) { +func (m Model) HighlightedPath() string { + if len(m.files) == 0 || m.selected < 0 || m.selected >= len(m.files) { return "" } - return filepath.Join(M.CurrentDirectory, M.files[M.selected].Name()) + return filepath.Join(m.CurrentDirectory, m.files[m.selected].Name()) } diff --git a/vendor/github.com/charmbracelet/bubbles/v2/textarea/textarea.go b/vendor/github.com/charmbracelet/bubbles/v2/textarea/textarea.go index f68db029188ba330e82c40b8cc89ecb30a0b258f..4c44935c3343c5d41c1a70c2a85e8e2951cfa9c4 100644 --- a/vendor/github.com/charmbracelet/bubbles/v2/textarea/textarea.go +++ b/vendor/github.com/charmbracelet/bubbles/v2/textarea/textarea.go @@ -722,6 +722,41 @@ func (m *Model) Reset() { m.SetCursorColumn(0) } +// Word returns the word at the cursor position. +// A word is delimited by spaces or line-breaks. +func (m *Model) Word() string { + line := m.value[m.row] + col := m.col - 1 + + if col < 0 { + return "" + } + + // If cursor is beyond the line, return empty string + if col >= len(line) { + return "" + } + + // If cursor is on a space, return empty string + if unicode.IsSpace(line[col]) { + return "" + } + + // Find the start of the word by moving left + start := col + for start > 0 && !unicode.IsSpace(line[start-1]) { + start-- + } + + // Find the end of the word by moving right + end := col + for end < len(line) && !unicode.IsSpace(line[end]) { + end++ + } + + return string(line[start:end]) +} + // san initializes or retrieves the rune sanitizer. func (m *Model) san() runeutil.Sanitizer { if m.rsan == nil { diff --git a/vendor/modules.txt b/vendor/modules.txt index 05b8504bdbb4df51e69c5a5eb7d49aec857144a3..68b2f80b68318cd32262cfee0c23b36064255823 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -241,7 +241,7 @@ github.com/bmatcuk/doublestar/v4 github.com/charlievieth/fastwalk github.com/charlievieth/fastwalk/internal/dirent github.com/charlievieth/fastwalk/internal/fmtdirent -# github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198 +# github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5 ## explicit; go 1.23.0 github.com/charmbracelet/bubbles/v2/cursor github.com/charmbracelet/bubbles/v2/filepicker