Support for highlighting all search matches (hlsearch) (#1762)

* Support for highlighting all search matches (hlsearch)

hlsearch is implemented efficiently using the buffer's line array,
somewhat similarly to the syntax highlighting.
Unlike the syntax highlighter which highlights the entire file,
hlsearch searches for matches for the displayed lines only.
Matches are searched when the given line is displayed first time
or after it was modified. Otherwise the previously found matches
are used.

* Add UnhighlightSearch action

and add it to the list of actions triggered by Esc key by default.

* Add comment explaining the purpose of search map

* Add hlsearch colors to colorschemes

Mostly just copied from the corresponding original (mostly vim) colorschemes.

* Highlight matches during/after replace as well

As a side effect it also changes the last search value, i.e. affects FindNext
and FindPrevious, but it's probably fine. In vim it works the same way.

* Improve hlsearch option description
This commit is contained in:
Dmitry Maluka
2021-09-28 22:39:03 +02:00
committed by GitHub
parent 9ad4437a98
commit ffbb257434
36 changed files with 197 additions and 22 deletions

View File

@@ -864,8 +864,10 @@ func (h *BufPane) FindLiteral() bool {
// Search searches for a given string/regex in the buffer and selects the next
// match if a match is found
// This function affects lastSearch and lastSearchRegex (saved searches) for
// use with FindNext and FindPrevious
// This function behaves the same way as Find and FindLiteral actions:
// it affects the buffer's LastSearch and LastSearchRegex (saved searches)
// for use with FindNext and FindPrevious, and turns HighlightSearch on or off
// according to hlsearch setting
func (h *BufPane) Search(str string, useRegex bool, searchDown bool) error {
match, found, err := h.Buf.FindNext(str, h.Buf.Start(), h.Buf.End(), h.Cursor.Loc, searchDown, useRegex)
if err != nil {
@@ -877,8 +879,9 @@ func (h *BufPane) Search(str string, useRegex bool, searchDown bool) error {
h.Cursor.OrigSelection[0] = h.Cursor.CurSelection[0]
h.Cursor.OrigSelection[1] = h.Cursor.CurSelection[1]
h.Cursor.GotoLoc(h.Cursor.CurSelection[1])
h.lastSearch = str
h.lastSearchRegex = useRegex
h.Buf.LastSearch = str
h.Buf.LastSearchRegex = useRegex
h.Buf.HighlightSearch = h.Buf.Settings["hlsearch"].(bool)
h.Relocate()
} else {
h.Cursor.ResetSelection()
@@ -922,8 +925,9 @@ func (h *BufPane) find(useRegex bool) bool {
h.Cursor.OrigSelection[0] = h.Cursor.CurSelection[0]
h.Cursor.OrigSelection[1] = h.Cursor.CurSelection[1]
h.Cursor.GotoLoc(h.Cursor.CurSelection[1])
h.lastSearch = resp
h.lastSearchRegex = useRegex
h.Buf.LastSearch = resp
h.Buf.LastSearchRegex = useRegex
h.Buf.HighlightSearch = h.Buf.Settings["hlsearch"].(bool)
} else {
h.Cursor.ResetSelection()
InfoBar.Message("No matches found")
@@ -944,6 +948,18 @@ func (h *BufPane) find(useRegex bool) bool {
return true
}
// ToggleHighlightSearch toggles highlighting all instances of the last used search term
func (h *BufPane) ToggleHighlightSearch() bool {
h.Buf.HighlightSearch = !h.Buf.HighlightSearch
return true
}
// UnhighlightSearch unhighlights all instances of the last used search term
func (h *BufPane) UnhighlightSearch() bool {
h.Buf.HighlightSearch = false
return true
}
// FindNext searches forwards for the last used search term
func (h *BufPane) FindNext() bool {
// If the cursor is at the start of a selection and we search we want
@@ -954,7 +970,7 @@ func (h *BufPane) FindNext() bool {
if h.Cursor.HasSelection() {
searchLoc = h.Cursor.CurSelection[1]
}
match, found, err := h.Buf.FindNext(h.lastSearch, h.Buf.Start(), h.Buf.End(), searchLoc, true, h.lastSearchRegex)
match, found, err := h.Buf.FindNext(h.Buf.LastSearch, h.Buf.Start(), h.Buf.End(), searchLoc, true, h.Buf.LastSearchRegex)
if err != nil {
InfoBar.Error(err)
}
@@ -981,7 +997,7 @@ func (h *BufPane) FindPrevious() bool {
if h.Cursor.HasSelection() {
searchLoc = h.Cursor.CurSelection[0]
}
match, found, err := h.Buf.FindNext(h.lastSearch, h.Buf.Start(), h.Buf.End(), searchLoc, false, h.lastSearchRegex)
match, found, err := h.Buf.FindNext(h.Buf.LastSearch, h.Buf.Start(), h.Buf.End(), searchLoc, false, h.Buf.LastSearchRegex)
if err != nil {
InfoBar.Error(err)
}

View File

@@ -226,9 +226,6 @@ type BufPane struct {
// Same here, just to keep track for mouse move events
tripleClick bool
// Last search stores the last successful search for FindNext and FindPrev
lastSearch string
lastSearchRegex bool
// Should the current multiple cursor selection search based on word or
// based on selection (false for selection, true for word)
multiWord bool
@@ -683,6 +680,8 @@ var BufKeyActions = map[string]BufKeyAction{
"ToggleKeyMenu": (*BufPane).ToggleKeyMenu,
"ToggleDiffGutter": (*BufPane).ToggleDiffGutter,
"ToggleRuler": (*BufPane).ToggleRuler,
"ToggleHighlightSearch": (*BufPane).ToggleHighlightSearch,
"UnhighlightSearch": (*BufPane).UnhighlightSearch,
"ClearStatus": (*BufPane).ClearStatus,
"ShellMode": (*BufPane).ShellMode,
"CommandMode": (*BufPane).CommandMode,

View File

@@ -835,6 +835,9 @@ func (h *BufPane) ReplaceCmd(args []string) {
h.Cursor.SetSelectionStart(locs[0])
h.Cursor.SetSelectionEnd(locs[1])
h.Cursor.GotoLoc(locs[0])
h.Buf.LastSearch = search
h.Buf.LastSearchRegex = true
h.Buf.HighlightSearch = h.Buf.Settings["hlsearch"].(bool)
h.Relocate()

View File

@@ -87,7 +87,7 @@ var bufdefaults = map[string]string{
"F4": "Quit",
"F7": "Find",
"F10": "Quit",
"Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors",
"Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch",
// Mouse bindings
"MouseWheelUp": "ScrollUp",

View File

@@ -89,7 +89,7 @@ var bufdefaults = map[string]string{
"F4": "Quit",
"F7": "Find",
"F10": "Quit",
"Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors",
"Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch",
// Mouse bindings
"MouseWheelUp": "ScrollUp",

View File

@@ -146,18 +146,20 @@ func (b *SharedBuffer) remove(start, end Loc) []byte {
func (b *SharedBuffer) MarkModified(start, end int) {
b.ModifiedThisFrame = true
if !b.Settings["syntax"].(bool) || b.SyntaxDef == nil {
return
}
start = util.Clamp(start, 0, len(b.lines)-1)
end = util.Clamp(end, 0, len(b.lines)-1)
l := -1
for i := start; i <= end; i++ {
l = util.Max(b.Highlighter.ReHighlightStates(b, i), l)
if b.Settings["syntax"].(bool) && b.SyntaxDef != nil {
l := -1
for i := start; i <= end; i++ {
l = util.Max(b.Highlighter.ReHighlightStates(b, i), l)
}
b.Highlighter.HighlightMatches(b, start, l)
}
for i := start; i <= end; i++ {
b.LineArray.invalidateSearchMatches(i)
}
b.Highlighter.HighlightMatches(b, start, l)
}
// DisableReload disables future reloads of this sharedbuffer
@@ -181,6 +183,7 @@ type DiffStatus byte
// The syntax highlighting info must be stored with the buffer because the syntax
// highlighter attaches information to each line of the buffer for optimization
// purposes so it doesn't have to rehighlight everything on every update.
// Likewise for the search highlighting.
type Buffer struct {
*EventHandler
*SharedBuffer
@@ -202,6 +205,12 @@ type Buffer struct {
// This is hacky. Maybe it would be better to move all the visual x logic
// from buffer to display, but it would require rewriting a lot of code.
GetVisualX func(loc Loc) int
// Last search stores the last successful search
LastSearch string
LastSearchRegex bool
// HighlightSearch enables highlighting all instances of the last successful search
HighlightSearch bool
}
// NewBufferFromFileAtLoc opens a new buffer with a given cursor location
@@ -1199,6 +1208,12 @@ func (b *Buffer) DiffStatus(lineN int) DiffStatus {
return b.diff[lineN]
}
// SearchMatch returns true if the given location is within a match of the last search.
// It is used for search highlighting
func (b *Buffer) SearchMatch(pos Loc) bool {
return b.LineArray.SearchMatch(b, pos)
}
// WriteLog writes a string to the log buffer
func WriteLog(s string) {
LogBuf.EventHandler.Insert(LogBuf.End(), s)

View File

@@ -32,6 +32,15 @@ func runeToByteIndex(n int, txt []byte) int {
return count
}
// A searchState contains the search match info for a single line
type 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 updated
type Line struct {
@@ -41,6 +50,14 @@ type Line struct {
match highlight.LineMatch
rehighlight bool
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 (
@@ -356,3 +373,79 @@ func (la *LineArray) SetRehighlight(lineN int, on bool) {
defer la.lines[lineN].lock.Unlock()
la.lines[lineN].rehighlight = on
}
// 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
}
}
}

View File

@@ -39,6 +39,12 @@ func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error {
b.isModified = true
} else if option == "readonly" && b.Type.Kind == BTDefault.Kind {
b.Type.Readonly = nativeValue.(bool)
} else if option == "hlsearch" {
for _, buf := range OpenBuffers {
if b.SharedBuffer == buf.SharedBuffer {
buf.HighlightSearch = nativeValue.(bool)
}
}
}
if b.OptionCallback != nil {

View File

@@ -269,6 +269,7 @@ var defaultCommonSettings = map[string]interface{}{
"fastdirty": false,
"fileformat": "unix",
"filetype": "unknown",
"hlsearch": false,
"incsearch": true,
"ignorecase": true,
"indentchar": " ",

View File

@@ -487,10 +487,17 @@ func (w *BufWindow) displayBuffer() {
draw := func(r rune, combc []rune, style tcell.Style, highlight bool, showcursor bool) {
if nColsBeforeStart <= 0 && vloc.Y >= 0 {
if highlight {
if w.Buf.HighlightSearch && w.Buf.SearchMatch(bloc) {
style = config.DefStyle.Reverse(true)
if s, ok := config.Colorscheme["hlsearch"]; ok {
style = s
}
}
_, origBg, _ := style.Decompose()
_, defBg, _ := config.DefStyle.Decompose()
// syntax highlighting with non-default background takes precedence
// syntax or hlsearch highlighting with non-default background takes precedence
// over cursor-line and color-column
dontOverrideBackground := origBg != defBg

View File

@@ -11,6 +11,7 @@ color-link special "#A6E22E,#1D1F21"
color-link underlined "#D33682,#1D1F21"
color-link error "bold #FF4444,#1D1F21"
color-link todo "bold #FF8844,#1D1F21"
color-link hlsearch "#000000,#B4EC85"
color-link statusline "#1D1F21,#C5C8C6"
color-link tabbar "#1D1F21,#C5C8C6"
color-link indent-char "#505050,#1D1F21"

View File

@@ -12,6 +12,7 @@ color-link special "167,231"
color-link error "231, 160"
color-link underlined "underline 241,231"
color-link todo "246,231"
color-link hlsearch "231,136"
color-link statusline "241,254"
color-link tabbar "241,254"
color-link diff-added "34"

View File

@@ -26,6 +26,7 @@ color-link special "magenta"
color-link ignore "default"
color-link error "bold ,brightred"
color-link todo "underline black,brightyellow"
color-link hlsearch "white,darkgreen"
color-link indent-char ",brightgreen"
color-link line-number "green"
color-link line-number.scrollbar "green"

View File

@@ -21,6 +21,7 @@ color-link special "#b57edc"
color-link ignore "default"
color-link error "bold ,#e34234"
color-link todo "bold underline #888888,#f26522"
color-link hlsearch "#b7b7b7,#32593d"
color-link indent-char ",#bdecb6"
color-link line-number "#bdecb6,#36393e"
color-link line-number.scrollbar "#3eb489"

View File

@@ -12,6 +12,7 @@ color-link special "#CC8242,#242424"
color-link underlined "#D33682,#242424"
color-link error "bold #CB4B16,#242424"
color-link todo "bold #D33682,#242424"
color-link hlsearch "#CCCCCC,#32593D"
color-link statusline "#242424,#CCCCCC"
color-link tabbar "#242424,#CCCCCC"
color-link indent-char "#4F4F4F,#242424"

View File

@@ -12,6 +12,7 @@ color-link special "#A6E22E,#282828"
color-link underlined "#D33682,#282828"
color-link error "bold #CB4B16,#282828"
color-link todo "bold #D33682,#282828"
color-link hlsearch "#282828,#E6DB74"
color-link statusline "#282828,#F8F8F2"
color-link tabbar "#282828,#F8F8F2"
color-link indent-char "#505050,#282828"

View File

@@ -24,6 +24,8 @@ color-link underlined "#FF79C6"
color-link error "bold #FF5555"
color-link todo "bold #FF79C6"
color-link hlsearch "#282A36,#50FA7B"
color-link diff-added "#50FA7B"
color-link diff-modified "#FFB86C"
color-link diff-deleted "#FF5555"

View File

@@ -15,6 +15,7 @@ color-link divider "#001e28,#d0d0d0"
color-link error "#cb4b16,#001e28"
color-link gutter-error "#cb4b16,#001e28"
color-link gutter-warning "#fce94f,#001e28"
color-link hlsearch "#ffffff,#005028"
color-link identifier "#00c8a0,#001e28"
color-link identifier.class "#00c8a0,#001e28"
color-link indent-char "#a0a0a0,#001e28"

View File

@@ -15,6 +15,7 @@ color-link divider "#f0f0f0,#004080"
color-link error "#500000,#f0f0f0"
color-link gutter-error "#500000,#f0f0f0"
color-link gutter-warning "#dcc800,#f0f0f0"
color-link hlsearch "#000000,#b8d8e8"
color-link identifier "bold #0078a0,#f0f0f0"
color-link identifier.class "bold #0078a0,#f0f0f0"
color-link indent-char "#404040,#f0f0f0"

View File

@@ -15,6 +15,7 @@ color-link divider "#2d0023,#d0d0d0"
color-link error "#cb4b16,#2d0023"
color-link gutter-error "#cb4b16,#2d0023"
color-link gutter-warning "#fce94f,#2d0023"
color-link hlsearch "#ffffff,#005028"
color-link identifier "#00c8a0,#2d0023"
color-link identifier.class "#00c8a0,#2d0023"
color-link indent-char "#a0a0a0,#2d0023"

View File

@@ -12,6 +12,7 @@ color-link type "blue"
color-link type.extended "default"
color-link error "red"
color-link todo "bold cyan"
color-link hlsearch "black,brightcyan"
color-link indent-char "bold black"
color-link line-number ""
color-link current-line-number ""

View File

@@ -11,6 +11,7 @@ color-link special "#D26937,#0C1014"
color-link underlined "#EDB443,#0C1014"
color-link error "bold #C23127,#0C1014"
color-link todo "bold #888CA6,#0C1014"
color-link hlsearch "#091F2E,#EDB443"
color-link statusline "#091F2E,#599CAB"
color-link indent-char "#505050,#0C1014"
color-link line-number "#245361,#11151C"

View File

@@ -11,6 +11,7 @@ color-link type "#fb4934,#282828"
color-link special "#d79921,#282828"
color-link underlined "underline #282828"
color-link error "#9d0006,#282828"
color-link hlsearch "#282828,#fabd2f"
color-link diff-added "#00AF00"
color-link diff-modified "#FFAF00"
color-link diff-deleted "#D70000"

View File

@@ -11,6 +11,7 @@ color-link special "172,235"
color-link underlined "underline 109,235"
color-link error "235,124"
color-link todo "bold 223,235"
color-link hlsearch "235,214"
color-link diff-added "34"
color-link diff-modified "214"
color-link diff-deleted "160"

View File

@@ -14,6 +14,7 @@ color-link divider "#263238,#80DEEA"
color-link error "bold #263238,#F07178"
color-link gutter-error "#EEFFFF,#F07178"
color-link gutter-warning "#EEFFFF,#FFF176"
color-link hlsearch "#FFFFFF,#546E7A"
color-link identifier "#82AAFF,#263238"
color-link identifier.macro "#FFCB6B,#263238"
color-link indent-char "#505050,#263238"

View File

@@ -11,6 +11,7 @@ color-link special "#A6E22E"
color-link underlined "#D33682"
color-link error "bold #CB4B16"
color-link todo "bold #D33682"
color-link hlsearch "#1D0000,#E6DB74"
color-link statusline "#282828,#F8F8F2"
color-link indent-char "#505050,#282828"
color-link line-number "#AAAAAA,#282828"

View File

@@ -12,6 +12,7 @@ color-link special "#A6E22E,#282828"
color-link underlined "#D33682,#282828"
color-link error "bold #CB4B16,#282828"
color-link todo "bold #D33682,#282828"
color-link hlsearch "#282828,#E6DB74"
color-link statusline "#282828,#F8F8F2"
color-link tabbar "#282828,#F8F8F2"
color-link indent-char "#505050,#282828"

View File

@@ -15,6 +15,7 @@ color-link diff-modified "#FFAF00"
color-link diff-deleted "#D70000"
color-link gutter-error "#9B859D"
color-link gutter-warning "#9B859D"
color-link hlsearch "#21252C,#E5C07B"
color-link identifier "#61AFEF"
color-link identifier.class "#C678DD"
color-link identifier.var "#C678DD"

View File

@@ -11,6 +11,7 @@ color-link underlined "#cc7833,#2b2b2b"
color-link todo "bold #cc7833,#2b2b2b"
color-link error "bold #cc7833,#2b2b2b"
color-link gutter-error "#cc7833,#11151C"
color-link hlsearch "#e6e1dc,#474d5c"
color-link indent-char "#414141,#2b2b2b"
color-link line-number "#a1a1a1,#232323"
color-link current-line-number "#e6e1dc,#2b2b2b"

View File

@@ -9,6 +9,7 @@ color-link special "magenta"
color-link ignore "default"
color-link error ",brightred"
color-link todo ",brightyellow"
color-link hlsearch "black,yellow"
color-link indent-char "black"
color-link line-number "yellow"
color-link current-line-number "red"

View File

@@ -11,6 +11,7 @@ color-link special "#268BD2,#002833"
color-link underlined "#D33682,#002833"
color-link error "bold #CB4B16,#002833"
color-link todo "bold #D33682,#002833"
color-link hlsearch "#002833,#B58900"
color-link statusline "#003541,#839496"
color-link tabbar "#003541,#839496"
color-link indent-char "#003541,#002833"

View File

@@ -10,6 +10,7 @@ color-link special "blue"
color-link underlined "magenta"
color-link error "bold brightred"
color-link todo "bold magenta"
color-link hlsearch "black,yellow"
color-link statusline "black,brightblue"
color-link tabbar "black,brightblue"
color-link indent-char "black"

View File

@@ -11,6 +11,7 @@ color-link special "22"
color-link underlined "61,230"
color-link error "88"
color-link todo "210"
color-link hlsearch "0,253"
color-link statusline "233,229"
color-link tabbar "233,229"
color-link indent-char "229"

View File

@@ -15,6 +15,7 @@ color-link diff-modified "#FFAF00"
color-link diff-deleted "#D70000"
color-link gutter-error "#9B859D"
color-link gutter-warning "#9B859D"
color-link hlsearch "#141414,#C0C000"
color-link identifier "#9B703F"
color-link identifier.class "#DAD085"
color-link identifier.var "#7587A6"

View File

@@ -12,6 +12,7 @@ color-link special "181,237"
color-link underlined "188,237"
color-link error "115,236"
color-link todo "bold 254,237"
color-link hlsearch "230,22"
color-link statusline "186,236"
color-link tabbar "186,236"
color-link indent-char "238,237"

View File

@@ -159,6 +159,15 @@ Here are the available options:
default value: `unknown`. This will be automatically overridden depending
on the file you open.
* `hlsearch`: highlight all instances of the searched text after a successful
search. This highlighting can be turned off via `UnhighlightSearch` action
(triggered by Esc key by default) or toggled on/off via `ToggleHighlightSearch`
action. Note that these actions don't change `hlsearch` setting.
As long as `hlsearch` is set to true, after the next search the highlighting
is turned on again.
default value: `false`
* `incsearch`: enable incremental search in "Find" prompt (matching as you type).
default value: `true`