Skip to content

Commit 0bbf8bf

Browse files
committed
implemented new editor shortcuts
1 parent 4aa0423 commit 0bbf8bf

3 files changed

Lines changed: 317 additions & 1 deletion

File tree

internal/ui/app.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ type App struct {
6565
showFilePicker bool // Whether file picker dialog is showing
6666
showBackupPicker bool // Whether backup picker dialog is showing
6767
showHelp bool // Whether help dialog is showing
68+
showEditorHelp bool // Whether editor shortcuts dialog is showing
6869
filePromptBuffer string // Buffer for file name input
6970
fileList []string // List of files for picker
7071
backupList []string // List of backups for picker
@@ -370,6 +371,15 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
370371
return a, nil
371372
}
372373

374+
// Handle editor shortcuts dialog
375+
if a.showEditorHelp {
376+
switch msg.String() {
377+
case "esc", "ctrl+e", "q":
378+
a.showEditorHelp = false
379+
}
380+
return a, nil
381+
}
382+
373383
// Handle file picker dialog
374384
if a.showFilePicker {
375385
switch msg.String() {
@@ -640,6 +650,11 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
640650
a.showHelp = !a.showHelp
641651
return a, nil
642652

653+
case "ctrl+e":
654+
// Toggle editor shortcuts help
655+
a.showEditorHelp = !a.showEditorHelp
656+
return a, nil
657+
643658
case "ctrl+a":
644659
// Insert entire last assistant response into editor file
645660
response := a.aiPane.GetLastAssistantResponse()
@@ -902,6 +917,11 @@ func (a *App) View() string {
902917
return a.renderHelpDialog()
903918
}
904919

920+
// Show editor shortcuts dialog if needed
921+
if a.showEditorHelp {
922+
return a.renderEditorHelpDialog()
923+
}
924+
905925
// Show backup picker dialog if needed
906926
if a.showBackupPicker {
907927
pickerStyle := lipgloss.NewStyle().

internal/ui/editor.go

Lines changed: 221 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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)
272282
func (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
365435
func (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.
390461
func (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.
429501
func (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.
470543
func (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

Comments
 (0)