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}