diff --git a/internal/action/actions.go b/internal/action/actions.go index b0867da9f8..bcdc121278 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1144,8 +1144,7 @@ func (h *BufPane) find(useRegex bool) bool { match, found, err := h.Buf.FindNext(resp, h.Buf.Start(), h.Buf.End(), h.searchOrig, true, useRegex) if err != nil { InfoBar.Error(err) - } - if found { + } else if found { h.Cursor.SetSelectionStart(match[0]) h.Cursor.SetSelectionEnd(match[1]) h.Cursor.OrigSelection[0] = h.Cursor.CurSelection[0] diff --git a/internal/action/command.go b/internal/action/command.go index 7cb5e523bc..7d7a304043 100644 --- a/internal/action/command.go +++ b/internal/action/command.go @@ -1006,12 +1006,12 @@ func (h *BufPane) ReplaceCmd(args []string) { replace := []byte(replaceStr) - var regex *regexp.Regexp + var rg *buffer.RegexpGroup var err error if h.Buf.Settings["ignorecase"].(bool) { - regex, err = regexp.Compile("(?im)" + search) + rg, err = buffer.NewRegexpGroup("(?im)" + search) } else { - regex, err = regexp.Compile("(?m)" + search) + rg, err = buffer.NewRegexpGroup("(?m)" + search) } if err != nil { // There was an error with the user's regex @@ -1030,7 +1030,7 @@ func (h *BufPane) ReplaceCmd(args []string) { searchLoc = start // otherwise me might start at the end } if all { - nreplaced, _ = h.Buf.ReplaceRegex(start, end, regex, replace, !noRegex) + nreplaced, _ = h.Buf.ReplaceRegexGroup(start, end, rg, replace, !noRegex) } else { inRange := func(l buffer.Loc) bool { return l.GreaterEqual(start) && l.LessEqual(end) @@ -1072,7 +1072,7 @@ func (h *BufPane) ReplaceCmd(args []string) { InfoBar.YNPrompt("Perform replacement (y,n,esc)", func(yes, canceled bool) { if !canceled && yes { - _, nrunes := h.Buf.ReplaceRegex(locs[0], locs[1], regex, replace, !noRegex) + _, nrunes := h.Buf.ReplaceRegexGroup(locs[0], locs[1], rg, replace, !noRegex) searchLoc = locs[0] searchLoc.X += nrunes + locs[0].Diff(locs[1], h.Buf) diff --git a/internal/buffer/search.go b/internal/buffer/search.go index a48e1f87f4..72aa792bdc 100644 --- a/internal/buffer/search.go +++ b/internal/buffer/search.go @@ -2,22 +2,34 @@ package buffer import ( "regexp" + "regexp/syntax" "unicode/utf8" "github.com/zyedidia/micro/v2/internal/util" ) -// We want "^" and "$" to match only the beginning/end of a line, not the -// beginning/end of the search region if it is in the middle of a line. -// In that case we use padded regexps to require a rune before or after -// the match. (This also affects other empty-string patters like "\\b".) -// The following two flags indicate the padding used. +// RegexpGroup combines a Regexp with padded versions. +type RegexpGroup struct { + // TODO: This is currently not intended to be used in plugins, and a better + // abstract API should be provided + + // We want "^" and "$" to match only the beginning/end of a line, not that + // of the search region somewhere in the middle of a line. In that case we + // use padded regexps to require a rune before or after the match. (This + // also affects other empty-string patterns like "\\b".) + // + // Although some are not needed in all cases, they are checked because + // not returning an error for a regex in some cases would be an + // implementation detail that could cause confusion. + regex [4]*regexp.Regexp +} + const ( padStart = 1 << iota padEnd ) -func findLineParams(b *Buffer, start, end Loc, i int, r *regexp.Regexp) ([]byte, int, int, *regexp.Regexp) { +func findLineParams(b *Buffer, start, end Loc, i int) ([]byte, int, int) { l := b.LineBytes(i) charpos := 0 padMode := 0 @@ -41,18 +53,27 @@ func findLineParams(b *Buffer, start, end Loc, i int, r *regexp.Regexp) ([]byte, } } - if padMode == padStart { - r = regexp.MustCompile(".(?:" + r.String() + ")") - } else if padMode == padEnd { - r = regexp.MustCompile("(?:" + r.String() + ").") - } else if padMode == padStart|padEnd { - r = regexp.MustCompile(".(?:" + r.String() + ").") - } + return l, charpos, padMode +} - return l, charpos, padMode, r +// NewRegexpGroup creates a RegexpGroup from a string +func NewRegexpGroup(s string) (*RegexpGroup, error) { + var regex [4]*regexp.Regexp + var err error + regex[0], err = regexp.Compile(s) + if err == nil { + regex[padStart], err = regexp.Compile(".(?:" + s + ")") + if err == nil { + regex[padEnd] = regexp.MustCompile("(?:" + s + ").") + regex[padStart|padEnd] = regexp.MustCompile(".(?:" + s + ").") + } else { + err = &syntax.Error{syntax.ErrorCode(`possibly \Q without \E`), s} + } + } + return &RegexpGroup{regex}, err } -func (b *Buffer) findDown(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) { +func (b *Buffer) findDown(rg *RegexpGroup, start, end Loc) ([2]Loc, bool) { lastcn := util.CharacterCount(b.LineBytes(b.LinesNum() - 1)) if start.Y > b.LinesNum()-1 { start.X = lastcn - 1 @@ -68,9 +89,9 @@ func (b *Buffer) findDown(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) { } for i := start.Y; i <= end.Y; i++ { - l, charpos, padMode, rPadded := findLineParams(b, start, end, i, r) + l, charpos, padMode := findLineParams(b, start, end, i) - match := rPadded.FindIndex(l) + match := rg.regex[padMode].FindIndex(l) if match != nil { if padMode&padStart != 0 { @@ -89,7 +110,7 @@ func (b *Buffer) findDown(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) { return [2]Loc{}, false } -func (b *Buffer) findUp(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) { +func (b *Buffer) findUp(rg *RegexpGroup, start, end Loc) ([2]Loc, bool) { lastcn := util.CharacterCount(b.LineBytes(b.LinesNum() - 1)) if start.Y > b.LinesNum()-1 { start.X = lastcn - 1 @@ -109,7 +130,7 @@ func (b *Buffer) findUp(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) { from := Loc{0, i}.Clamp(start, end) to := Loc{charCount, i}.Clamp(start, end) - allMatches := b.findAll(r, from, to) + allMatches := b.findAll(rg, from, to) if allMatches != nil { match := allMatches[len(allMatches)-1] return [2]Loc{match[0], match[1]}, true @@ -118,11 +139,11 @@ func (b *Buffer) findUp(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) { return [2]Loc{}, false } -func (b *Buffer) findAll(r *regexp.Regexp, start, end Loc) [][2]Loc { +func (b *Buffer) findAll(rg *RegexpGroup, start, end Loc) [][2]Loc { var matches [][2]Loc loc := start for { - match, found := b.findDown(r, loc, end) + match, found := b.findDown(rg, loc, end) if !found { break } @@ -147,19 +168,15 @@ func (b *Buffer) FindNext(s string, start, end, from Loc, down bool, useRegex bo 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 { - r, err = regexp.Compile(s) + s = "(?i)" + s } + rg, err := NewRegexpGroup(s) if err != nil { return [2]Loc{}, false, err } @@ -167,14 +184,14 @@ func (b *Buffer) FindNext(s string, start, end, from Loc, down bool, useRegex bo var found bool var l [2]Loc if down { - l, found = b.findDown(r, from, end) + l, found = b.findDown(rg, from, end) if !found { - l, found = b.findDown(r, start, end) + l, found = b.findDown(rg, start, end) } } else { - l, found = b.findUp(r, from, start) + l, found = b.findUp(rg, from, start) if !found { - l, found = b.findUp(r, end, start) + l, found = b.findUp(rg, end, start) } } return l, found, nil @@ -184,6 +201,14 @@ func (b *Buffer) FindNext(s string, start, end, from Loc, down bool, useRegex bo // and returns the number of replacements made and the number of characters // added or removed on the last line of the range func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []byte, captureGroups bool) (int, int) { + rg, err := NewRegexpGroup(search.String()) + if err != nil { + return 0, 0 + } + return b.ReplaceRegexGroup(start, end, rg, replace, captureGroups) +} + +func (b *Buffer) ReplaceRegexGroup(start, end Loc, rg *RegexpGroup, replace []byte, captureGroups bool) (int, int) { if start.GreaterThan(end) { start, end = end, start } @@ -192,6 +217,8 @@ func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []b found := 0 var deltas []Delta + search := rg.regex[0] + for i := start.Y; i <= end.Y; i++ { l := b.LineBytes(i) charCount := util.CharacterCount(l) @@ -202,7 +229,7 @@ func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []b from := Loc{0, i}.Clamp(start, end) to := Loc{charCount, i}.Clamp(start, end) - matches := b.findAll(search, from, to) + matches := b.findAll(rg, from, to) found += len(matches) for j := len(matches) - 1; j >= 0; j-- {