From a3885bfb1237597ce1ae97a9c013c864cdbb5ce8 Mon Sep 17 00:00:00 2001 From: Zachary Yedidia Date: Tue, 15 Jan 2019 22:45:28 -0500 Subject: [PATCH] Add search and replace --- cmd/micro/action/actions.go | 12 ++-- cmd/micro/action/command.go | 111 ++++++++++++++++++++++++++++++++ cmd/micro/action/infohandler.go | 11 ++-- cmd/micro/buffer/search.go | 100 ++++++++++++++++++++++++---- cmd/micro/info/infobuffer.go | 5 -- 5 files changed, 212 insertions(+), 27 deletions(-) diff --git a/cmd/micro/action/actions.go b/cmd/micro/action/actions.go index ade2f0d5..588437fb 100644 --- a/cmd/micro/action/actions.go +++ b/cmd/micro/action/actions.go @@ -552,7 +552,7 @@ func (h *BufHandler) SaveAs() bool { func (h *BufHandler) Find() bool { InfoBar.Prompt("Find: ", "", "Find", func(resp string) { // Event callback - match, found, _ := h.Buf.FindNext(resp, h.Cursor.Loc, true) + match, found, _ := h.Buf.FindNext(resp, h.Buf.Start(), h.Buf.End(), h.Cursor.Loc, true, true) if found { h.Cursor.SetSelectionStart(match[0]) h.Cursor.SetSelectionEnd(match[1]) @@ -564,7 +564,7 @@ func (h *BufHandler) Find() bool { }, func(resp string, canceled bool) { // Finished callback if !canceled { - match, found, err := h.Buf.FindNext(resp, h.Cursor.Loc, true) + match, found, err := h.Buf.FindNext(resp, h.Buf.Start(), h.Buf.End(), h.Cursor.Loc, true, true) if err != nil { InfoBar.Error(err) } @@ -597,7 +597,7 @@ func (h *BufHandler) FindNext() bool { if h.Cursor.HasSelection() { searchLoc = h.Cursor.CurSelection[1] } - match, found, err := h.Buf.FindNext(h.lastSearch, searchLoc, true) + match, found, err := h.Buf.FindNext(h.lastSearch, h.Buf.Start(), h.Buf.End(), searchLoc, true, true) if err != nil { InfoBar.Error(err) } @@ -623,7 +623,7 @@ func (h *BufHandler) FindPrevious() bool { if h.Cursor.HasSelection() { searchLoc = h.Cursor.CurSelection[0] } - match, found, err := h.Buf.FindNext(h.lastSearch, searchLoc, false) + match, found, err := h.Buf.FindNext(h.lastSearch, h.Buf.Start(), h.Buf.End(), searchLoc, false, true) if err != nil { InfoBar.Error(err) } @@ -1189,7 +1189,7 @@ func (h *BufHandler) SpawnMultiCursor() bool { if h.multiWord { search = "\\b" + search + "\\b" } - match, found, err := h.Buf.FindNext(search, searchStart, true) + match, found, err := h.Buf.FindNext(search, h.Buf.Start(), h.Buf.End(), searchStart, true, false) if err != nil { InfoBar.Error(err) } @@ -1262,7 +1262,7 @@ func (h *BufHandler) SkipMultiCursor() bool { sel := lastC.GetSelection() searchStart := lastC.CurSelection[1] - match, found, err := h.Buf.FindNext(string(sel), searchStart, true) + match, found, err := h.Buf.FindNext(string(sel), h.Buf.Start(), h.Buf.End(), searchStart, true, false) if err != nil { InfoBar.Error(err) } diff --git a/cmd/micro/action/command.go b/cmd/micro/action/command.go index ef4762df..196cdf08 100644 --- a/cmd/micro/action/command.go +++ b/cmd/micro/action/command.go @@ -5,8 +5,10 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strconv" "strings" + "unicode/utf8" "github.com/zyedidia/micro/cmd/micro/buffer" "github.com/zyedidia/micro/cmd/micro/config" @@ -540,6 +542,115 @@ func (h *BufHandler) SaveCmd(args []string) { // ReplaceCmd runs search and replace func (h *BufHandler) ReplaceCmd(args []string) { + if len(args) < 2 || len(args) > 4 { + // We need to find both a search and replace expression + InfoBar.Error("Invalid replace statement: " + strings.Join(args, " ")) + return + } + + all := false + noRegex := false + + if len(args) > 2 { + for _, arg := range args[2:] { + switch arg { + case "-a": + all = true + case "-l": + noRegex = true + default: + InfoBar.Error("Invalid flag: " + arg) + return + } + } + } + + search := args[0] + + if noRegex { + search = regexp.QuoteMeta(search) + } + + replace := []byte(args[1]) + + var regex *regexp.Regexp + var err error + if h.Buf.Settings["ignorecase"].(bool) { + regex, err = regexp.Compile("(?im)" + search) + } else { + regex, err = regexp.Compile("(?m)" + search) + } + if err != nil { + // There was an error with the user's regex + InfoBar.Error(err) + return + } + + nreplaced := 0 + start := h.Buf.Start() + end := h.Buf.End() + if h.Cursor.HasSelection() { + start = h.Cursor.CurSelection[0] + end = h.Cursor.CurSelection[1] + } + if all { + nreplaced = h.Buf.ReplaceRegex(start, end, regex, replace) + } else { + inRange := func(l buffer.Loc) bool { + return l.GreaterEqual(start) && l.LessThan(end) + } + + searchLoc := start + searching := true + var doReplacement func() + doReplacement = func() { + locs, found, err := h.Buf.FindNext(search, start, end, searchLoc, true, !noRegex) + if err != nil { + InfoBar.Error(err) + return + } + if !found || !inRange(locs[0]) || !inRange(locs[1]) { + h.Cursor.ResetSelection() + h.Cursor.Relocate() + return + } + + h.Cursor.SetSelectionStart(locs[0]) + h.Cursor.SetSelectionEnd(locs[1]) + + InfoBar.YNPrompt("Perform replacement (y,n,esc)", func(yes, canceled bool) { + if !canceled && yes { + h.Buf.Replace(locs[0], locs[1], replace) + searchLoc = locs[0] + searchLoc.X += utf8.RuneCount(replace) + h.Cursor.Loc = searchLoc + nreplaced++ + } else if !canceled && !yes { + searchLoc = locs[0] + searchLoc.X += utf8.RuneCount(replace) + } else if canceled { + h.Cursor.ResetSelection() + h.Cursor.Relocate() + return + } + if searching { + doReplacement() + } + }) + } + doReplacement() + } + + // TODO: relocate all cursors? + h.Cursor.Relocate() + + if nreplaced > 1 { + InfoBar.Message("Replaced ", nreplaced, " occurrences of ", search) + } else if nreplaced == 1 { + InfoBar.Message("Replaced ", nreplaced, " occurrence of ", search) + } else { + InfoBar.Message("Nothing matched ", search) + } } // ReplaceAllCmd replaces search term all at once diff --git a/cmd/micro/action/infohandler.go b/cmd/micro/action/infohandler.go index 2840e22c..11347960 100644 --- a/cmd/micro/action/infohandler.go +++ b/cmd/micro/action/infohandler.go @@ -33,20 +33,21 @@ func (h *InfoHandler) HandleEvent(event tcell.Event) { } done := h.DoKeyEvent(ke) - if e.Key() == tcell.KeyRune && h.HasYN { - if e.Rune() == 'y' && h.HasYN { + hasYN := h.HasYN + if e.Key() == tcell.KeyRune && hasYN { + if e.Rune() == 'y' && hasYN { h.YNResp = true h.DonePrompt(false) - } else if e.Rune() == 'n' && h.HasYN { + } else if e.Rune() == 'n' && hasYN { h.YNResp = false h.DonePrompt(false) } } - if e.Key() == tcell.KeyRune && !done && !h.HasYN { + if e.Key() == tcell.KeyRune && !done && !hasYN { h.DoRuneInsert(e.Rune()) done = true } - if done && h.HasPrompt && !h.HasYN { + if done && h.HasPrompt && !hasYN { resp := strings.TrimSpace(string(h.LineBytes(0))) hist := h.History[h.PromptType] hist[h.HistoryNum] = resp diff --git a/cmd/micro/buffer/search.go b/cmd/micro/buffer/search.go index a44b91ff..03932fc3 100644 --- a/cmd/micro/buffer/search.go +++ b/cmd/micro/buffer/search.go @@ -9,16 +9,32 @@ import ( func (b *Buffer) findDown(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) { start.Y = util.Clamp(start.Y, 0, b.LinesNum()-1) + end.Y = util.Clamp(end.Y, 0, b.LinesNum()-1) + + if start.GreaterThan(end) { + start, end = end, start + } for i := start.Y; i <= end.Y; i++ { l := b.LineBytes(i) charpos := 0 - if i == start.Y { + if i == start.Y && start.Y == end.Y { + nchars := utf8.RuneCount(l) + start.X = util.Clamp(start.X, 0, nchars-1) + end.X = util.Clamp(end.X, 0, nchars-1) + l = util.SliceStart(l, end.X) + l = util.SliceEnd(l, start.X) + charpos = start.X + } else if i == start.Y { nchars := utf8.RuneCount(l) start.X = util.Clamp(start.X, 0, nchars-1) l = util.SliceEnd(l, start.X) charpos = start.X + } else if i == end.Y { + nchars := utf8.RuneCount(l) + end.X = util.Clamp(end.X, 0, nchars-1) + l = util.SliceStart(l, end.X) } match := r.FindIndex(l) @@ -34,21 +50,39 @@ func (b *Buffer) findDown(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) { func (b *Buffer) findUp(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) { start.Y = util.Clamp(start.Y, 0, b.LinesNum()-1) + end.Y = util.Clamp(end.Y, 0, b.LinesNum()-1) - for i := start.Y; i >= end.Y; i-- { + if start.GreaterThan(end) { + start, end = end, start + } + + for i := end.Y; i >= start.Y; i-- { l := b.LineBytes(i) + charpos := 0 - if i == start.Y { + if i == start.Y && start.Y == end.Y { nchars := utf8.RuneCount(l) start.X = util.Clamp(start.X, 0, nchars-1) - l = util.SliceStart(l, start.X) + end.X = util.Clamp(end.X, 0, nchars-1) + l = util.SliceStart(l, end.X) + l = util.SliceEnd(l, start.X) + charpos = start.X + } else if i == start.Y { + nchars := utf8.RuneCount(l) + start.X = util.Clamp(start.X, 0, nchars-1) + l = util.SliceEnd(l, start.X) + charpos = start.X + } else if i == end.Y { + nchars := utf8.RuneCount(l) + end.X = util.Clamp(end.X, 0, nchars-1) + l = util.SliceStart(l, end.X) } match := r.FindIndex(l) if match != nil { - start := Loc{util.RunePos(l, match[0]), i} - end := Loc{util.RunePos(l, match[1]), i} + start := Loc{charpos + util.RunePos(l, match[0]), i} + end := Loc{charpos + util.RunePos(l, match[1]), i} return [2]Loc{start, end}, true } } @@ -59,13 +93,18 @@ func (b *Buffer) findUp(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) { // It returns the start and end location of the match (if found) and // a boolean indicating if it was found // May also return an error if the search regex is invalid -func (b *Buffer) FindNext(s string, from Loc, down bool) ([2]Loc, bool, error) { +func (b *Buffer) FindNext(s string, start, end, from Loc, down bool, useRegex bool) ([2]Loc, bool, error) { if s == "" { return [2]Loc{}, false, nil } var r *regexp.Regexp var err error + + if !useRegex { + s = regexp.QuoteMeta(s) + } + if b.Settings["ignorecase"].(bool) { r, err = regexp.Compile("(?i)" + s) } else { @@ -79,15 +118,54 @@ func (b *Buffer) FindNext(s string, from Loc, down bool) ([2]Loc, bool, error) { found := false var l [2]Loc if down { - l, found = b.findDown(r, from, b.End()) + l, found = b.findDown(r, from, end) if !found { - l, found = b.findDown(r, b.Start(), from) + l, found = b.findDown(r, start, from) } } else { - l, found = b.findUp(r, from, b.Start()) + l, found = b.findUp(r, from, start) if !found { - l, found = b.findUp(r, b.End(), from) + l, found = b.findUp(r, end, from) } } return l, found, nil } + +// ReplaceRegex replaces all occurrences of 'search' with 'replace' in the given area +// and returns the number of replacements made +func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []byte) int { + if start.GreaterThan(end) { + start, end = end, start + } + + found := 0 + var deltas []Delta + for i := start.Y; i <= end.Y; i++ { + l := b.lines[i].data + charpos := 0 + + // TODO: replace within X coords of selection + if start.Y == end.Y && i == start.Y { + l = util.SliceStart(l, end.X) + l = util.SliceEnd(l, start.X) + charpos = start.X + } else if i == start.Y { + l = util.SliceEnd(l, start.X) + charpos = start.X + } else if i == end.Y { + l = util.SliceStart(l, end.X) + } + newText := search.ReplaceAllFunc(l, func(in []byte) []byte { + found++ + return replace + }) + + from := Loc{charpos, i} + to := Loc{charpos + utf8.RuneCount(l), i} + + deltas = append(deltas, Delta{newText, from, to}) + } + b.MultipleReplace(deltas) + + return found +} diff --git a/cmd/micro/info/infobuffer.go b/cmd/micro/info/infobuffer.go index b22c8a52..cd6d4d4c 100644 --- a/cmd/micro/info/infobuffer.go +++ b/cmd/micro/info/infobuffer.go @@ -146,16 +146,11 @@ func (i *InfoBuf) DonePrompt(canceled bool) { h[len(h)-1] = resp } i.PromptCallback = nil - i.EventCallback = nil - } - if i.EventCallback != nil { - i.EventCallback = nil } i.Replace(i.Start(), i.End(), []byte{}) } if i.YNCallback != nil && hadYN { i.YNCallback(i.YNResp, canceled) - i.YNCallback = nil } }