summaryrefslogtreecommitdiff
path: root/kevent.go
diff options
context:
space:
mode:
Diffstat (limited to 'kevent.go')
-rw-r--r--kevent.go965
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"
+ }
+}