From 53d56d032c4e806f6a914ea1d30a594e16bbe713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Thu, 4 Apr 2024 21:26:53 +0200 Subject: [PATCH 1/6] buffer: Remove unneeded recursion of `insert()` This is necessary as a preparation to introduce a lock for the whole LineArray. The modification can then be done without trying to lock the same lock twice. Co-authored-by: Dmytro Maluka --- internal/buffer/line_array.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/buffer/line_array.go b/internal/buffer/line_array.go index a906b1f4..7323dc1d 100644 --- a/internal/buffer/line_array.go +++ b/internal/buffer/line_array.go @@ -233,14 +233,14 @@ func (la *LineArray) insertByte(pos Loc, value byte) { // joinLines joins the two lines a and b func (la *LineArray) joinLines(a, b int) { - la.insert(Loc{len(la.lines[a].data), a}, la.lines[b].data) + la.lines[a].data = append(la.lines[a].data, la.lines[b].data...) la.deleteLine(b) } // split splits a line at a given position func (la *LineArray) split(pos Loc) { la.newlineBelow(pos.Y) - la.insert(Loc{0, pos.Y + 1}, la.lines[pos.Y].data[pos.X:]) + 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 From 2830c4878eef35d39ec0d222f9443d5feaa4bbb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Thu, 4 Apr 2024 21:22:22 +0200 Subject: [PATCH 2/6] buffer: Lock the `LineArray` in case of modifications and export this lock --- internal/buffer/buffer.go | 4 ++++ internal/buffer/line_array.go | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index 35e650fd..54fc266f 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -1180,7 +1180,11 @@ func (b *Buffer) Retab() { } l = bytes.TrimLeft(l, " \t") + + b.Lock() b.lines[i].data = append(ws, l...) + b.Unlock() + b.MarkModified(i, i) dirty = true } diff --git a/internal/buffer/line_array.go b/internal/buffer/line_array.go index 7323dc1d..9897e720 100644 --- a/internal/buffer/line_array.go +++ b/internal/buffer/line_array.go @@ -75,6 +75,7 @@ type LineArray struct { lines []Line Endings FileFormat initsize uint64 + lock sync.Mutex } // Append efficiently appends lines together @@ -206,6 +207,9 @@ func (la *LineArray) newlineBelow(y int) { // Inserts a byte array at a given location func (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') { @@ -251,6 +255,9 @@ func (la *LineArray) split(pos Loc) { // removes from start to end func (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) @@ -374,6 +381,16 @@ func (la *LineArray) SetRehighlight(lineN int, on bool) { la.lines[lineN].rehighlight = on } +// Locks the whole LineArray +func (la *LineArray) Lock() { + la.lock.Lock() +} + +// Unlocks the whole LineArray +func (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 dd7134a76226d1e2279d15f166a5fb70e66c80a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Thu, 4 Apr 2024 21:38:41 +0200 Subject: [PATCH 3/6] buffer: Remove superfluous `rehighlight` from `LineArray` ...which isn't used so far and probably handled better in a different way. --- internal/buffer/line_array.go | 48 +++++++++++------------------------ 1 file changed, 15 insertions(+), 33 deletions(-) diff --git a/internal/buffer/line_array.go b/internal/buffer/line_array.go index 9897e720..08847271 100644 --- a/internal/buffer/line_array.go +++ b/internal/buffer/line_array.go @@ -46,10 +46,9 @@ type searchState struct { type Line struct { data []byte - state highlight.State - match highlight.LineMatch - rehighlight bool - lock sync.Mutex + 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. @@ -148,20 +147,18 @@ func NewLineArray(size uint64, endings FileFormat, reader io.Reader) *LineArray if err != nil { if err == io.EOF { la.lines = Append(la.lines, Line{ - data: data, - state: nil, - match: nil, - rehighlight: false, + 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, - rehighlight: false, + data: data[:dlen-1], + state: nil, + match: nil, }) } n++ @@ -191,17 +188,15 @@ func (la *LineArray) Bytes() []byte { // newlineBelow adds a newline below the given line number func (la *LineArray) newlineBelow(y int) { la.lines = append(la.lines, Line{ - data: []byte{' '}, - state: nil, - match: nil, - rehighlight: false, + 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, - rehighlight: false, + data: []byte{}, + state: la.lines[y].state, + match: nil, } } @@ -249,7 +244,6 @@ func (la *LineArray) split(pos Loc) { la.lines[pos.Y].state = nil la.lines[pos.Y].match = nil la.lines[pos.Y+1].match = nil - la.lines[pos.Y].rehighlight = true la.deleteToEnd(Loc{pos.X, pos.Y}) } @@ -369,18 +363,6 @@ func (la *LineArray) Match(lineN int) highlight.LineMatch { return la.lines[lineN].match } -func (la *LineArray) Rehighlight(lineN int) bool { - la.lines[lineN].lock.Lock() - defer la.lines[lineN].lock.Unlock() - return la.lines[lineN].rehighlight -} - -func (la *LineArray) SetRehighlight(lineN int, on bool) { - la.lines[lineN].lock.Lock() - defer la.lines[lineN].lock.Unlock() - la.lines[lineN].rehighlight = on -} - // Locks the whole LineArray func (la *LineArray) Lock() { la.lock.Lock() From 6e71e375680c6a07f25e64ac35a56cddb6cd6163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Thu, 4 Apr 2024 21:40:54 +0200 Subject: [PATCH 4/6] buffer: Rename `LineBytes` parameter to "lineN" to fit to the rest --- internal/buffer/line_array.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/buffer/line_array.go b/internal/buffer/line_array.go index 08847271..7906524a 100644 --- a/internal/buffer/line_array.go +++ b/internal/buffer/line_array.go @@ -328,11 +328,11 @@ func (la *LineArray) End() Loc { } // LineBytes returns line n as an array of bytes -func (la *LineArray) LineBytes(n int) []byte { - if n >= len(la.lines) || n < 0 { +func (la *LineArray) LineBytes(lineN int) []byte { + if lineN >= len(la.lines) || lineN < 0 { return []byte{} } - return la.lines[n].data + return la.lines[lineN].data } // State gets the highlight state for the given line number From b6dcbfa846c2a618a04969ccbdf2dd1a1af15601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Thu, 4 Apr 2024 21:55:50 +0200 Subject: [PATCH 5/6] highlighter: Fix race between the async highlighter and the main routine This is achieved by the usage of the new `LineArray` locking machanism, which prevents the interruption in the moment of modifications like insertion or removal of lines. Co-authored-by: Dmytro Maluka --- pkg/highlight/highlighter.go | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/pkg/highlight/highlighter.go b/pkg/highlight/highlighter.go index 7552671c..547e606d 100644 --- a/pkg/highlight/highlighter.go +++ b/pkg/highlight/highlighter.go @@ -77,6 +77,8 @@ type LineStates interface { State(lineN int) State SetState(lineN int, s State) SetMatch(lineN int, m LineMatch) + Lock() + Unlock() } // A Highlighter contains the information needed to highlight a string @@ -303,7 +305,13 @@ func (h *Highlighter) HighlightString(input string) []LineMatch { // HighlightStates correctly sets all states for the buffer func (h *Highlighter) HighlightStates(input LineStates) { - for i := 0; i < input.LinesNum(); i++ { + for i := 0; ; i++ { + input.Lock() + if i >= input.LinesNum() { + input.Unlock() + break + } + line := input.LineBytes(i) // highlights := make(LineMatch) @@ -316,6 +324,7 @@ func (h *Highlighter) HighlightStates(input LineStates) { curState := h.lastRegion input.SetState(i, curState) + input.Unlock() } } @@ -324,7 +333,9 @@ func (h *Highlighter) HighlightStates(input LineStates) { // This assumes that all the states are set correctly func (h *Highlighter) HighlightMatches(input LineStates, startline, endline int) { for i := startline; i <= endline; i++ { + input.Lock() if i >= input.LinesNum() { + input.Unlock() break } @@ -339,6 +350,7 @@ func (h *Highlighter) HighlightMatches(input LineStates, startline, endline int) } input.SetMatch(i, match) + input.Unlock() } } @@ -350,9 +362,17 @@ func (h *Highlighter) ReHighlightStates(input LineStates, startline int) int { h.lastRegion = nil if startline > 0 { + input.Lock() h.lastRegion = input.State(startline - 1) + input.Unlock() } - for i := startline; i < input.LinesNum(); i++ { + for i := startline; ; i++ { + input.Lock() + if i >= input.LinesNum() { + input.Unlock() + break + } + line := input.LineBytes(i) // highlights := make(LineMatch) @@ -366,6 +386,7 @@ func (h *Highlighter) ReHighlightStates(input LineStates, startline int) int { lastState := input.State(i) input.SetState(i, curState) + input.Unlock() if curState == lastState { return i @@ -377,6 +398,9 @@ func (h *Highlighter) ReHighlightStates(input LineStates, startline int) int { // ReHighlightLine will rehighlight the state and match for a single line func (h *Highlighter) ReHighlightLine(input LineStates, lineN int) { + input.Lock() + defer input.Unlock() + line := input.LineBytes(lineN) highlights := make(LineMatch) From a3ca054371b753dd98580bd13ffb4ca2713018f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:26:16 +0200 Subject: [PATCH 6/6] buffer: Uncomment `InitRuntimeFiles(false)` in the buffer_test.go ...since we fixed the race between the syntax highlighting and the buffer editing. --- internal/buffer/buffer_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/buffer/buffer_test.go b/internal/buffer/buffer_test.go index dcfde610..7d7602e4 100644 --- a/internal/buffer/buffer_test.go +++ b/internal/buffer/buffer_test.go @@ -20,9 +20,7 @@ type operation struct { func init() { ulua.L = lua.NewState() - // TODO: uncomment InitRuntimeFiles once we fix races between syntax - // highlighting and buffer editing. - // config.InitRuntimeFiles(false) + config.InitRuntimeFiles(false) config.InitGlobalSettings() config.GlobalSettings["backup"] = false config.GlobalSettings["fastdirty"] = true