mirror of
https://github.com/zyedidia/micro.git
synced 2025-06-18 23:05:40 -04:00

Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
239 lines
6.0 KiB
Go
239 lines
6.0 KiB
Go
package buffer
|
|
|
|
import (
|
|
"regexp"
|
|
"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.
|
|
const (
|
|
padStart = 1 << iota
|
|
padEnd
|
|
)
|
|
|
|
func findLineParams(b *Buffer, start, end Loc, i int, r *regexp.Regexp) ([]byte, int, int, *regexp.Regexp) {
|
|
l := b.LineBytes(i)
|
|
charpos := 0
|
|
padMode := 0
|
|
|
|
if i == end.Y {
|
|
nchars := util.CharacterCount(l)
|
|
end.X = util.Clamp(end.X, 0, nchars)
|
|
if end.X < nchars {
|
|
l = util.SliceStart(l, end.X+1)
|
|
padMode |= padEnd
|
|
}
|
|
}
|
|
|
|
if i == start.Y {
|
|
nchars := util.CharacterCount(l)
|
|
start.X = util.Clamp(start.X, 0, nchars)
|
|
if start.X > 0 {
|
|
charpos = start.X - 1
|
|
l = util.SliceEnd(l, charpos)
|
|
padMode |= padStart
|
|
}
|
|
}
|
|
|
|
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, r
|
|
}
|
|
|
|
func (b *Buffer) findDown(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
|
|
lastcn := util.CharacterCount(b.LineBytes(b.LinesNum() - 1))
|
|
if start.Y > b.LinesNum()-1 {
|
|
start.X = lastcn - 1
|
|
}
|
|
if end.Y > b.LinesNum()-1 {
|
|
end.X = lastcn
|
|
}
|
|
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, charpos, padMode, rPadded := findLineParams(b, start, end, i, r)
|
|
|
|
match := rPadded.FindIndex(l)
|
|
|
|
if match != nil {
|
|
if padMode&padStart != 0 {
|
|
_, size := utf8.DecodeRune(l[match[0]:])
|
|
match[0] += size
|
|
}
|
|
if padMode&padEnd != 0 {
|
|
_, size := utf8.DecodeLastRune(l[:match[1]])
|
|
match[1] -= size
|
|
}
|
|
start := Loc{charpos + util.RunePos(l, match[0]), i}
|
|
end := Loc{charpos + util.RunePos(l, match[1]), i}
|
|
return [2]Loc{start, end}, true
|
|
}
|
|
}
|
|
return [2]Loc{}, false
|
|
}
|
|
|
|
func (b *Buffer) findUp(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
|
|
lastcn := util.CharacterCount(b.LineBytes(b.LinesNum() - 1))
|
|
if start.Y > b.LinesNum()-1 {
|
|
start.X = lastcn - 1
|
|
}
|
|
if end.Y > b.LinesNum()-1 {
|
|
end.X = lastcn
|
|
}
|
|
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 := end.Y; i >= start.Y; i-- {
|
|
charCount := util.CharacterCount(b.LineBytes(i))
|
|
from := Loc{0, i}.Clamp(start, end)
|
|
to := Loc{charCount, i}.Clamp(start, end)
|
|
|
|
allMatches := b.findAll(r, from, to)
|
|
if allMatches != nil {
|
|
match := allMatches[len(allMatches)-1]
|
|
return [2]Loc{match[0], match[1]}, true
|
|
}
|
|
}
|
|
return [2]Loc{}, false
|
|
}
|
|
|
|
func (b *Buffer) findAll(r *regexp.Regexp, start, end Loc) [][2]Loc {
|
|
var matches [][2]Loc
|
|
loc := start
|
|
for {
|
|
match, found := b.findDown(r, loc, end)
|
|
if !found {
|
|
break
|
|
}
|
|
matches = append(matches, match)
|
|
if match[0] != match[1] {
|
|
loc = match[1]
|
|
} else if match[1] != end {
|
|
loc = match[1].Move(1, b)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
return matches
|
|
}
|
|
|
|
// FindNext finds the next occurrence of a given string in the buffer
|
|
// 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, 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 {
|
|
r, err = regexp.Compile(s)
|
|
}
|
|
|
|
if err != nil {
|
|
return [2]Loc{}, false, err
|
|
}
|
|
|
|
var found bool
|
|
var l [2]Loc
|
|
if down {
|
|
l, found = b.findDown(r, from, end)
|
|
if !found {
|
|
l, found = b.findDown(r, start, end)
|
|
}
|
|
} else {
|
|
l, found = b.findUp(r, from, start)
|
|
if !found {
|
|
l, found = b.findUp(r, end, start)
|
|
}
|
|
}
|
|
return l, found, nil
|
|
}
|
|
|
|
// ReplaceRegex replaces all occurrences of 'search' with 'replace' in the given area
|
|
// 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) {
|
|
if start.GreaterThan(end) {
|
|
start, end = end, start
|
|
}
|
|
|
|
charsEnd := util.CharacterCount(b.LineBytes(end.Y))
|
|
found := 0
|
|
var deltas []Delta
|
|
|
|
for i := start.Y; i <= end.Y; i++ {
|
|
l := b.LineBytes(i)
|
|
charCount := util.CharacterCount(l)
|
|
if (i == start.Y && start.X > 0) || (i == end.Y && end.X < charCount) {
|
|
// This replacement code works in general, but it creates a separate
|
|
// modification for each match. We only use it for the first and last
|
|
// lines, which may use padded regexps
|
|
|
|
from := Loc{0, i}.Clamp(start, end)
|
|
to := Loc{charCount, i}.Clamp(start, end)
|
|
matches := b.findAll(search, from, to)
|
|
found += len(matches)
|
|
|
|
for j := len(matches) - 1; j >= 0; j-- {
|
|
// if we counted upwards, the different deltas would interfere
|
|
match := matches[j]
|
|
var newText []byte
|
|
if captureGroups {
|
|
newText = search.ReplaceAll(b.Substr(match[0], match[1]), replace)
|
|
} else {
|
|
newText = replace
|
|
}
|
|
deltas = append(deltas, Delta{newText, match[0], match[1]})
|
|
}
|
|
} else {
|
|
newLine := search.ReplaceAllFunc(l, func(in []byte) []byte {
|
|
found++
|
|
var result []byte
|
|
if captureGroups {
|
|
match := search.FindSubmatchIndex(in)
|
|
result = search.Expand(result, replace, in, match)
|
|
} else {
|
|
result = replace
|
|
}
|
|
return result
|
|
})
|
|
deltas = append(deltas, Delta{newLine, Loc{0, i}, Loc{charCount, i}})
|
|
}
|
|
}
|
|
|
|
b.MultipleReplace(deltas)
|
|
|
|
return found, util.CharacterCount(b.LineBytes(end.Y)) - charsEnd
|
|
}
|