diff --git a/commandmap.go b/commandmap.go index f9e4aa3..e997b9a 100644 --- a/commandmap.go +++ b/commandmap.go @@ -5,6 +5,8 @@ import ( "fmt" "regexp" + "github.com/lmika/shellwords" + "github.com/lmika/ted/ui" ) @@ -60,20 +62,19 @@ func (cm *CommandMapping) KeyMapping(key rune) *Command { // Evaluate a command func (cm *CommandMapping) Eval(ctx *CommandContext, expr string) error { // TODO: Use propper expression language here - cmd := cm.Commands[expr] + toks := shellwords.Split(expr) + if len(toks) == 0 { + return nil + } + + cmd := cm.Commands[toks[0]] if cmd != nil { - return cmd.Do(ctx) + return cmd.Do(ctx.WithArgs(toks[1:])) } return fmt.Errorf("no such command: %v", expr) } -//func (cm *CommandMapping) DoEval(ctx *CommandContext, expr string) { -// if err := cm.Eval(ctx, expr); err != nil { -// ctx.ShowError(err) -// } -//} - // Registers the standard view navigation commands. These commands require the frame func (cm *CommandMapping) RegisterViewCommands() { cm.Define("move-down", "Moves the cursor down one row", "", gridNavOperation(func(grid *ui.Grid) { grid.MoveBy(0, 1) })) @@ -153,6 +154,34 @@ func (cm *CommandMapping) RegisterViewCommands() { } } }) + cm.Define("x-replace", "Performs a search and replace", "", func(ctx *CommandContext) error { + if len(ctx.Args()) != 2 { + return errors.New("Usage: x-replace MATCH REPLACEMENT") + } + + match := ctx.Args()[0] + repl := ctx.Args()[1] + + re, err := regexp.Compile(match) + if err != nil { + return fmt.Errorf("invalid regexp: %v", err) + } + + matchCount := 0 + height, width := ctx.ModelVC().Model().Dimensions() + for r := 0; r < height; r++ { + for c := 0; c < width; c++ { + cell := ctx.ModelVC().Model().CellValue(r, c) + if re.FindStringIndex(cell) != nil { + ctx.ModelVC().SetCellValue(r, c, re.ReplaceAllString(cell, repl)) + matchCount++ + } + } + } + + ctx.Frame().ShowMessage(fmt.Sprintf("Replaced %d matches", matchCount)) + return nil + }) cm.Define("open-right", "Inserts a column to the right of the curser", "", func(ctx *CommandContext) error { grid := ctx.Frame().Grid() diff --git a/go.mod b/go.mod index ff3693c..1aaa82d 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/lmika/ted go 1.15 require ( + github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe github.com/mattn/go-runewidth v0.0.9 // indirect github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1 ) diff --git a/go.sum b/go.sum index 72bf7b1..87c946c 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe h1:1UXS/6OFkbi6JrihPykmYO1VtsABB02QQ+YmYYzTY18= +github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe/go.mod h1:qpdOkLougV5Yry4Px9f1w1pNMavcr6Z67VW5Ro+vW5I= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1 h1:lh3PyZvY+B9nFliSGTn5uFuqQQJGuNrD0MLCokv09ag= diff --git a/session.go b/session.go index d36d296..a8154b7 100644 --- a/session.go +++ b/session.go @@ -1,8 +1,9 @@ package main import ( - "github.com/lmika/ted/ui" "regexp" + + "github.com/lmika/ted/ui" ) // The session is responsible for managing the UI and the model and handling @@ -15,7 +16,7 @@ type Session struct { UIManager *ui.Ui modelController *ModelViewCtrl - LastSearch *regexp.Regexp + LastSearch *regexp.Regexp } func NewSession(uiManager *ui.Ui, frame *Frame, source ModelSource) *Session { @@ -61,7 +62,7 @@ func (session *Session) KeyPressed(key rune, mod int) { cmd := session.Commands.KeyMapping(key) if cmd != nil { - err := cmd.Do(&CommandContext{session}) + err := cmd.Do(&CommandContext{session, nil}) if err != nil { session.Frame.ShowMessage(err.Error()) } @@ -71,6 +72,18 @@ func (session *Session) KeyPressed(key rune, mod int) { // The command context used by the session type CommandContext struct { session *Session + args []string +} + +func (scc *CommandContext) WithArgs(args []string) *CommandContext { + return &CommandContext{ + session: scc.session, + args: args, + } +} + +func (scc *CommandContext) Args() []string { + return scc.args } func (scc *CommandContext) ModelVC() *ModelViewCtrl { @@ -125,11 +138,11 @@ func (sgm *SessionGridModel) CellAttributes(x int, y int) (fg, bg ui.Attribute) } else if colAttrs.Marker != MarkerNone { return markerAttributes[colAttrs.Marker], 0 } - return 0,0 + return 0, 0 } -var markerAttributes = map[Marker]ui.Attribute { - MarkerRed: ui.ColorRed, +var markerAttributes = map[Marker]ui.Attribute{ + MarkerRed: ui.ColorRed, MarkerGreen: ui.ColorGreen, - MarkerBlue: ui.ColorBlue, -} \ No newline at end of file + MarkerBlue: ui.ColorBlue, +} diff --git a/ui/stdcomps.go b/ui/stdcomps.go index b25c784..7c7090b 100644 --- a/ui/stdcomps.go +++ b/ui/stdcomps.go @@ -4,7 +4,7 @@ package ui import ( "unicode" - ) +) // A text component. This simply renders a text string. type TextView struct { @@ -124,6 +124,8 @@ func (te *TextEntry) KeyPressed(key rune, mod int) { if mod&ModKeyAlt != 0 { te.backspaceWhile(unicode.IsSpace) te.backspaceWhile(func(r rune) bool { return !unicode.IsSpace(r) }) + } else if te.cursorOffset == 0 { + te.cancelAndExit() } else { te.backspace() } @@ -136,14 +138,18 @@ func (te *TextEntry) KeyPressed(key rune, mod int) { te.OnEntry(te.value) } } else if key == KeyCtrlC { - if te.OnCancel != nil { - te.OnCancel() - } + te.cancelAndExit() } //panic(fmt.Sprintf("Entered key: '%x', mod: '%x'", key, mod)) } +func (te *TextEntry) cancelAndExit() { + if te.OnCancel != nil { + te.OnCancel() + } +} + // Backspace func (te *TextEntry) backspace() { te.removeCharAtPos(te.cursorOffset - 1)