Files
task-master/internal/command/executor.go
T
SkrueckenandCursor 73216ec4ae Persist undo/redo history across runs with a 20-entry cap.
Serialize create/update/delete commands to DataDir/history.json and reload them on startup via OpenHistory.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-19 10:25:07 +02:00

150 lines
3.1 KiB
Go

package command
import (
"context"
"fmt"
"sync"
"task/internal/ticket"
)
// Executor runs commands and supports undo/redo of successful ones.
type Executor interface {
Do(cmd Command) error
Undo() error
Redo() error
CanUndo() bool
CanRedo() bool
LastUndoName() string
LastRedoName() string
}
// History is an undo/redo stack for Command values, optionally persisted to disk.
type History struct {
mu sync.Mutex
undoS []Command
redoS []Command
path string
store ticket.Store
max int
}
// NewHistory creates an empty in-memory command history (cap MaxHistory).
func NewHistory() *History {
return &History{max: MaxHistory}
}
// OpenHistory loads a persisted history from path (DataDir/history.json) or
// starts empty if the file is missing. Subsequent Do/Undo/Redo write back.
func OpenHistory(path string, store ticket.Store) (*History, error) {
h := &History{
path: path,
store: store,
max: MaxHistory,
}
undo, redo, err := loadHistoryFile(path, store)
if err != nil {
return nil, err
}
h.undoS = undo
h.redoS = redo
return h, nil
}
// Do executes cmd and pushes it onto the undo stack. The redo stack is cleared.
func (h *History) Do(cmd Command) error {
if err := cmd.Execute(context.Background()); err != nil {
return err
}
h.mu.Lock()
defer h.mu.Unlock()
h.undoS = append(h.undoS, cmd)
h.undoS = trimStack(h.undoS, h.max)
h.redoS = nil
return h.persistLocked()
}
// Undo reverses the last successful command.
func (h *History) Undo() error {
h.mu.Lock()
if len(h.undoS) == 0 {
h.mu.Unlock()
return fmt.Errorf("nothing to undo")
}
cmd := h.undoS[len(h.undoS)-1]
h.undoS = h.undoS[:len(h.undoS)-1]
h.mu.Unlock()
if err := cmd.Undo(context.Background()); err != nil {
h.mu.Lock()
h.undoS = append(h.undoS, cmd)
h.mu.Unlock()
return err
}
h.mu.Lock()
defer h.mu.Unlock()
h.redoS = append(h.redoS, cmd)
h.redoS = trimStack(h.redoS, h.max)
return h.persistLocked()
}
// Redo re-applies the last undone command.
func (h *History) Redo() error {
h.mu.Lock()
if len(h.redoS) == 0 {
h.mu.Unlock()
return fmt.Errorf("nothing to redo")
}
cmd := h.redoS[len(h.redoS)-1]
h.redoS = h.redoS[:len(h.redoS)-1]
h.mu.Unlock()
if err := cmd.Execute(context.Background()); err != nil {
h.mu.Lock()
h.redoS = append(h.redoS, cmd)
h.mu.Unlock()
return err
}
h.mu.Lock()
defer h.mu.Unlock()
h.undoS = append(h.undoS, cmd)
h.undoS = trimStack(h.undoS, h.max)
return h.persistLocked()
}
// CanUndo reports whether Undo would succeed.
func (h *History) CanUndo() bool {
h.mu.Lock()
defer h.mu.Unlock()
return len(h.undoS) > 0
}
// CanRedo reports whether Redo would succeed.
func (h *History) CanRedo() bool {
h.mu.Lock()
defer h.mu.Unlock()
return len(h.redoS) > 0
}
// LastUndoName returns the name of the next undo command, or "".
func (h *History) LastUndoName() string {
h.mu.Lock()
defer h.mu.Unlock()
if len(h.undoS) == 0 {
return ""
}
return h.undoS[len(h.undoS)-1].Name()
}
// LastRedoName returns the name of the next redo command, or "".
func (h *History) LastRedoName() string {
h.mu.Lock()
defer h.mu.Unlock()
if len(h.redoS) == 0 {
return ""
}
return h.redoS[len(h.redoS)-1].Name()
}