package buffer import "strings" // Find returns the first occurrence of needle at or after from, and whether // there was one. // // The search wraps round the end of the buffer back to its start, so pressing // "find next" repeatedly cycles through every match rather than stopping at // the bottom of the file. // // if r, ok := b.Find("func", b.Cursor(), false); ok { // b.SetCursor(r.Start) // } func (b *Buffer) Find(needle string, from Position, matchCase bool) (Range, bool) { if needle == "" { return Range{}, false } from = b.clamp(from) if found, ok := b.findBetween(needle, from, len(b.lines)-1, matchCase); ok { return found, true } // Nothing ahead: start again from the top, up to and including the line // the search began on. return b.findBetween(needle, Position{}, from.Line, matchCase) } // FindPrevious returns the last occurrence of needle strictly before from, // wrapping round to the end of the buffer. func (b *Buffer) FindPrevious(needle string, from Position, matchCase bool) (Range, bool) { if needle == "" { return Range{}, false } from = b.clamp(from) if found, ok := b.findBackwardsFrom(needle, from, matchCase); ok { return found, true } return b.findBackwardsFrom(needle, b.wholeRange().End, matchCase) } // findBetween scans forwards from a position to the end of a line. func (b *Buffer) findBetween(needle string, from Position, lastLine int, matchCase bool) (Range, bool) { for line := from.Line; line <= lastLine && line < len(b.lines); line++ { start := 0 if line == from.Line { start = from.Col } if column, ok := indexFrom(b.Line(line), needle, start, matchCase); ok { return matchRange(line, column, needle), true } } return Range{}, false } // findBackwardsFrom scans backwards from a position to the top of the buffer. func (b *Buffer) findBackwardsFrom(needle string, from Position, matchCase bool) (Range, bool) { for line := from.Line; line >= 0; line-- { limit := len(b.lines[line]) if line == from.Line { limit = from.Col } if column, ok := lastIndexBefore(b.Line(line), needle, limit, matchCase); ok { return matchRange(line, column, needle), true } } return Range{}, false } // matchRange turns a rune column and a needle into the range it covers. func matchRange(line, column int, needle string) Range { return Range{ Start: Position{Line: line, Col: column}, End: Position{Line: line, Col: column + len([]rune(needle))}, } } // indexFrom returns the rune column of the first occurrence of needle in line // at or after the rune column start. func indexFrom(line, needle string, start int, matchCase bool) (int, bool) { runes := []rune(line) if start > len(runes) { return 0, false } haystack, wanted := foldIfNeeded(string(runes[start:]), needle, matchCase) index := strings.Index(haystack, wanted) if index < 0 { return 0, false } return start + len([]rune(haystack[:index])), true } // lastIndexBefore returns the rune column of the last occurrence of needle in // line that starts before the rune column limit. func lastIndexBefore(line, needle string, limit int, matchCase bool) (int, bool) { runes := []rune(line) limit = min(max(limit, 0), len(runes)) haystack, wanted := foldIfNeeded(string(runes[:limit]), needle, matchCase) index := strings.LastIndex(haystack, wanted) if index < 0 { return 0, false } return len([]rune(haystack[:index])), true } // foldIfNeeded lowercases both strings for a case-insensitive search. // // Lowercasing can change a string's byte length, but never its rune count for // the alphabets this matters for, and every offset above is converted back // through a rune count — so the columns stay right. func foldIfNeeded(haystack, needle string, matchCase bool) (string, string) { if matchCase { return haystack, needle } return strings.ToLower(haystack), strings.ToLower(needle) }