Something went wrong. Try again.
Fork of micro-editor/micro
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446package buffer
import ( "bufio" "bytes" "io" "sync"
"github.com/micro-editor/micro/v2/internal/util" "github.com/micro-editor/micro/v2/pkg/highlight")
// Finds the byte index of the nth rune in a byte slicefunc runeToByteIndex(n int, txt []byte) int { if n == 0 { return 0 }
count := 0 i := 0 for len(txt) > 0 { _, _, size := util.DecodeCharacter(txt)
txt = txt[size:] count += size i++
if i == n { break } } return count}
// A searchState contains the search match info for a single linetype searchState struct { search string useRegex bool ignorecase bool match [][2]int done bool}
// A Line contains the data in bytes as well as a highlight state, match// and a flag for whether the highlighting needs to be updatedtype Line struct { data []byte
state highlight.State match highlight.LineMatch lock sync.Mutex
// The search states for the line, used for highlighting of search matches, // separately from the syntax highlighting. // A map is used because the line array may be shared between multiple buffers // (multiple instances of the same file opened in different edit panes) // which have distinct searches, so in the general case there are multiple // searches per a line, one search per a Buffer containing this line. search map[*Buffer]*searchState}
const ( // Line ending file formats FFAuto = 0 // Autodetect format FFUnix = 1 // LF line endings (unix style '\n') FFDos = 2 // CRLF line endings (dos style '\r\n'))
type FileFormat byte
// A LineArray simply stores and array of lines and makes it easy to insert// and delete in ittype LineArray struct { lines []Line Endings FileFormat initsize uint64 lock sync.Mutex}
// Append efficiently appends lines together// It allocates an additional 10000 lines if the original estimate// is incorrectfunc Append(slice []Line, data ...Line) []Line { l := len(slice) if l+len(data) > cap(slice) { // reallocate newSlice := make([]Line, (l+len(data))+10000) copy(newSlice, slice) slice = newSlice } slice = slice[0 : l+len(data)] for i, c := range data { slice[l+i] = c } return slice}
// NewLineArray returns a new line array from an array of bytesfunc NewLineArray(size uint64, endings FileFormat, reader io.Reader) *LineArray { la := new(LineArray)
la.lines = make([]Line, 0, 1000) la.initsize = size
br := bufio.NewReader(reader) var loaded int
la.Endings = endings
n := 0 for { data, err := br.ReadBytes('\n') // Detect the line ending by checking to see if there is a '\r' char // before the '\n' // Even if the file format is set to DOS, the '\r' is removed so // that all lines end with '\n' dlen := len(data) if dlen > 1 && data[dlen-2] == '\r' { data = append(data[:dlen-2], '\n') if la.Endings == FFAuto { la.Endings = FFDos } dlen = len(data) } else if dlen > 0 { if la.Endings == FFAuto { la.Endings = FFUnix } }
// If we are loading a large file (greater than 1000) we use the file // size and the length of the first 1000 lines to try to estimate // how many lines will need to be allocated for the rest of the file // We add an extra 10000 to the original estimate to be safe and give // plenty of room for expansion if n >= 1000 && loaded >= 0 { totalLinesNum := int(float64(size) * (float64(n) / float64(loaded))) newSlice := make([]Line, len(la.lines), totalLinesNum+10000) copy(newSlice, la.lines) la.lines = newSlice loaded = -1 }
// Counter for the number of bytes in the first 1000 lines if loaded >= 0 { loaded += dlen }
if err != nil { if err == io.EOF { la.lines = Append(la.lines, Line{ data: data, state: nil, match: nil, }) } // Last line was read break } else { la.lines = Append(la.lines, Line{ data: data[:dlen-1], state: nil, match: nil, }) } n++ }
return la}
// Bytes returns the string that should be written to disk when// the line array is savedfunc (la *LineArray) Bytes() []byte { b := new(bytes.Buffer) // initsize should provide a good estimate b.Grow(int(la.initsize + 4096)) for i, l := range la.lines { b.Write(l.data) if i != len(la.lines)-1 { if la.Endings == FFDos { b.WriteByte('\r') } b.WriteByte('\n') } } return b.Bytes()}
// newlineBelow adds a newline below the given line numberfunc (la *LineArray) newlineBelow(y int) { la.lines = append(la.lines, Line{ data: []byte{' '}, state: nil, match: nil, }) copy(la.lines[y+2:], la.lines[y+1:]) la.lines[y+1] = Line{ data: []byte{}, state: la.lines[y].state, match: nil, }}
// Inserts a byte array at a given locationfunc (la *LineArray) insert(pos Loc, value []byte) { la.lock.Lock() defer la.lock.Unlock()
x, y := runeToByteIndex(pos.X, la.lines[pos.Y].data), pos.Y for i := 0; i < len(value); i++ { if value[i] == '\n' || (value[i] == '\r' && i < len(value)-1 && value[i+1] == '\n') { la.split(Loc{x, y}) x = 0 y++
if value[i] == '\r' { i++ }
continue } la.insertByte(Loc{x, y}, value[i]) x++ }}
// InsertByte inserts a byte at a given locationfunc (la *LineArray) insertByte(pos Loc, value byte) { la.lines[pos.Y].data = append(la.lines[pos.Y].data, 0) copy(la.lines[pos.Y].data[pos.X+1:], la.lines[pos.Y].data[pos.X:]) la.lines[pos.Y].data[pos.X] = value}
// joinLines joins the two lines a and bfunc (la *LineArray) joinLines(a, b int) { la.lines[a].data = append(la.lines[a].data, la.lines[b].data...) la.deleteLine(b)}
// split splits a line at a given positionfunc (la *LineArray) split(pos Loc) { la.newlineBelow(pos.Y) la.lines[pos.Y+1].data = append(la.lines[pos.Y+1].data, la.lines[pos.Y].data[pos.X:]...) la.lines[pos.Y+1].state = la.lines[pos.Y].state la.lines[pos.Y].state = nil la.lines[pos.Y].match = nil la.lines[pos.Y+1].match = nil la.deleteToEnd(Loc{pos.X, pos.Y})}
// removes from start to endfunc (la *LineArray) remove(start, end Loc) []byte { la.lock.Lock() defer la.lock.Unlock()
sub := la.Substr(start, end) startX := runeToByteIndex(start.X, la.lines[start.Y].data) endX := runeToByteIndex(end.X, la.lines[end.Y].data) if start.Y == end.Y { la.lines[start.Y].data = append(la.lines[start.Y].data[:startX], la.lines[start.Y].data[endX:]...) } else { la.deleteLines(start.Y+1, end.Y-1) la.deleteToEnd(Loc{startX, start.Y}) la.deleteFromStart(Loc{endX - 1, start.Y + 1}) la.joinLines(start.Y, start.Y+1) } return sub}
// deleteToEnd deletes from the end of a line to the positionfunc (la *LineArray) deleteToEnd(pos Loc) { la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X]}
// deleteFromStart deletes from the start of a line to the positionfunc (la *LineArray) deleteFromStart(pos Loc) { la.lines[pos.Y].data = la.lines[pos.Y].data[pos.X+1:]}
// deleteLine deletes the line numberfunc (la *LineArray) deleteLine(y int) { la.lines = la.lines[:y+copy(la.lines[y:], la.lines[y+1:])]}
func (la *LineArray) deleteLines(y1, y2 int) { la.lines = la.lines[:y1+copy(la.lines[y1:], la.lines[y2+1:])]}
// Substr returns the string representation between two locationsfunc (la *LineArray) Substr(start, end Loc) []byte { startX := runeToByteIndex(start.X, la.lines[start.Y].data) endX := runeToByteIndex(end.X, la.lines[end.Y].data) if start.Y == end.Y { src := la.lines[start.Y].data[startX:endX] dest := make([]byte, len(src)) copy(dest, src) return dest } str := make([]byte, 0, len(la.lines[start.Y+1].data)*(end.Y-start.Y)) str = append(str, la.lines[start.Y].data[startX:]...) str = append(str, '\n') for i := start.Y + 1; i <= end.Y-1; i++ { str = append(str, la.lines[i].data...) str = append(str, '\n') } str = append(str, la.lines[end.Y].data[:endX]...) return str}
// LinesNum returns the number of lines in the bufferfunc (la *LineArray) LinesNum() int { return len(la.lines)}
// Start returns the start of the bufferfunc (la *LineArray) Start() Loc { return Loc{0, 0}}
// End returns the location of the last character in the bufferfunc (la *LineArray) End() Loc { numlines := len(la.lines) return Loc{util.CharacterCount(la.lines[numlines-1].data), numlines - 1}}
// LineBytes returns line n as an array of bytesfunc (la *LineArray) LineBytes(lineN int) []byte { if lineN >= len(la.lines) || lineN < 0 { return []byte{} } return la.lines[lineN].data}
// State gets the highlight state for the given line numberfunc (la *LineArray) State(lineN int) highlight.State { la.lines[lineN].lock.Lock() defer la.lines[lineN].lock.Unlock() return la.lines[lineN].state}
// SetState sets the highlight state at the given line numberfunc (la *LineArray) SetState(lineN int, s highlight.State) { la.lines[lineN].lock.Lock() defer la.lines[lineN].lock.Unlock() la.lines[lineN].state = s}
// SetMatch sets the match at the given line numberfunc (la *LineArray) SetMatch(lineN int, m highlight.LineMatch) { la.lines[lineN].lock.Lock() defer la.lines[lineN].lock.Unlock() la.lines[lineN].match = m}
// Match retrieves the match for the given line numberfunc (la *LineArray) Match(lineN int) highlight.LineMatch { la.lines[lineN].lock.Lock() defer la.lines[lineN].lock.Unlock() return la.lines[lineN].match}
// Locks the whole LineArrayfunc (la *LineArray) Lock() { la.lock.Lock()}
// Unlocks the whole LineArrayfunc (la *LineArray) Unlock() { la.lock.Unlock()}
// SearchMatch returns true if the location `pos` is within a match// of the last search for the buffer `b`.// It is used for efficient highlighting of search matches (separately// from the syntax highlighting).// SearchMatch searches for the matches if it is called first time// for the given line or if the line was modified. Otherwise the// previously found matches are used.//// The buffer `b` needs to be passed because the line array may be shared// between multiple buffers (multiple instances of the same file opened// in different edit panes) which have distinct searches, so SearchMatch// needs to know which search to match against.func (la *LineArray) SearchMatch(b *Buffer, pos Loc) bool { if b.LastSearch == "" { return false }
lineN := pos.Y if la.lines[lineN].search == nil { la.lines[lineN].search = make(map[*Buffer]*searchState) } s, ok := la.lines[lineN].search[b] if !ok { // Note: here is a small harmless leak: when the buffer `b` is closed, // `s` is not deleted from the map. It means that the buffer // will not be garbage-collected until the line array is garbage-collected, // i.e. until all the buffers sharing this file are closed. s = new(searchState) la.lines[lineN].search[b] = s } if !ok || s.search != b.LastSearch || s.useRegex != b.LastSearchRegex || s.ignorecase != b.Settings["ignorecase"].(bool) { s.search = b.LastSearch s.useRegex = b.LastSearchRegex s.ignorecase = b.Settings["ignorecase"].(bool) s.done = false }
if !s.done { s.match = nil start := Loc{0, lineN} end := Loc{util.CharacterCount(la.lines[lineN].data), lineN} for start.X < end.X { m, found, _ := b.FindNext(b.LastSearch, start, end, start, true, b.LastSearchRegex) if !found { break } s.match = append(s.match, [2]int{m[0].X, m[1].X})
start.X = m[1].X if m[1].X == m[0].X { start.X = m[1].X + 1 } }
s.done = true }
for _, m := range s.match { if pos.X >= m[0] && pos.X < m[1] { return true } } return false}
// invalidateSearchMatches marks search matches for the given line as outdated.// It is called when the line is modified.func (la *LineArray) invalidateSearchMatches(lineN int) { if la.lines[lineN].search != nil { for _, s := range la.lines[lineN].search { s.done = false } }}