Add search and replace

This commit is contained in:
Zachary Yedidia 2019-01-15 22:45:28 -05:00
parent df968db5a3
commit a3885bfb12
5 changed files with 212 additions and 27 deletions

View file

@ -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)
}

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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
}
}