@@ -45,6 +45,16 @@ type EditorPane struct {
4545 height int // Pane height
4646 focused bool // Whether this pane is focused
4747 diffMarkers map [int ]string // Tracks red/green line styling for diffs
48+ undoStack []editorSnapshot // Undo history
49+ redoStack []editorSnapshot // Redo history
50+ pendingAltD bool // Waiting for second key after Alt+D
51+ }
52+
53+ // editorSnapshot stores editor state for undo/redo
54+ type editorSnapshot struct {
55+ content string
56+ cursorLine int
57+ cursorCol int
4858}
4959
5060// NewEditorPane creates a new editor pane.
@@ -271,8 +281,68 @@ func (e *EditorPane) Update(msg tea.Msg) tea.Cmd {
271281// - tea.Cmd: Command to execute (currently always nil)
272282func (e * EditorPane ) handleKeyPress (msg tea.KeyMsg ) tea.Cmd {
273283 lines := strings .Split (e .content , "\n " )
284+ keyStr := msg .String ()
285+
286+ // Handle pending Alt+D sequence (waiting for d/w/number)
287+ if e .pendingAltD {
288+ e .pendingAltD = false
289+ // Accept both plain and alt+ variants (user may still hold Alt)
290+ switch {
291+ case keyStr == "d" || keyStr == "D" || keyStr == "alt+d" || keyStr == "alt+D" :
292+ e .deleteLine ()
293+ return nil
294+ case keyStr == "w" || keyStr == "W" || keyStr == "alt+w" || keyStr == "alt+W" :
295+ e .deleteWord ()
296+ return nil
297+ case keyStr >= "1" && keyStr <= "9" :
298+ n := int (keyStr [0 ] - '0' )
299+ e .deleteLines (n )
300+ return nil
301+ default :
302+ if len (keyStr ) == 5 && keyStr [:4 ] == "alt+" && keyStr [4 ] >= '1' && keyStr [4 ] <= '9' {
303+ n := int (keyStr [4 ] - '0' )
304+ e .deleteLines (n )
305+ return nil
306+ }
307+ return nil
308+ }
309+ }
274310
275- switch msg .String () {
311+ switch keyStr {
312+ case "alt+d" :
313+ // Start Alt+D sequence, wait for next key
314+ e .pendingAltD = true
315+ return nil
316+ // Direct Alt shortcuts for delete (single press alternatives)
317+ case "alt+l" :
318+ // Alt+L = delete line (single-key alternative to Alt+D,D)
319+ e .deleteLine ()
320+ return nil
321+ case "alt+w" :
322+ // Alt+W = delete word (single-key alternative to Alt+D,W)
323+ e .deleteWord ()
324+ return nil
325+ case "alt+u" :
326+ e .undo ()
327+ return nil
328+ case "alt+r" :
329+ e .redo ()
330+ return nil
331+ case "alt+g" :
332+ // Go to end of file
333+ e .cursorLine = len (lines ) - 1
334+ if e .cursorLine < 0 {
335+ e .cursorLine = 0
336+ }
337+ e .cursorCol = len (lines [e .cursorLine ])
338+ e .adjustScroll ()
339+ return nil
340+ case "alt+h" :
341+ // Go to top of file
342+ e .cursorLine = 0
343+ e .cursorCol = 0
344+ e .adjustScroll ()
345+ return nil
276346 case "up" :
277347 if e .cursorLine > 0 {
278348 e .cursorLine --
@@ -363,6 +433,7 @@ func (e *EditorPane) shiftMarkers(fromLine int, amount int) {
363433// Parameters:
364434// - char: The character to insert
365435func (e * EditorPane ) insertChar (char string ) {
436+ e .saveSnapshot ()
366437 lines := strings .Split (e .content , "\n " )
367438 if e .cursorLine >= len (lines ) {
368439 lines = append (lines , "" )
@@ -388,6 +459,7 @@ func (e *EditorPane) insertChar(char string) {
388459// Moves cursor to the beginning of the new line.
389460// Updates the modified flag and adjusts scroll.
390461func (e * EditorPane ) insertNewline () {
462+ e .saveSnapshot ()
391463 lines := strings .Split (e .content , "\n " )
392464 if e .cursorLine >= len (lines ) {
393465 lines = append (lines , "" )
@@ -427,6 +499,7 @@ func (e *EditorPane) insertNewline() {
427499// If at the beginning of a line, merges with the previous line.
428500// Updates the modified flag and adjusts scroll.
429501func (e * EditorPane ) deleteChar () {
502+ e .saveSnapshot ()
430503 lines := strings .Split (e .content , "\n " )
431504 if e .cursorLine >= len (lines ) {
432505 return
@@ -468,6 +541,7 @@ func (e *EditorPane) deleteChar() {
468541// If at the end of a line, merges with the next line.
469542// Updates the modified flag.
470543func (e * EditorPane ) deleteNextChar () {
544+ e .saveSnapshot ()
471545 lines := strings .Split (e .content , "\n " )
472546 if e .cursorLine >= len (lines ) {
473547 return
@@ -501,6 +575,152 @@ func (e *EditorPane) deleteNextChar() {
501575 }
502576}
503577
578+ // saveSnapshot pushes current state onto the undo stack and clears redo
579+ func (e * EditorPane ) saveSnapshot () {
580+ e .undoStack = append (e .undoStack , editorSnapshot {
581+ content : e .content ,
582+ cursorLine : e .cursorLine ,
583+ cursorCol : e .cursorCol ,
584+ })
585+ e .redoStack = nil
586+ }
587+
588+ // undo restores the previous editor state
589+ func (e * EditorPane ) undo () {
590+ if len (e .undoStack ) == 0 {
591+ return
592+ }
593+ // Push current state to redo
594+ e .redoStack = append (e .redoStack , editorSnapshot {
595+ content : e .content ,
596+ cursorLine : e .cursorLine ,
597+ cursorCol : e .cursorCol ,
598+ })
599+ snap := e .undoStack [len (e .undoStack )- 1 ]
600+ e .undoStack = e .undoStack [:len (e .undoStack )- 1 ]
601+ e .content = snap .content
602+ e .cursorLine = snap .cursorLine
603+ e .cursorCol = snap .cursorCol
604+ if e .currentFile != nil {
605+ e .currentFile .IsModified = (e .content != e .originalContent ) || len (e .diffMarkers ) > 0
606+ }
607+ e .adjustScroll ()
608+ }
609+
610+ // redo restores the next editor state
611+ func (e * EditorPane ) redo () {
612+ if len (e .redoStack ) == 0 {
613+ return
614+ }
615+ e .undoStack = append (e .undoStack , editorSnapshot {
616+ content : e .content ,
617+ cursorLine : e .cursorLine ,
618+ cursorCol : e .cursorCol ,
619+ })
620+ snap := e .redoStack [len (e .redoStack )- 1 ]
621+ e .redoStack = e .redoStack [:len (e .redoStack )- 1 ]
622+ e .content = snap .content
623+ e .cursorLine = snap .cursorLine
624+ e .cursorCol = snap .cursorCol
625+ if e .currentFile != nil {
626+ e .currentFile .IsModified = (e .content != e .originalContent ) || len (e .diffMarkers ) > 0
627+ }
628+ e .adjustScroll ()
629+ }
630+
631+ // deleteLine deletes the current line
632+ func (e * EditorPane ) deleteLine () {
633+ e .saveSnapshot ()
634+ lines := strings .Split (e .content , "\n " )
635+ if len (lines ) == 0 {
636+ return
637+ }
638+ if e .cursorLine >= len (lines ) {
639+ e .cursorLine = len (lines ) - 1
640+ }
641+ lines = append (lines [:e .cursorLine ], lines [e .cursorLine + 1 :]... )
642+ if len (lines ) == 0 {
643+ lines = []string {"" }
644+ }
645+ e .content = strings .Join (lines , "\n " )
646+ if e .cursorLine >= len (lines ) {
647+ e .cursorLine = len (lines ) - 1
648+ }
649+ if e .cursorLine < 0 {
650+ e .cursorLine = 0
651+ }
652+ if e .cursorCol > len (strings .Split (e .content , "\n " )[e .cursorLine ]) {
653+ e .cursorCol = len (strings .Split (e .content , "\n " )[e .cursorLine ])
654+ }
655+ if e .currentFile != nil {
656+ e .currentFile .IsModified = (e .content != e .originalContent ) || len (e .diffMarkers ) > 0
657+ }
658+ e .adjustScroll ()
659+ }
660+
661+ // deleteLines deletes n lines starting from the current line
662+ func (e * EditorPane ) deleteLines (n int ) {
663+ e .saveSnapshot ()
664+ lines := strings .Split (e .content , "\n " )
665+ if len (lines ) == 0 || n <= 0 {
666+ return
667+ }
668+ if e .cursorLine >= len (lines ) {
669+ e .cursorLine = len (lines ) - 1
670+ }
671+ end := e .cursorLine + n
672+ if end > len (lines ) {
673+ end = len (lines )
674+ }
675+ lines = append (lines [:e .cursorLine ], lines [end :]... )
676+ if len (lines ) == 0 {
677+ lines = []string {"" }
678+ }
679+ e .content = strings .Join (lines , "\n " )
680+ if e .cursorLine >= len (lines ) {
681+ e .cursorLine = len (lines ) - 1
682+ }
683+ if e .cursorLine < 0 {
684+ e .cursorLine = 0
685+ }
686+ curLines := strings .Split (e .content , "\n " )
687+ if e .cursorCol > len (curLines [e .cursorLine ]) {
688+ e .cursorCol = len (curLines [e .cursorLine ])
689+ }
690+ if e .currentFile != nil {
691+ e .currentFile .IsModified = (e .content != e .originalContent ) || len (e .diffMarkers ) > 0
692+ }
693+ e .adjustScroll ()
694+ }
695+
696+ // deleteWord deletes from cursor to end of current word (or next word boundary)
697+ func (e * EditorPane ) deleteWord () {
698+ e .saveSnapshot ()
699+ lines := strings .Split (e .content , "\n " )
700+ if e .cursorLine >= len (lines ) {
701+ return
702+ }
703+ line := lines [e .cursorLine ]
704+ if e .cursorCol >= len (line ) {
705+ return
706+ }
707+ // Find end of word: skip non-spaces, then skip spaces
708+ pos := e .cursorCol
709+ // Skip current word characters
710+ for pos < len (line ) && line [pos ] != ' ' && line [pos ] != '\t' {
711+ pos ++
712+ }
713+ // Skip trailing whitespace
714+ for pos < len (line ) && (line [pos ] == ' ' || line [pos ] == '\t' ) {
715+ pos ++
716+ }
717+ lines [e .cursorLine ] = line [:e .cursorCol ] + line [pos :]
718+ e .content = strings .Join (lines , "\n " )
719+ if e .currentFile != nil {
720+ e .currentFile .IsModified = (e .content != e .originalContent ) || len (e .diffMarkers ) > 0
721+ }
722+ }
723+
504724// adjustScroll adjusts the scroll offset to keep cursor visible.
505725// Scrolls down if cursor is below visible area.
506726// Scrolls up if cursor is above visible area.
0 commit comments