diff --git a/cmd/root.go b/cmd/root.go index 02ebbd0104e88a59b659c322a0605189b01afaaf..500909360407ccac25111acf33f28c692eeb8ffa 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -120,6 +120,8 @@ to assist developers in writing, debugging, and understanding code directly from tea.WithAltScreen(), tea.WithKeyReleases(), tea.WithUniformKeyLayout(), + tea.WithMouseCellMotion(), // Use cell motion instead of all motion to reduce event flooding + tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state ) // Setup the subscriptions, this will send services events to the TUI diff --git a/internal/tui/components/chat/editor/editor.go b/internal/tui/components/chat/editor/editor.go index 30d3aeef80c57487e665952f699cd7b4522db214..0e23799f3345ef01618856129d56b5b7982d8e22 100644 --- a/internal/tui/components/chat/editor/editor.go +++ b/internal/tui/components/chat/editor/editor.go @@ -7,6 +7,7 @@ import ( "runtime" "slices" "strings" + "time" "unicode" "github.com/charmbracelet/bubbles/v2/key" @@ -30,6 +31,8 @@ type FileCompletionItem struct { Path string // The file path } +type CompletionDebounceMsg struct{} + type editorCmp struct { width int height int @@ -46,6 +49,9 @@ type editorCmp struct { currentQuery string completionsStartIndex int isCompletionsOpen bool + + // Debouncing for completions + debounceTimer *time.Timer } var DeleteKeyMaps = DeleteAttachmentKeyMaps{ @@ -141,6 +147,22 @@ func (m *editorCmp) send() tea.Cmd { ) } +// debouncedCompletionFilter creates a debounced command for filtering completions +func (m *editorCmp) debouncedCompletionFilter() tea.Cmd { + // Cancel existing timer if any + if m.debounceTimer != nil { + m.debounceTimer.Stop() + } + + // Create new timer for debouncing + m.debounceTimer = time.NewTimer(150 * time.Millisecond) + + return func() tea.Msg { + <-m.debounceTimer.C + return CompletionDebounceMsg{} + } +} + func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd @@ -160,6 +182,18 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.isCompletionsOpen = false m.currentQuery = "" m.completionsStartIndex = 0 + // Cancel any pending debounce timer + if m.debounceTimer != nil { + m.debounceTimer.Stop() + m.debounceTimer = nil + } + case CompletionDebounceMsg: + // Handle debounced completion filtering + if m.isCompletionsOpen { + return m, util.CmdHandler(completions.FilterCompletionsMsg{ + Query: m.currentQuery, + }) + } case completions.SelectCompletionMsg: if !m.isCompletionsOpen { return m, nil @@ -196,9 +230,8 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 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, - })) + // Use debounced filtering instead of immediate filtering + cmds = append(cmds, m.debouncedCompletionFilter()) } else { m.isCompletionsOpen = false m.currentQuery = "" @@ -208,9 +241,8 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { default: if m.isCompletionsOpen { m.currentQuery += msg.String() - cmds = append(cmds, util.CmdHandler(completions.FilterCompletionsMsg{ - Query: m.currentQuery, - })) + // Use debounced filtering instead of immediate filtering + cmds = append(cmds, m.debouncedCompletionFilter()) } } if key.Matches(msg, DeleteKeyMaps.AttachmentDeleteMode) { @@ -258,6 +290,7 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } } + m.textarea, cmd = m.textarea.Update(msg) cmds = append(cmds, cmd) return m, tea.Batch(cmds...) @@ -355,6 +388,11 @@ func (m *editorCmp) startCompletions() tea.Msg { // Blur implements Container. func (c *editorCmp) Blur() tea.Cmd { c.textarea.Blur() + // Clean up debounce timer when losing focus + if c.debounceTimer != nil { + c.debounceTimer.Stop() + c.debounceTimer = nil + } return nil } diff --git a/internal/tui/page/chat/chat.go b/internal/tui/page/chat/chat.go index b079859c5b935f02247f84ff0757933fc7a11a4f..59b8ac86da3901bfb5a3affe8aeedf27f293a507 100644 --- a/internal/tui/page/chat/chat.go +++ b/internal/tui/page/chat/chat.go @@ -39,6 +39,7 @@ type ( type ChatPage interface { util.Model layout.Help + IsChatFocused() bool } type chatPage struct { @@ -415,3 +416,8 @@ func NewChatPage(app *app.App) ChatPage { header: header.New(app.LSPClients), } } + +// IsChatFocused returns whether the chat messages are focused (true) or editor is focused (false) +func (p *chatPage) IsChatFocused() bool { + return p.chatFocused +} diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 7ab868f20f830d040df3a5d2f17347faaec235d0..dd0d4f9085d92b20aa92c8d2f25227b5e2206816 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -31,6 +31,29 @@ import ( "github.com/charmbracelet/lipgloss/v2" ) +// MouseEventFilter filters mouse events based on the current focus state +// This is used with tea.WithFilter to prevent mouse scroll events from +// interfering with typing performance in the editor +func MouseEventFilter(m tea.Model, msg tea.Msg) tea.Msg { + // Only filter mouse events + switch msg.(type) { + case tea.MouseWheelMsg, tea.MouseMotionMsg: + // Check if we have an appModel and if editor is focused + if appModel, ok := m.(*appModel); ok { + if appModel.currentPage == chat.ChatPageID { + if chatPage, ok := appModel.pages[appModel.currentPage].(chat.ChatPage); ok { + // If editor is focused (not chatFocused), filter out mouse wheel/motion events + if !chatPage.IsChatFocused() { + return nil // Filter out the event + } + } + } + } + } + // Allow all other events to pass through + return msg +} + // appModel represents the main application model that manages pages, dialogs, and UI state. type appModel struct { wWidth, wHeight int // Window dimensions @@ -82,9 +105,6 @@ func (a appModel) Init() tea.Cmd { return nil }) - // Enable mouse support. - cmds = append(cmds, tea.EnableMouseAllMotion) - return tea.Batch(cmds...) }