diff options
| author | Mitja Felicijan <mitja.felicijan@gmail.com> | 2026-01-21 20:22:09 +0100 |
|---|---|---|
| committer | Mitja Felicijan <mitja.felicijan@gmail.com> | 2026-01-21 20:22:09 +0100 |
| commit | 5a8dbc6347b3541e84fe669b22c17ad3b715e258 (patch) | |
| tree | b148c450939688caaaeb4adac6f2faa1eaffe649 /kevent.go | |
| download | qwe-editor-5a8dbc6347b3541e84fe669b22c17ad3b715e258.tar.gz | |
Engage!
Diffstat (limited to 'kevent.go')
| -rw-r--r-- | kevent.go | 965 |
1 files changed, 965 insertions, 0 deletions
diff --git a/kevent.go b/kevent.go new file mode 100644 index 0000000..d96f088 --- /dev/null +++ b/kevent.go @@ -0,0 +1,965 @@ +package main + +// Input processing engine. It contains the main event loop and dispatches +// keyboard/mouse events to mode-specific handlers (Normal, Insert, Visual, +// etc.). + +import ( + "github.com/nsf/termbox-go" +) + +// HandleEvents is the central loop that waits for and processes all user input. +func (e *Editor) HandleEvents() { + for { + // Redraw the screen before waiting for the next event. + e.draw() + ev := termbox.PollEvent() + + // Handle interrupt events (triggered by diagnostic updates). + // Fetch latest diagnostics from LSP client. + if ev.Type == termbox.EventInterrupt { + b := e.activeBuffer() + if b != nil && b.lspClient != nil { + b.diagnostics = b.lspClient.GetDiagnostics() + } + e.CheckFilesOnDisk() + continue + } + + if ev.Type == termbox.EventKey { + // Clear message on any key press unless specifically set. + e.message = "" + // Hide hover popup if any key other than Ctrl+K is pressed. + if e.showHover && ev.Key != termbox.KeyCtrlK { + e.showHover = false + } + + // If dev mode, exit the editor with Ctrl+C. + if ev.Key == termbox.KeyCtrlC && e.devMode { + return + } + + // Dispatch the key event to the handler for the current editor mode. + switch e.mode { + case ModeNormal: + e.handleNormalMode(ev) + case ModeInsert: + e.handleInsertMode(ev) + case ModeCommand: + e.handleCommandMode(ev) + case ModeFuzzy: + e.handleFuzzyMode(ev) + case ModeFind: + e.handleFindMode(ev) + case ModeVisual: + e.handleVisualMode(ev) + case ModeVisualLine: + e.handleVisualLineMode(ev) + case ModeVisualBlock: + e.handleVisualBlockMode(ev) + case ModeReplace: + e.handleReplaceMode(ev) + case ModeConfirm: + e.handleConfirmMode(ev) + } + } else if ev.Type == termbox.EventMouse { + e.handleMouseEvent(ev) + } + } +} + +// handleNormalMode processes keyboard input when the editor is in Normal mode. +func (e *Editor) handleNormalMode(ev termbox.Event) { + // Escape clears any pending multi-key commands or secondary cursors. + if ev.Key == termbox.KeyEsc { + b := e.activeBuffer() + if b != nil && len(b.cursors) > 1 { + e.clearSecondaryCursors() + e.pendingKey = 0 + e.message = "Cleared secondary cursors" + return + } + e.pendingKey = 0 + return + } + + switch ev.Key { + case termbox.KeyArrowLeft: + e.moveCursor(-1, 0) + case termbox.KeyArrowRight: + e.moveCursor(1, 0) + case termbox.KeyArrowUp: + if ev.Mod != 0 { + e.addCursorAbove() + } else { + e.moveCursor(0, -1) + } + case termbox.KeyArrowDown: + if ev.Mod != 0 { + e.addCursorBelow() + } else { + e.moveCursor(0, 1) + } + case termbox.KeyCtrlX: + e.addCursorBelow() + case termbox.KeyCtrlP: + e.prevBuffer() + case termbox.KeyCtrlN: + e.nextBuffer() + case termbox.KeyCtrlO: + e.jumpBack() + case termbox.KeyCtrlI: + e.jumpForward() + case termbox.KeyCtrlV: + b := e.activeBuffer() + if b != nil { + e.visualStartX = b.PrimaryCursor().X + e.visualStartY = b.PrimaryCursor().Y + } + e.mode = ModeVisualBlock + case termbox.KeyCtrlK: + e.triggerHover() + } + + switch ev.Ch { + case 'i': + e.saveState() + e.mode = ModeInsert + e.introDismissed = true + case 'a': + e.saveState() + e.moveCursor(1, 0) + e.mode = ModeInsert + e.introDismissed = true + case 'A': + e.saveState() + e.jumpToLineEnd() + e.mode = ModeInsert + e.introDismissed = true + case 'I': + e.saveState() + e.jumpToFirstNonBlank() + e.mode = ModeInsert + e.introDismissed = true + case 'o': + e.saveState() + e.insertLineBelow() + e.mode = ModeInsert + e.introDismissed = true + case 'O': + e.saveState() + e.insertLineAbove() + e.mode = ModeInsert + e.introDismissed = true + case ']': + e.pushJump() + e.jumpToNextEmptyLine() + case '}': + e.pushJump() + e.jumpToBottom() + case 'v': + b := e.activeBuffer() + if b != nil { + e.visualStartX = b.PrimaryCursor().X + e.visualStartY = b.PrimaryCursor().Y + } + e.mode = ModeVisual + case 'V': + b := e.activeBuffer() + if b != nil { + e.visualStartX = b.PrimaryCursor().X + e.visualStartY = b.PrimaryCursor().Y + } + e.mode = ModeVisualLine + case ':': + e.mode = ModeCommand + e.commandBuffer = []rune{} + e.commandCursorX = 0 + case '/': + e.findSavedSearch = e.lastSearch + e.mode = ModeFind + e.findBuffer = []rune{} + case Config.LeaderKey: + e.pendingKey = Config.LeaderKey + case 'l': + if e.pendingKey == Config.LeaderKey { + e.toggleDebugWindow() + e.pendingKey = 0 + } + case 'w': + if e.pendingKey == 'd' { + e.saveState() + e.deleteWord(true) + e.checkDiagnostics() + e.pendingKey = 0 + } else if e.pendingKey == 'c' { + e.saveState() + e.changeWord() + e.checkDiagnostics() + e.pendingKey = 0 + } else if e.pendingKey == Config.LeaderKey { + e.startWarningsFuzzyFinder() + e.pendingKey = 0 + } else { + e.moveWordForward() + } + case 'q': + if e.pendingKey == 'z' { + e.formatText() + e.checkDiagnostics() + e.pendingKey = 0 + } else if e.pendingKey == Config.LeaderKey { + e.lastSearch = "" + e.pendingKey = 0 + } else { + e.moveWordBackward() + } + case 'Q': + e.jumpToFirstNonBlank() + case 'W': + e.jumpToLineEnd() + case 'g': + e.pendingKey = 'g' + case 'j': + e.saveState() + e.JoinLines() + e.checkDiagnostics() + case 'f': + if e.pendingKey == 'g' { + e.gotoFile() + e.pendingKey = 0 + } + case 'd': + if e.pendingKey == Config.LeaderKey { + e.deleteCurrentBuffer() + e.pendingKey = 0 + } else if e.pendingKey == 'd' { + e.saveState() + e.deleteLine() + e.checkDiagnostics() + e.pendingKey = 0 + } else if e.pendingKey == 'g' { + e.gotoDefinition() + e.pendingKey = 0 + } else { + e.pendingKey = 'd' + } + case 'y': + e.yankLine() + e.message = "Line yanked" + case 'x': + if e.pendingKey == 'z' { + e.saveState() + e.toggleCommentLine() + e.checkDiagnostics() + e.pendingKey = 0 + } else { + e.saveState() + e.DeleteChar() + e.checkDiagnostics() + e.pendingKey = 0 + } + case 'z': + if e.pendingKey == 'z' { + e.centerScreen() + e.pendingKey = 0 + } else { + e.pendingKey = 'z' + } + case 'c': + if e.pendingKey == 'd' { + e.saveState() + e.DeleteChar() + e.checkDiagnostics() + e.pendingKey = 0 + } else if e.pendingKey == 'c' { + e.saveState() + e.changeCharacter() + e.checkDiagnostics() + e.pendingKey = 0 + } else { + e.pendingKey = 'c' + } + case 'C': + e.saveState() + e.changeToEndOfLine() + e.checkDiagnostics() + e.pendingKey = 0 + case 'D': + e.saveState() + e.deleteToEndOfLine() + e.checkDiagnostics() + e.pendingKey = 0 + case '(': + if e.pendingKey == 'c' { + e.saveState() + e.changeInside('(', ')') + e.checkDiagnostics() + e.pendingKey = 0 + } else if e.pendingKey == 'd' { + e.saveState() + e.deleteInside('(', ')') + e.checkDiagnostics() + e.pendingKey = 0 + } + case '[': + if e.pendingKey == 'c' { + e.saveState() + e.changeInside('[', ']') + e.checkDiagnostics() + e.pendingKey = 0 + } else if e.pendingKey == 'd' { + e.saveState() + e.deleteInside('[', ']') + e.checkDiagnostics() + e.pendingKey = 0 + } else { + e.pushJump() + e.jumpToPrevEmptyLine() + } + case '{': + if e.pendingKey == 'c' { + e.saveState() + e.changeInside('{', '}') + e.checkDiagnostics() + e.pendingKey = 0 + } else if e.pendingKey == 'd' { + e.saveState() + e.deleteInside('{', '}') + e.checkDiagnostics() + e.pendingKey = 0 + } else { + e.pushJump() + e.jumpToTop() + } + case '\'': + if e.pendingKey == 'c' { + e.saveState() + e.changeInside('\'', '\'') + e.checkDiagnostics() + e.pendingKey = 0 + } else if e.pendingKey == 'd' { + e.saveState() + e.deleteInside('\'', '\'') + e.checkDiagnostics() + e.pendingKey = 0 + } + case '"': + if e.pendingKey == 'c' { + e.saveState() + e.changeInside('"', '"') + e.checkDiagnostics() + e.pendingKey = 0 + } else if e.pendingKey == 'd' { + e.saveState() + e.deleteInside('"', '"') + e.checkDiagnostics() + e.pendingKey = 0 + } + case 's': + e.saveState() + e.changeCharacter() + e.checkDiagnostics() + e.pendingKey = 0 + case 'n': + e.findNext() + e.centerCursor() + case 'N': + e.findPrev() + e.centerCursor() + case 'u': + e.undo() + e.checkDiagnostics() + e.pendingKey = 0 + case 'U': + e.redo() + e.checkDiagnostics() + e.pendingKey = 0 + case 'p': + if e.pendingKey == Config.LeaderKey { + e.startFileFuzzyFinder() + e.pendingKey = 0 + } else { + e.saveState() + e.pasteLine() + e.checkDiagnostics() + e.pendingKey = 0 + } + case 'b': + if e.pendingKey == Config.LeaderKey { + e.startBufferFuzzyFinder() + e.pendingKey = 0 + } + case 'P': + if e.pendingKey == Config.LeaderKey { + e.pendingKey = 0 + } else { + e.saveState() + e.pasteLineAbove() + e.checkDiagnostics() + e.pendingKey = 0 + } + default: + e.pendingKey = 0 + } +} + +// handleInsertMode processes keyboard input when the editor is in Insert mode. +func (e *Editor) handleInsertMode(ev termbox.Event) { + if e.showAutocomplete { + switch ev.Key { + case termbox.KeyArrowUp: + e.autocompleteIndex-- + if e.autocompleteIndex < 0 { + e.autocompleteIndex = len(e.autocompleteItems) - 1 + } + // Adjust scroll to keep selection visible + if e.autocompleteIndex < e.autocompleteScroll { + e.autocompleteScroll = e.autocompleteIndex + } + if e.autocompleteIndex >= e.autocompleteScroll+10 { + e.autocompleteScroll = e.autocompleteIndex - 9 + } + return + case termbox.KeyArrowDown: + e.autocompleteIndex++ + if e.autocompleteIndex >= len(e.autocompleteItems) { + e.autocompleteIndex = 0 + } + // Adjust scroll to keep selection visible + if e.autocompleteIndex < e.autocompleteScroll { + e.autocompleteScroll = e.autocompleteIndex + } + if e.autocompleteIndex >= e.autocompleteScroll+10 { + e.autocompleteScroll = e.autocompleteIndex - 9 + } + return + case termbox.KeyEnter: + e.insertCompletion(e.autocompleteItems[e.autocompleteIndex]) + return + case termbox.KeyEsc: + e.showAutocomplete = false + return + } + } + + switch ev.Key { + case termbox.KeyEsc: + // Return to Normal mode and trigger a diagnostic check. + e.mode = ModeNormal + e.checkDiagnostics() + case termbox.KeyEnter: + e.insertNewline() + case termbox.KeySpace: + e.insertRune(' ') + case termbox.KeyBackspace, termbox.KeyBackspace2: + e.backspace() + if e.showAutocomplete { + e.showAutocomplete = false + } + case termbox.KeyTab: + e.insertTab() + if e.showAutocomplete { + e.showAutocomplete = false + } + case termbox.KeyArrowLeft: + e.moveCursor(-1, 0) + if e.showAutocomplete { + e.showAutocomplete = false + } + case termbox.KeyArrowRight: + e.moveCursor(1, 0) + if e.showAutocomplete { + e.showAutocomplete = false + } + case termbox.KeyArrowUp: + e.moveCursor(0, -1) + if e.showAutocomplete { + e.showAutocomplete = false + } + case termbox.KeyArrowDown: + e.moveCursor(0, 1) + if e.showAutocomplete { + e.showAutocomplete = false + } + case termbox.KeyCtrlW: + e.deleteWordBackward() + case termbox.KeyCtrlN: + e.triggerAutocomplete() + default: + // If a character key was pressed, insert the character. + if ev.Ch != 0 { + e.insertRune(ev.Ch) + // Close autocomplete if user keeps typing. + if e.showAutocomplete { + e.showAutocomplete = false + } + } + } +} + +// handleCommandMode processes keyboard input for the colon command line. +func (e *Editor) handleCommandMode(ev termbox.Event) { + switch ev.Key { + case termbox.KeyEsc: + // Cancel command entry. + e.mode = ModeNormal + e.commandBuffer = []rune{} + e.commandCursorX = 0 + e.commandHistoryIdx = -1 + e.checkDiagnostics() + case termbox.KeyEnter: + // Execute the entered command and save to history if valid. + cmd := string(e.commandBuffer) + e.commands.HandleAndSaveToHistory(cmd) + e.commandHistoryIdx = -1 + case termbox.KeyBackspace, termbox.KeyBackspace2: + if e.commandCursorX > 0 { + // Delete character before cursor + e.commandBuffer = append(e.commandBuffer[:e.commandCursorX-1], e.commandBuffer[e.commandCursorX:]...) + e.commandCursorX-- + } else if len(e.commandBuffer) == 0 { + // If buffer is empty, backspace returns to Normal mode. + e.mode = ModeNormal + } + e.commandHistoryIdx = -1 + case termbox.KeySpace: + // Insert space at cursor position + e.commandBuffer = append(e.commandBuffer[:e.commandCursorX], append([]rune{' '}, e.commandBuffer[e.commandCursorX:]...)...) + e.commandCursorX++ + e.commandHistoryIdx = -1 + case termbox.KeyCtrlW: + e.deleteWordBackwardFromBuffer() + e.commandHistoryIdx = -1 + case termbox.KeyArrowLeft: + // Move cursor left + if e.commandCursorX > 0 { + e.commandCursorX-- + } + case termbox.KeyArrowRight: + // Move cursor right + if e.commandCursorX < len(e.commandBuffer) { + e.commandCursorX++ + } + case termbox.KeyArrowUp: + // Navigate to previous command in history + e.commands.NavigateHistoryUp() + case termbox.KeyArrowDown: + // Navigate to next command in history + e.commands.NavigateHistoryDown() + default: + if ev.Ch != 0 { + // Insert character at cursor position + e.commandBuffer = append(e.commandBuffer[:e.commandCursorX], append([]rune{ev.Ch}, e.commandBuffer[e.commandCursorX:]...)...) + e.commandCursorX++ + e.commandHistoryIdx = -1 + } + } +} + +// handleFuzzyMode processes input for the fuzzy finder (files or buffers). +func (e *Editor) handleFuzzyMode(ev termbox.Event) { + switch ev.Key { + case termbox.KeyEsc: + e.mode = ModeNormal + case termbox.KeyEnter: + // Open the currently selected item in the list. + e.openSelectedFile() + case termbox.KeyArrowUp: + e.fuzzyMove(1) + case termbox.KeyArrowDown: + e.fuzzyMove(-1) + case termbox.KeyBackspace, termbox.KeyBackspace2: + if len(e.fuzzyBuffer) > 0 { + e.fuzzyBuffer = e.fuzzyBuffer[:len(e.fuzzyBuffer)-1] + e.updateFuzzyResults() + } + case termbox.KeySpace: + e.fuzzyBuffer = append(e.fuzzyBuffer, ' ') + e.updateFuzzyResults() + default: + // Update filter as user types. + if ev.Ch != 0 { + e.fuzzyBuffer = append(e.fuzzyBuffer, ev.Ch) + e.updateFuzzyResults() + } + } +} + +// handleFindMode processes input for the in-file search (/). +func (e *Editor) handleFindMode(ev termbox.Event) { + switch ev.Key { + case termbox.KeyEsc: + e.mode = ModeNormal + e.findBuffer = []rune{} + // Revert to the last successful search term. + e.lastSearch = e.findSavedSearch + e.checkDiagnostics() + case termbox.KeyEnter: + if len(e.findBuffer) > 0 { + e.lastSearch = string(e.findBuffer) + e.findNext() + e.centerCursor() + } + e.mode = ModeNormal + case termbox.KeyBackspace, termbox.KeyBackspace2: + if len(e.findBuffer) > 0 { + e.findBuffer = e.findBuffer[:len(e.findBuffer)-1] + e.lastSearch = string(e.findBuffer) + } else { + e.lastSearch = e.findSavedSearch + } + case termbox.KeySpace: + e.findBuffer = append(e.findBuffer, ' ') + e.lastSearch = string(e.findBuffer) + default: + // Incremental search: update e.lastSearch as the user types. + if ev.Ch != 0 { + e.findBuffer = append(e.findBuffer, ev.Ch) + e.lastSearch = string(e.findBuffer) + } + } +} + +// handleVisualMode processes input for character-wise visual selection. +func (e *Editor) handleVisualMode(ev termbox.Event) { + if ev.Key == termbox.KeyEsc { + // Exit visual mode and return to Normal. + e.mode = ModeNormal + return + } + + switch ev.Key { + case termbox.KeyArrowLeft: + e.moveCursor(-1, 0) + case termbox.KeyArrowRight: + e.moveCursor(1, 0) + case termbox.KeyArrowUp: + e.moveCursor(0, -1) + case termbox.KeyArrowDown: + e.moveCursor(0, 1) + } + + switch ev.Ch { + case Config.LeaderKey: + e.pendingKey = Config.LeaderKey + case 'w': + e.moveWordForward() + case 'q': + if e.pendingKey == 'z' { + e.formatText() + e.checkDiagnostics() + e.pendingKey = 0 + } else { + e.moveWordBackward() + } + case 'y': + e.yankVisualSelection() + e.message = "Selection yanked" + case 'd': + e.saveState() + e.deleteVisualSelection() + e.checkDiagnostics() + e.message = "Selection deleted" + case 'x': + if e.pendingKey == 'z' { + e.saveState() + e.commentVisualSelection() + e.checkDiagnostics() + e.pendingKey = 0 + } else { + e.saveState() + e.deleteVisualSelection() + e.checkDiagnostics() + e.message = "Selection deleted" + } + case 'p': + e.saveState() + e.pasteVisualSelection() + e.checkDiagnostics() + case 'c': + e.saveState() + e.changeVisualSelection() + e.checkDiagnostics() + case 'Q': + e.jumpToFirstNonBlank() + case 'W': + e.jumpToLineEnd() + case '~': + e.saveState() + e.ToggleCaseVisualSelection() + e.checkDiagnostics() + case 'o': + if e.pendingKey == Config.LeaderKey { + e.ollamaComplete() + e.pendingKey = 0 + } else { + // Swap cursor and visual anchor + b := e.activeBuffer() + if b != nil { + tmpX, tmpY := b.PrimaryCursor().X, b.PrimaryCursor().Y + b.PrimaryCursor().X, b.PrimaryCursor().Y = e.visualStartX, e.visualStartY + e.visualStartX, e.visualStartY = tmpX, tmpY + } + } + case '{': + e.jumpToTop() + case '}': + e.jumpToBottom() + case '[': + e.jumpToPrevEmptyLine() + case ']': + e.jumpToNextEmptyLine() + case ':': + e.mode = ModeCommand + e.commandBuffer = []rune{} + e.commandCursorX = 0 + case 'V': + e.mode = ModeVisualLine + case 'z': + e.pendingKey = 'z' + case 'R': + e.startReplaceMode() + } +} + +func (e *Editor) handleVisualLineMode(ev termbox.Event) { + if ev.Key == termbox.KeyEsc { + e.mode = ModeNormal + return + } + + switch ev.Key { + case termbox.KeyArrowLeft: + e.moveCursor(-1, 0) + case termbox.KeyArrowRight: + e.moveCursor(1, 0) + case termbox.KeyArrowUp: + e.moveCursor(0, -1) + case termbox.KeyArrowDown: + e.moveCursor(0, 1) + } + + switch ev.Ch { + case Config.LeaderKey: + e.pendingKey = Config.LeaderKey + case 'w': + e.moveWordForward() + case 'q': + if e.pendingKey == 'z' { + e.formatText() + e.checkDiagnostics() + e.pendingKey = 0 + } else { + e.moveWordBackward() + } + case 'y': + e.yankVisualSelection() + e.message = "Selection yanked" + case 'd': + e.saveState() + e.deleteVisualSelection() + e.checkDiagnostics() + e.message = "Selection deleted" + case 'x': + if e.pendingKey == 'z' { + e.saveState() + e.commentVisualSelection() + e.checkDiagnostics() + e.pendingKey = 0 + } else { + e.saveState() + e.deleteVisualSelection() + e.checkDiagnostics() + e.message = "Selection deleted" + } + case 'p': + e.saveState() + e.pasteVisualSelection() + e.checkDiagnostics() + case 'c': + e.saveState() + e.changeVisualSelection() + e.checkDiagnostics() + case 'Q': + e.jumpToFirstNonBlank() + case 'W': + e.jumpToLineEnd() + case '~': + e.saveState() + e.ToggleCaseVisualSelection() + e.checkDiagnostics() + case 'o': + if e.pendingKey == Config.LeaderKey { + e.ollamaComplete() + e.pendingKey = 0 + } else { + // Swap cursor and visual anchor + b := e.activeBuffer() + if b != nil { + tmpX, tmpY := b.PrimaryCursor().X, b.PrimaryCursor().Y + b.PrimaryCursor().X, b.PrimaryCursor().Y = e.visualStartX, e.visualStartY + e.visualStartX, e.visualStartY = tmpX, tmpY + } + } + case '{': + e.jumpToTop() + case '}': + e.jumpToBottom() + case '[': + e.jumpToPrevEmptyLine() + case ']': + e.jumpToNextEmptyLine() + case 'z': + e.pendingKey = 'z' + case 'v': + e.mode = ModeVisual + case 'V': + e.mode = ModeNormal + case 'R': + e.startReplaceMode() + } +} + +// handleVisualBlockMode processes input for column-wise (rectangular) selection. +func (e *Editor) handleVisualBlockMode(ev termbox.Event) { + if ev.Key == termbox.KeyEsc { + e.mode = ModeNormal + return + } + + switch ev.Key { + case termbox.KeyArrowLeft: + e.moveCursor(-1, 0) + case termbox.KeyArrowRight: + e.moveCursor(1, 0) + case termbox.KeyArrowUp: + e.moveCursor(0, -1) + case termbox.KeyArrowDown: + e.moveCursor(0, 1) + } + + switch ev.Ch { + case Config.LeaderKey: + e.pendingKey = Config.LeaderKey + case 'w': + e.moveWordForward() + case 'q': + if e.pendingKey == 'z' { + e.formatText() + e.checkDiagnostics() + e.pendingKey = 0 + } else { + e.moveWordBackward() + } + case 'y': + e.yankVisualSelection() + e.message = "Selection yanked" + case 'd': + e.saveState() + e.deleteVisualSelection() + e.checkDiagnostics() + e.message = "Selection deleted" + case 'x': + if e.pendingKey == 'z' { + e.saveState() + e.commentVisualSelection() + e.checkDiagnostics() + e.pendingKey = 0 + } else { + e.saveState() + e.deleteVisualSelection() + e.checkDiagnostics() + e.message = "Selection deleted" + } + case 'p': + e.saveState() + e.pasteVisualSelection() + e.checkDiagnostics() + case 'c': + e.saveState() + e.changeVisualSelection() + e.checkDiagnostics() + case 'Q': + e.jumpToFirstNonBlank() + case 'W': + e.jumpToLineEnd() + case '~': + e.saveState() + e.ToggleCaseVisualSelection() + e.checkDiagnostics() + case 'o': + if e.pendingKey == Config.LeaderKey { + e.ollamaComplete() + e.pendingKey = 0 + } else { + // Swap cursor and visual anchor + b := e.activeBuffer() + if b != nil { + tmpX, tmpY := b.PrimaryCursor().X, b.PrimaryCursor().Y + b.PrimaryCursor().X, b.PrimaryCursor().Y = e.visualStartX, e.visualStartY + e.visualStartX, e.visualStartY = tmpX, tmpY + } + } + case '{': + e.jumpToTop() + case '}': + e.jumpToBottom() + case '[': + e.jumpToPrevEmptyLine() + case ']': + e.jumpToNextEmptyLine() + case 'z': + e.pendingKey = 'z' + case 'v': + e.mode = ModeVisual + case 'V': + e.mode = ModeVisualLine + case 'R': + e.startReplaceMode() + } +} + +// handleMouseEvent handles simple mouse wheel scrolling. +func (e *Editor) handleMouseEvent(ev termbox.Event) { + switch ev.Key { + case termbox.MouseWheelUp: + e.moveCursor(0, -1) // Scroll up by moving the cursor. + case termbox.MouseWheelDown: + e.moveCursor(0, 1) // Scroll down by moving the cursor. + } +} + +// handleConfirmMode processes yes/no confirmations for dangerous actions (like overwriting files). +func (e *Editor) handleConfirmMode(ev termbox.Event) { + if ev.Key == termbox.KeyEsc { + e.mode = ModeNormal + e.pendingConfirm = nil + e.message = "Cancelled" + return + } + + if ev.Key == termbox.KeyEnter { + // Default Enter to "no/cancel" to avoid accidental execution. + e.mode = ModeNormal + e.pendingConfirm = nil + e.message = "Cancelled" + return + } + + switch ev.Ch { + case 'y', 'Y': + if e.pendingConfirm != nil { + action := e.pendingConfirm + e.pendingConfirm = nil + e.mode = ModeNormal + action() + } else { + e.mode = ModeNormal + } + case 'n', 'N': + e.mode = ModeNormal + e.pendingConfirm = nil + e.message = "Cancelled" + } +} |
