1package main
  2
  3// Input processing engine. It contains the main event loop and dispatches
  4// keyboard/mouse events to mode-specific handlers (Normal, Insert, Visual,
  5// etc.).
  6
  7import (
  8	"github.com/nsf/termbox-go"
  9)
 10
 11// HandleEvents is the central loop that waits for and processes all user input.
 12func (e *Editor) HandleEvents() {
 13	for {
 14		// Redraw the screen before waiting for the next event.
 15		e.draw()
 16		ev := termbox.PollEvent()
 17
 18		// Handle interrupt events (triggered by diagnostic updates).
 19		// Fetch latest diagnostics from LSP client.
 20		if ev.Type == termbox.EventInterrupt {
 21			b := e.activeBuffer()
 22			if b != nil && b.lspClient != nil {
 23				b.diagnostics = b.lspClient.GetDiagnostics()
 24			}
 25			e.CheckFilesOnDisk()
 26			continue
 27		}
 28
 29		if ev.Type == termbox.EventKey {
 30			// Clear message on any key press unless specifically set.
 31			e.message = ""
 32			// Hide hover popup if any key other than Ctrl+K is pressed.
 33			if e.showHover && ev.Key != termbox.KeyCtrlK {
 34				e.showHover = false
 35			}
 36
 37			// If dev mode, exit the editor with Ctrl+C.
 38			if ev.Key == termbox.KeyCtrlC && e.devMode {
 39				return
 40			}
 41
 42			// Dispatch the key event to the handler for the current editor mode.
 43			switch e.mode {
 44			case ModeNormal:
 45				e.handleNormalMode(ev)
 46			case ModeInsert:
 47				e.handleInsertMode(ev)
 48			case ModeCommand:
 49				e.handleCommandMode(ev)
 50			case ModeFuzzy:
 51				e.handleFuzzyMode(ev)
 52			case ModeFind:
 53				e.handleFindMode(ev)
 54			case ModeVisual:
 55				e.handleVisualMode(ev)
 56			case ModeVisualLine:
 57				e.handleVisualLineMode(ev)
 58			case ModeVisualBlock:
 59				e.handleVisualBlockMode(ev)
 60			case ModeReplace:
 61				e.handleReplaceMode(ev)
 62			case ModeConfirm:
 63				e.handleConfirmMode(ev)
 64			}
 65		} else if ev.Type == termbox.EventMouse {
 66			e.handleMouseEvent(ev)
 67		}
 68	}
 69}
 70
 71// handleNormalMode processes keyboard input when the editor is in Normal mode.
 72func (e *Editor) handleNormalMode(ev termbox.Event) {
 73	// Escape clears any pending multi-key commands or secondary cursors.
 74	if ev.Key == termbox.KeyEsc {
 75		b := e.activeBuffer()
 76		if b != nil && len(b.cursors) > 1 {
 77			e.clearSecondaryCursors()
 78			e.pendingKey = 0
 79			e.message = "Cleared secondary cursors"
 80			return
 81		}
 82		e.pendingKey = 0
 83		return
 84	}
 85
 86	switch ev.Key {
 87	case termbox.KeyArrowLeft:
 88		e.moveCursor(-1, 0)
 89	case termbox.KeyArrowRight:
 90		e.moveCursor(1, 0)
 91	case termbox.KeyArrowUp:
 92		if ev.Mod != 0 {
 93			e.addCursorAbove()
 94		} else {
 95			e.moveCursor(0, -1)
 96		}
 97	case termbox.KeyArrowDown:
 98		if ev.Mod != 0 {
 99			e.addCursorBelow()
100		} else {
101			e.moveCursor(0, 1)
102		}
103	case termbox.KeyCtrlX:
104		e.addCursorBelow()
105	case termbox.KeyCtrlP:
106		e.prevBuffer()
107	case termbox.KeyCtrlN:
108		e.nextBuffer()
109	case termbox.KeyCtrlO:
110		e.jumpBack()
111	case termbox.KeyCtrlI:
112		e.jumpForward()
113	case termbox.KeyCtrlV:
114		b := e.activeBuffer()
115		if b != nil {
116			e.visualStartX = b.PrimaryCursor().X
117			e.visualStartY = b.PrimaryCursor().Y
118		}
119		e.mode = ModeVisualBlock
120	case termbox.KeyCtrlK:
121		e.triggerHover()
122	}
123
124	// Prevent key event fallthrough.
125	if ev.Key != 0 {
126		return
127	}
128
129	switch ev.Ch {
130	case 'i':
131		e.saveState()
132		e.mode = ModeInsert
133		e.introDismissed = true
134	case 'a':
135		e.saveState()
136		e.moveCursor(1, 0)
137		e.mode = ModeInsert
138		e.introDismissed = true
139	case 'A':
140		e.saveState()
141		e.jumpToLineEnd()
142		e.mode = ModeInsert
143		e.introDismissed = true
144	case 'I':
145		e.saveState()
146		e.jumpToFirstNonBlank()
147		e.mode = ModeInsert
148		e.introDismissed = true
149	case 'o':
150		e.saveState()
151		e.insertLineBelow()
152		e.mode = ModeInsert
153		e.introDismissed = true
154	case 'O':
155		e.saveState()
156		e.insertLineAbove()
157		e.mode = ModeInsert
158		e.introDismissed = true
159	case ']':
160		e.pushJump()
161		e.jumpToNextEmptyLine()
162	case '}':
163		e.pushJump()
164		e.jumpToBottom()
165	case 'v':
166		b := e.activeBuffer()
167		if b != nil {
168			e.visualStartX = b.PrimaryCursor().X
169			e.visualStartY = b.PrimaryCursor().Y
170		}
171		e.mode = ModeVisual
172	case 'V':
173		b := e.activeBuffer()
174		if b != nil {
175			e.visualStartX = b.PrimaryCursor().X
176			e.visualStartY = b.PrimaryCursor().Y
177		}
178		e.mode = ModeVisualLine
179	case ':':
180		e.mode = ModeCommand
181		e.commandBuffer = []rune{}
182		e.commandCursorX = 0
183	case '/':
184		e.findSavedSearch = e.lastSearch
185		e.mode = ModeFind
186		e.findBuffer = []rune{}
187	case Config.LeaderKey:
188		e.pendingKey = Config.LeaderKey
189	case 'l':
190		if e.pendingKey == Config.LeaderKey {
191			e.toggleDebugWindow()
192			e.pendingKey = 0
193		}
194	case 'w':
195		if e.pendingKey == 'd' {
196			e.saveState()
197			e.deleteWord(true)
198			e.checkDiagnostics()
199			e.pendingKey = 0
200		} else if e.pendingKey == 'c' {
201			e.saveState()
202			e.changeWord()
203			e.checkDiagnostics()
204			e.pendingKey = 0
205		} else if e.pendingKey == Config.LeaderKey {
206			e.startWarningsFuzzyFinder()
207			e.pendingKey = 0
208		} else {
209			e.moveWordForward()
210		}
211	case 'q':
212		if e.pendingKey == 'z' {
213			e.formatText()
214			e.checkDiagnostics()
215			e.pendingKey = 0
216		} else if e.pendingKey == Config.LeaderKey {
217			e.lastSearch = ""
218			e.pendingKey = 0
219		} else {
220			e.moveWordBackward()
221		}
222	case 'Q':
223		e.jumpToFirstNonBlank()
224	case 'W':
225		e.jumpToLineEnd()
226	case 'g':
227		e.pendingKey = 'g'
228	case 'j':
229		e.saveState()
230		e.JoinLines()
231		e.checkDiagnostics()
232	case 'f':
233		if e.pendingKey == 'g' {
234			e.gotoFile()
235			e.pendingKey = 0
236		}
237	case 'd':
238		if e.pendingKey == Config.LeaderKey {
239			e.deleteCurrentBuffer()
240			e.pendingKey = 0
241		} else if e.pendingKey == 'd' {
242			e.saveState()
243			e.deleteLine()
244			e.checkDiagnostics()
245			e.pendingKey = 0
246		} else if e.pendingKey == 'g' {
247			e.gotoDefinition()
248			e.pendingKey = 0
249		} else {
250			e.pendingKey = 'd'
251		}
252	case 'y':
253		e.yankLine()
254		e.message = "Line yanked"
255	case 'x':
256		if e.pendingKey == 'z' {
257			e.saveState()
258			e.toggleCommentLine()
259			e.checkDiagnostics()
260			e.pendingKey = 0
261		} else {
262			e.saveState()
263			e.DeleteChar()
264			e.checkDiagnostics()
265			e.pendingKey = 0
266		}
267	case 'z':
268		if e.pendingKey == 'z' {
269			e.centerScreen()
270			e.pendingKey = 0
271		} else {
272			e.pendingKey = 'z'
273		}
274	case 'c':
275		if e.pendingKey == 'd' {
276			e.saveState()
277			e.DeleteChar()
278			e.checkDiagnostics()
279			e.pendingKey = 0
280		} else if e.pendingKey == 'c' {
281			e.saveState()
282			e.changeCharacter()
283			e.checkDiagnostics()
284			e.pendingKey = 0
285		} else {
286			e.pendingKey = 'c'
287		}
288	case 'C':
289		e.saveState()
290		e.changeToEndOfLine()
291		e.checkDiagnostics()
292		e.pendingKey = 0
293	case 'D':
294		e.saveState()
295		e.deleteToEndOfLine()
296		e.checkDiagnostics()
297		e.pendingKey = 0
298	case '(':
299		if e.pendingKey == 'c' {
300			e.saveState()
301			e.changeInside('(', ')')
302			e.checkDiagnostics()
303			e.pendingKey = 0
304		} else if e.pendingKey == 'd' {
305			e.saveState()
306			e.deleteInside('(', ')')
307			e.checkDiagnostics()
308			e.pendingKey = 0
309		}
310	case '[':
311		if e.pendingKey == 'c' {
312			e.saveState()
313			e.changeInside('[', ']')
314			e.checkDiagnostics()
315			e.pendingKey = 0
316		} else if e.pendingKey == 'd' {
317			e.saveState()
318			e.deleteInside('[', ']')
319			e.checkDiagnostics()
320			e.pendingKey = 0
321		} else {
322			e.pushJump()
323			e.jumpToPrevEmptyLine()
324		}
325	case '{':
326		if e.pendingKey == 'c' {
327			e.saveState()
328			e.changeInside('{', '}')
329			e.checkDiagnostics()
330			e.pendingKey = 0
331		} else if e.pendingKey == 'd' {
332			e.saveState()
333			e.deleteInside('{', '}')
334			e.checkDiagnostics()
335			e.pendingKey = 0
336		} else {
337			e.pushJump()
338			e.jumpToTop()
339		}
340	case '\'':
341		if e.pendingKey == 'c' {
342			e.saveState()
343			e.changeInside('\'', '\'')
344			e.checkDiagnostics()
345			e.pendingKey = 0
346		} else if e.pendingKey == 'd' {
347			e.saveState()
348			e.deleteInside('\'', '\'')
349			e.checkDiagnostics()
350			e.pendingKey = 0
351		}
352	case '"':
353		if e.pendingKey == 'c' {
354			e.saveState()
355			e.changeInside('"', '"')
356			e.checkDiagnostics()
357			e.pendingKey = 0
358		} else if e.pendingKey == 'd' {
359			e.saveState()
360			e.deleteInside('"', '"')
361			e.checkDiagnostics()
362			e.pendingKey = 0
363		}
364	case 's':
365		e.saveState()
366		e.changeCharacter()
367		e.checkDiagnostics()
368		e.pendingKey = 0
369	case 'n':
370		e.findNext()
371		e.centerCursor()
372	case 'N':
373		e.findPrev()
374		e.centerCursor()
375	case 'u':
376		e.undo()
377		e.checkDiagnostics()
378		e.pendingKey = 0
379	case 'U':
380		e.redo()
381		e.checkDiagnostics()
382		e.pendingKey = 0
383	case 'p':
384		if e.pendingKey == Config.LeaderKey {
385			e.startFileFuzzyFinder()
386			e.pendingKey = 0
387		} else {
388			e.saveState()
389			e.pasteLine()
390			e.checkDiagnostics()
391			e.pendingKey = 0
392		}
393	case 'b':
394		if e.pendingKey == Config.LeaderKey {
395			e.startBufferFuzzyFinder()
396			e.pendingKey = 0
397		}
398	case 'P':
399		if e.pendingKey == Config.LeaderKey {
400			e.pendingKey = 0
401		} else {
402			e.saveState()
403			e.pasteLineAbove()
404			e.checkDiagnostics()
405			e.pendingKey = 0
406		}
407	default:
408		e.pendingKey = 0
409	}
410}
411
412// handleInsertMode processes keyboard input when the editor is in Insert mode.
413func (e *Editor) handleInsertMode(ev termbox.Event) {
414	if e.showAutocomplete {
415		switch ev.Key {
416		case termbox.KeyArrowUp:
417			e.autocompleteIndex--
418			if e.autocompleteIndex < 0 {
419				e.autocompleteIndex = len(e.autocompleteItems) - 1
420			}
421			// Adjust scroll to keep selection visible
422			if e.autocompleteIndex < e.autocompleteScroll {
423				e.autocompleteScroll = e.autocompleteIndex
424			}
425			if e.autocompleteIndex >= e.autocompleteScroll+10 {
426				e.autocompleteScroll = e.autocompleteIndex - 9
427			}
428			return
429		case termbox.KeyArrowDown:
430			e.autocompleteIndex++
431			if e.autocompleteIndex >= len(e.autocompleteItems) {
432				e.autocompleteIndex = 0
433			}
434			// Adjust scroll to keep selection visible
435			if e.autocompleteIndex < e.autocompleteScroll {
436				e.autocompleteScroll = e.autocompleteIndex
437			}
438			if e.autocompleteIndex >= e.autocompleteScroll+10 {
439				e.autocompleteScroll = e.autocompleteIndex - 9
440			}
441			return
442		case termbox.KeyEnter:
443			e.insertCompletion(e.autocompleteItems[e.autocompleteIndex])
444			return
445		case termbox.KeyEsc:
446			e.showAutocomplete = false
447			return
448		}
449	}
450
451	switch ev.Key {
452	case termbox.KeyEsc:
453		// Return to Normal mode and trigger a diagnostic check.
454		e.mode = ModeNormal
455		e.checkDiagnostics()
456	case termbox.KeyEnter:
457		e.insertNewline()
458	case termbox.KeySpace:
459		e.insertRune(' ')
460	case termbox.KeyBackspace, termbox.KeyBackspace2:
461		e.backspace()
462		if e.showAutocomplete {
463			e.showAutocomplete = false
464		}
465	case termbox.KeyTab:
466		e.insertTab()
467		if e.showAutocomplete {
468			e.showAutocomplete = false
469		}
470	case termbox.KeyArrowLeft:
471		e.moveCursor(-1, 0)
472		if e.showAutocomplete {
473			e.showAutocomplete = false
474		}
475	case termbox.KeyArrowRight:
476		e.moveCursor(1, 0)
477		if e.showAutocomplete {
478			e.showAutocomplete = false
479		}
480	case termbox.KeyArrowUp:
481		e.moveCursor(0, -1)
482		if e.showAutocomplete {
483			e.showAutocomplete = false
484		}
485	case termbox.KeyArrowDown:
486		e.moveCursor(0, 1)
487		if e.showAutocomplete {
488			e.showAutocomplete = false
489		}
490	case termbox.KeyCtrlW:
491		e.deleteWordBackward()
492	case termbox.KeyCtrlN:
493		e.triggerAutocomplete()
494	default:
495		// If a character key was pressed, insert the character.
496		if ev.Ch != 0 {
497			e.insertRune(ev.Ch)
498			// Close autocomplete if user keeps typing.
499			if e.showAutocomplete {
500				e.showAutocomplete = false
501			}
502		}
503	}
504}
505
506// handleCommandMode processes keyboard input for the colon command line.
507func (e *Editor) handleCommandMode(ev termbox.Event) {
508	switch ev.Key {
509	case termbox.KeyEsc:
510		// Cancel command entry.
511		e.mode = ModeNormal
512		e.commandBuffer = []rune{}
513		e.commandCursorX = 0
514		e.commandHistoryIdx = -1
515		e.checkDiagnostics()
516	case termbox.KeyEnter:
517		// Execute the entered command and save to history if valid.
518		cmd := string(e.commandBuffer)
519		e.commands.HandleAndSaveToHistory(cmd)
520		e.commandHistoryIdx = -1
521	case termbox.KeyBackspace, termbox.KeyBackspace2:
522		if e.commandCursorX > 0 {
523			// Delete character before cursor
524			e.commandBuffer = append(e.commandBuffer[:e.commandCursorX-1], e.commandBuffer[e.commandCursorX:]...)
525			e.commandCursorX--
526		} else if len(e.commandBuffer) == 0 {
527			// If buffer is empty, backspace returns to Normal mode.
528			e.mode = ModeNormal
529		}
530		e.commandHistoryIdx = -1
531	case termbox.KeySpace:
532		// Insert space at cursor position
533		e.commandBuffer = append(e.commandBuffer[:e.commandCursorX], append([]rune{' '}, e.commandBuffer[e.commandCursorX:]...)...)
534		e.commandCursorX++
535		e.commandHistoryIdx = -1
536	case termbox.KeyCtrlW:
537		e.deleteWordBackwardFromBuffer()
538		e.commandHistoryIdx = -1
539	case termbox.KeyArrowLeft:
540		// Move cursor left
541		if e.commandCursorX > 0 {
542			e.commandCursorX--
543		}
544	case termbox.KeyArrowRight:
545		// Move cursor right
546		if e.commandCursorX < len(e.commandBuffer) {
547			e.commandCursorX++
548		}
549	case termbox.KeyArrowUp:
550		// Navigate to previous command in history
551		e.commands.NavigateHistoryUp()
552	case termbox.KeyArrowDown:
553		// Navigate to next command in history
554		e.commands.NavigateHistoryDown()
555	default:
556		if ev.Ch != 0 {
557			// Insert character at cursor position
558			e.commandBuffer = append(e.commandBuffer[:e.commandCursorX], append([]rune{ev.Ch}, e.commandBuffer[e.commandCursorX:]...)...)
559			e.commandCursorX++
560			e.commandHistoryIdx = -1
561		}
562	}
563}
564
565// handleFuzzyMode processes input for the fuzzy finder (files or buffers).
566func (e *Editor) handleFuzzyMode(ev termbox.Event) {
567	switch ev.Key {
568	case termbox.KeyEsc:
569		e.mode = ModeNormal
570	case termbox.KeyEnter:
571		// Open the currently selected item in the list.
572		e.openSelectedFile()
573	case termbox.KeyArrowUp:
574		e.fuzzyMove(1)
575	case termbox.KeyArrowDown:
576		e.fuzzyMove(-1)
577	case termbox.KeyBackspace, termbox.KeyBackspace2:
578		if len(e.fuzzyBuffer) > 0 {
579			e.fuzzyBuffer = e.fuzzyBuffer[:len(e.fuzzyBuffer)-1]
580			e.updateFuzzyResults()
581		}
582	case termbox.KeySpace:
583		e.fuzzyBuffer = append(e.fuzzyBuffer, ' ')
584		e.updateFuzzyResults()
585	default:
586		// Update filter as user types.
587		if ev.Ch != 0 {
588			e.fuzzyBuffer = append(e.fuzzyBuffer, ev.Ch)
589			e.updateFuzzyResults()
590		}
591	}
592}
593
594// handleFindMode processes input for the in-file search (/).
595func (e *Editor) handleFindMode(ev termbox.Event) {
596	switch ev.Key {
597	case termbox.KeyEsc:
598		e.mode = ModeNormal
599		e.findBuffer = []rune{}
600		// Revert to the last successful search term.
601		e.lastSearch = e.findSavedSearch
602		e.checkDiagnostics()
603	case termbox.KeyEnter:
604		if len(e.findBuffer) > 0 {
605			e.lastSearch = string(e.findBuffer)
606			e.findNext()
607			e.centerCursor()
608		}
609		e.mode = ModeNormal
610	case termbox.KeyBackspace, termbox.KeyBackspace2:
611		if len(e.findBuffer) > 0 {
612			e.findBuffer = e.findBuffer[:len(e.findBuffer)-1]
613			e.lastSearch = string(e.findBuffer)
614		} else {
615			e.lastSearch = e.findSavedSearch
616		}
617	case termbox.KeySpace:
618		e.findBuffer = append(e.findBuffer, ' ')
619		e.lastSearch = string(e.findBuffer)
620	default:
621		// Incremental search: update e.lastSearch as the user types.
622		if ev.Ch != 0 {
623			e.findBuffer = append(e.findBuffer, ev.Ch)
624			e.lastSearch = string(e.findBuffer)
625		}
626	}
627}
628
629// handleVisualMode processes input for character-wise visual selection.
630func (e *Editor) handleVisualMode(ev termbox.Event) {
631	if ev.Key == termbox.KeyEsc {
632		// Exit visual mode and return to Normal.
633		e.mode = ModeNormal
634		return
635	}
636
637	switch ev.Key {
638	case termbox.KeyArrowLeft:
639		e.moveCursor(-1, 0)
640	case termbox.KeyArrowRight:
641		e.moveCursor(1, 0)
642	case termbox.KeyArrowUp:
643		e.moveCursor(0, -1)
644	case termbox.KeyArrowDown:
645		e.moveCursor(0, 1)
646	}
647
648	// Prevent key event fallthrough.
649	if ev.Key != 0 {
650		return
651	}
652
653	switch ev.Ch {
654	case Config.LeaderKey:
655		e.pendingKey = Config.LeaderKey
656	case 'w':
657		e.moveWordForward()
658	case 'q':
659		if e.pendingKey == 'z' {
660			e.formatText()
661			e.checkDiagnostics()
662			e.pendingKey = 0
663		} else {
664			e.moveWordBackward()
665		}
666	case 'y':
667		e.yankVisualSelection()
668		e.message = "Selection yanked"
669	case 'd':
670		e.saveState()
671		e.deleteVisualSelection()
672		e.checkDiagnostics()
673		e.message = "Selection deleted"
674	case 'x':
675		if e.pendingKey == 'z' {
676			e.saveState()
677			e.commentVisualSelection()
678			e.checkDiagnostics()
679			e.pendingKey = 0
680		} else {
681			e.saveState()
682			e.deleteVisualSelection()
683			e.checkDiagnostics()
684			e.message = "Selection deleted"
685		}
686	case 'p':
687		e.saveState()
688		e.pasteVisualSelection()
689		e.checkDiagnostics()
690	case 'c':
691		e.saveState()
692		e.changeVisualSelection()
693		e.checkDiagnostics()
694	case 'Q':
695		e.jumpToFirstNonBlank()
696	case 'W':
697		e.jumpToLineEnd()
698	case '~':
699		e.saveState()
700		e.ToggleCaseVisualSelection()
701		e.checkDiagnostics()
702	case 'o':
703		if e.pendingKey == Config.LeaderKey {
704			e.ollamaComplete()
705			e.pendingKey = 0
706		} else {
707			// Swap cursor and visual anchor
708			b := e.activeBuffer()
709			if b != nil {
710				tmpX, tmpY := b.PrimaryCursor().X, b.PrimaryCursor().Y
711				b.PrimaryCursor().X, b.PrimaryCursor().Y = e.visualStartX, e.visualStartY
712				e.visualStartX, e.visualStartY = tmpX, tmpY
713			}
714		}
715	case '{':
716		e.jumpToTop()
717	case '}':
718		e.jumpToBottom()
719	case '[':
720		e.jumpToPrevEmptyLine()
721	case ']':
722		e.jumpToNextEmptyLine()
723	case ':':
724		e.mode = ModeCommand
725		e.commandBuffer = []rune{}
726		e.commandCursorX = 0
727	case 'V':
728		e.mode = ModeVisualLine
729	case 'z':
730		e.pendingKey = 'z'
731	case 'R':
732		e.startReplaceMode()
733	}
734}
735
736func (e *Editor) handleVisualLineMode(ev termbox.Event) {
737	if ev.Key == termbox.KeyEsc {
738		e.mode = ModeNormal
739		return
740	}
741
742	switch ev.Key {
743	case termbox.KeyArrowLeft:
744		e.moveCursor(-1, 0)
745	case termbox.KeyArrowRight:
746		e.moveCursor(1, 0)
747	case termbox.KeyArrowUp:
748		e.moveCursor(0, -1)
749	case termbox.KeyArrowDown:
750		e.moveCursor(0, 1)
751	}
752
753	// Prevent key event fallthrough.
754	if ev.Key != 0 {
755		return
756	}
757
758	switch ev.Ch {
759	case Config.LeaderKey:
760		e.pendingKey = Config.LeaderKey
761	case 'w':
762		e.moveWordForward()
763	case 'q':
764		if e.pendingKey == 'z' {
765			e.formatText()
766			e.checkDiagnostics()
767			e.pendingKey = 0
768		} else {
769			e.moveWordBackward()
770		}
771	case 'y':
772		e.yankVisualSelection()
773		e.message = "Selection yanked"
774	case 'd':
775		e.saveState()
776		e.deleteVisualSelection()
777		e.checkDiagnostics()
778		e.message = "Selection deleted"
779	case 'x':
780		if e.pendingKey == 'z' {
781			e.saveState()
782			e.commentVisualSelection()
783			e.checkDiagnostics()
784			e.pendingKey = 0
785		} else {
786			e.saveState()
787			e.deleteVisualSelection()
788			e.checkDiagnostics()
789			e.message = "Selection deleted"
790		}
791	case 'p':
792		e.saveState()
793		e.pasteVisualSelection()
794		e.checkDiagnostics()
795	case 'c':
796		e.saveState()
797		e.changeVisualSelection()
798		e.checkDiagnostics()
799	case 'Q':
800		e.jumpToFirstNonBlank()
801	case 'W':
802		e.jumpToLineEnd()
803	case '~':
804		e.saveState()
805		e.ToggleCaseVisualSelection()
806		e.checkDiagnostics()
807	case 'o':
808		if e.pendingKey == Config.LeaderKey {
809			e.ollamaComplete()
810			e.pendingKey = 0
811		} else {
812			// Swap cursor and visual anchor
813			b := e.activeBuffer()
814			if b != nil {
815				tmpX, tmpY := b.PrimaryCursor().X, b.PrimaryCursor().Y
816				b.PrimaryCursor().X, b.PrimaryCursor().Y = e.visualStartX, e.visualStartY
817				e.visualStartX, e.visualStartY = tmpX, tmpY
818			}
819		}
820	case '{':
821		e.jumpToTop()
822	case '}':
823		e.jumpToBottom()
824	case '[':
825		e.jumpToPrevEmptyLine()
826	case ']':
827		e.jumpToNextEmptyLine()
828	case 'z':
829		e.pendingKey = 'z'
830	case 'v':
831		e.mode = ModeVisual
832	case 'V':
833		e.mode = ModeNormal
834	case 'R':
835		e.startReplaceMode()
836	}
837}
838
839// handleVisualBlockMode processes input for column-wise (rectangular) selection.
840func (e *Editor) handleVisualBlockMode(ev termbox.Event) {
841	if ev.Key == termbox.KeyEsc {
842		e.mode = ModeNormal
843		return
844	}
845
846	switch ev.Key {
847	case termbox.KeyArrowLeft:
848		e.moveCursor(-1, 0)
849	case termbox.KeyArrowRight:
850		e.moveCursor(1, 0)
851	case termbox.KeyArrowUp:
852		e.moveCursor(0, -1)
853	case termbox.KeyArrowDown:
854		e.moveCursor(0, 1)
855	}
856
857	// Prevent key event fallthrough.
858	if ev.Key != 0 {
859		return
860	}
861
862	switch ev.Ch {
863	case Config.LeaderKey:
864		e.pendingKey = Config.LeaderKey
865	case 'w':
866		e.moveWordForward()
867	case 'q':
868		if e.pendingKey == 'z' {
869			e.formatText()
870			e.checkDiagnostics()
871			e.pendingKey = 0
872		} else {
873			e.moveWordBackward()
874		}
875	case 'y':
876		e.yankVisualSelection()
877		e.message = "Selection yanked"
878	case 'd':
879		e.saveState()
880		e.deleteVisualSelection()
881		e.checkDiagnostics()
882		e.message = "Selection deleted"
883	case 'x':
884		if e.pendingKey == 'z' {
885			e.saveState()
886			e.commentVisualSelection()
887			e.checkDiagnostics()
888			e.pendingKey = 0
889		} else {
890			e.saveState()
891			e.deleteVisualSelection()
892			e.checkDiagnostics()
893			e.message = "Selection deleted"
894		}
895	case 'p':
896		e.saveState()
897		e.pasteVisualSelection()
898		e.checkDiagnostics()
899	case 'c':
900		e.saveState()
901		e.changeVisualSelection()
902		e.checkDiagnostics()
903	case 'Q':
904		e.jumpToFirstNonBlank()
905	case 'W':
906		e.jumpToLineEnd()
907	case '~':
908		e.saveState()
909		e.ToggleCaseVisualSelection()
910		e.checkDiagnostics()
911	case 'o':
912		if e.pendingKey == Config.LeaderKey {
913			e.ollamaComplete()
914			e.pendingKey = 0
915		} else {
916			// Swap cursor and visual anchor
917			b := e.activeBuffer()
918			if b != nil {
919				tmpX, tmpY := b.PrimaryCursor().X, b.PrimaryCursor().Y
920				b.PrimaryCursor().X, b.PrimaryCursor().Y = e.visualStartX, e.visualStartY
921				e.visualStartX, e.visualStartY = tmpX, tmpY
922			}
923		}
924	case '{':
925		e.jumpToTop()
926	case '}':
927		e.jumpToBottom()
928	case '[':
929		e.jumpToPrevEmptyLine()
930	case ']':
931		e.jumpToNextEmptyLine()
932	case 'z':
933		e.pendingKey = 'z'
934	case 'v':
935		e.mode = ModeVisual
936	case 'V':
937		e.mode = ModeVisualLine
938	case 'R':
939		e.startReplaceMode()
940	}
941}
942
943// handleMouseEvent handles simple mouse wheel scrolling.
944func (e *Editor) handleMouseEvent(ev termbox.Event) {
945	switch ev.Key {
946	case termbox.MouseWheelUp:
947		e.moveCursor(0, -1) // Scroll up by moving the cursor.
948	case termbox.MouseWheelDown:
949		e.moveCursor(0, 1) // Scroll down by moving the cursor.
950	}
951}
952
953// handleConfirmMode processes yes/no confirmations for dangerous actions (like overwriting files).
954func (e *Editor) handleConfirmMode(ev termbox.Event) {
955	if ev.Key == termbox.KeyEsc {
956		e.mode = ModeNormal
957		e.pendingConfirm = nil
958		e.message = "Cancelled"
959		return
960	}
961
962	if ev.Key == termbox.KeyEnter {
963		// Default Enter to "no/cancel" to avoid accidental execution.
964		e.mode = ModeNormal
965		e.pendingConfirm = nil
966		e.message = "Cancelled"
967		return
968	}
969
970	// Prevent key event fallthrough.
971	if ev.Key != 0 {
972		return
973	}
974
975	switch ev.Ch {
976	case 'y', 'Y':
977		if e.pendingConfirm != nil {
978			action := e.pendingConfirm
979			e.pendingConfirm = nil
980			e.mode = ModeNormal
981			action()
982		} else {
983			e.mode = ModeNormal
984		}
985	case 'n', 'N':
986		e.mode = ModeNormal
987		e.pendingConfirm = nil
988		e.message = "Cancelled"
989	}
990}