Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Fix global strategy highlighting #132

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ is based on `Keep a Changelog`_ and this project adheres to `Semantic
Versioning`_.


Unreleased
##########

Fixed
=====

- Highlighting wrong in global strategy after making changes inside a nested
node

Changed
=======

- Queries no longer need the `@sentinel` capture group


[0.6.2] - 2024-09-26
####################

Expand Down
18 changes: 6 additions & 12 deletions doc/rainbow-delimiters.txt
Original file line number Diff line number Diff line change
Expand Up @@ -569,16 +569,12 @@ The queries need to define the following capture groups:
The entire delimited node.
- `@delimiter`
Any delimiter you want to highlight in the current `@container`.
- `@sentinel`
A marker used to signal that you are done with the `@container`. This
should almost always be put right after the last `@delimiter` in the given
`@container`.
- `@_<capture_name>`
Delimiters starting with `_` (underscore) are ignored for highlighting
purposes, but you can use them for treesitter predicates like
`#eq?`, `#any-eq?`, etc. (These are very rarely needed.)

`@container` and `@sentinel` are mandatory, and `@delimiter` will always be
`@container` is mandatory, and at least one `@delimiter` will always be
present as well since `@delimiter` is what is highlighted. The captures
starting with underscore will be rarely used, since you only need them for
predicates in a few special cases.
Expand Down Expand Up @@ -632,7 +628,7 @@ the `<br/>` tag a delimiter. The corresponding query is as follows:
(start_tag) @delimiter
(element
(self_closing_tag) @delimiter)? ; Optional!
(end_tag) @delimiter @sentinel) @container
(end_tag) @delimiter) @container
<
Highlighting the entire tag might be too vibrant though. What if we want to
highlight only the opening and closing angle brackets? The query gets
Expand All @@ -647,10 +643,8 @@ tree.
["<" "/>"] @delimiter))? ;Optional!
(end_tag
"</" @delimiter
">" @delimiter @sentinel))) @container
">" @delimiter))) @container
<
Note that we don't want to put the `@sentinel` on the second to last `@delimiter`
`"</"`, we need to be careful to put it on the last `@delimiter`.

You might now see why we need the `@container` capture group: there is no way
to know in general how deeply the delimiter is nested. Even for one language
Expand All @@ -672,7 +666,7 @@ pleasant middle ground between the two above extremes.
(end_tag
"</" @delimiter
(tag_name) @delimiter
">" @delimiter @sentinel)) @container
">" @delimiter)) @container
<
Here both opening and closing tag have three delimiters each.

Expand All @@ -683,7 +677,7 @@ In HTML the terminating slash in a self-closing tag is optional. Instead of
(start_tag
"<" @delimiter
(tag_name) @delimiter @_tag_name
">" @delimiter @sentinel)) @container
">" @delimiter)) @container
<
However, this query also matches the opening tag of regular tags like `<div>`.
This is where the `@_tag_name` capture comes in. The set of self-closing tags
Expand All @@ -694,7 +688,7 @@ will not match this particular pattern.
(start_tag
"<" @delimiter
(tag_name) @delimiter @_tag_name
">" @delimiter @sentinel)
">" @delimiter)
;; List abridged for brevity
(#any-of? @_tag_name "br" "hr" "input")) @container
<
Expand Down
1 change: 1 addition & 0 deletions lua/rainbow-delimiters/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ local M = {
end
}),
enabled_for = function(lang)
if not lang then return false end
local conf = vim.g.rainbow_delimiters
if not conf then return true end

Expand Down
120 changes: 120 additions & 0 deletions lua/rainbow-delimiters/match-tree.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---Functions for dealing with match trees. This library is only relevant to
---strategy authors. A match tree is the tree-like structure we use to
---organize a subset of the buffer's node tree for highlighting.
local M = {}

local lib = require 'rainbow-delimiters.lib'
local Set = require 'rainbow-delimiters.set'

---A single match from the query. All matches contain the same fields, which
---correspond to the captures from the query. Matches are hierarchical and can
---be arranged in a tree where the container of a parent match contains all the
---nodes of the descendant matches.
---@class rainbow_delimiters.Match
---The container node.
---@field container vim.treesitter.TSNode
---Sentinel node, marks the last delimiter of the match.
---@field sentinel vim.treesitter.TSNode
---The actual delimiters we want to highlight, there can be any number of them.
---@field delimiters rainbow_delimiters.Set

---A hierarchical structure of nested matches. Each node of the tree consists
---of exactly one match and a set of any number of child matches. Terminal
---matches have no children.
---
---Match trees have a strict partial ordering: for two matches `m1` and `m2` we
---say that `m1` < `m2` if and only if the container of `m1` contains the
---container of `m2`, i.e. `m1` is an ancestor of `m2`. The root node will
---have the lowest value.
---@class rainbow_delimiters.MatchTree
---The match object
---@field public match rainbow_delimiters.Match
---The children of the match
---@field public children rainbow_delimiters.Set

local match_mt = {
__tostring = function(self)
return string.format(
'{container = %s, delimiters = %s}',
tostring(self.container),
tostring(self.delimiters)
)
end
}

local tree_mt = {
---@param m1 rainbow_delimiters.MatchTree
---@param m2 rainbow_delimiters.MatchTree
---@return boolean
__lt = function(m1, m2)
local c1 = m1.match.container
local r2 = {m2.match.container:range()}
return vim.treesitter.node_contains(c1, r2)
end,
---Appends the given match tree `m2` to this match tree. Will traverse
---through the descendants until it finds the most appropriate one.
---@return boolean success Whether appending was successfull
__call = function(self, other)
if not (self < other) then return false end
for child in self.children:items() do
if child < other then
return child(other)
end
end
self.children:add(other)
return true
end,
__tostring = function(self)
return string.format(
'{match = %s, children = %s}',
tostring(self.match),
tostring(self.children)
)
end
}

---Instantiate a new match tree node without children based on the results of
---the `iter_matches` method of a query.
---@param query vim.treesitter.Query
---@param match Table<integer, vim.treesitter.TSNode[]>
---@return rainbow_delimiters.MatchTree
function M.assemble(query, match)
local result = {delimiters = Set.new()}
for id, nodes in pairs(match) do
local capture = query.captures[id]
if capture == 'delimiter' then
-- It is expected for a match to contain any number of delimiters
for _, node in ipairs(nodes) do
result.delimiters:add(node)
end
else
-- We assume that there is only ever exactly one node per
-- non-delimiter capture
result[capture] = nodes[1]
end
end

---@type rainbow_delimiters.MatchTree
local matchtree = {
match = setmetatable(result, match_mt),
children = Set.new(),
}
return setmetatable(matchtree, tree_mt)
end

---Apply highlighting to a given match tree at a given level
---@param bufnr integer
---@param lang string
---@param tree rainbow_delimiters.MatchTree
---@param level integer Highlight level of this tree
function M.highlight(tree, bufnr, lang, level)
local hlgroup = lib.hlgroup_at(level)
for delimiter in tree.match.delimiters:items() do
lib.highlight(bufnr, lang, delimiter, hlgroup)
end
for child in tree.children:items() do
M.highlight(child, bufnr, lang, level + 1)
end
end

return M
99 changes: 99 additions & 0 deletions lua/rainbow-delimiters/set.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
--[[
Copyright 2024 Alejandro "HiPhish" Sanchez

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
--]]

---Helper library for set-like tables.
local M = {}

---A set-like structure which holds any number of items, but only one of each.
---@generic T
---@class rainbow_delimiters.Set<T>
---
---Add an item to the set; this function is idempotent: adding an item more
---than once produces the same result
---@field public add fun(self: rainbow_delimiters.Set, item: `T`): nil
---
---Predicate whether the set currently contains a given item
---@field public contains fun(self: rainbow_delimiters.Set, item: `T`): boolean
---
---Returns the current size of the set
---@field public size fun(self: rainbow_delimiters.Set): integer
---@field package content `T`[]
---
---Iterator which returns the contents one at a time in an arbitrary order.
---@field public items fun(self: rainbow_delimiters.Set): ((fun(content: table<`T`, true>, key: `T`): `T`), table<`T`, true>)


local function size(self)
local result = 0
for _, _ in pairs(self.content) do
result = result + 1
end
return result
end

local function add(self, item)
self.content[item] = true
end

local function contains(self, item)
return self.content[item] == true
end

---Wrapper around the built-in `next`, except that it only returns the key.
local function iter(t, k)
local result = next(t, k)
return result
end

local function items(self)
return iter, self.content
end

local function pick_key(key, _value)
return key
end

local mt = {
---A human-readable representation of a set like `'Set{1, 2, "a", "b"}'`
---@param self rainbow_delimiters.Set
---@return string
__tostring = function(self)
local keys = vim.iter(self.content)
:map(pick_key)
:map(tostring)
:join(', ')
return string.format('Set{%s}', keys)
end
}

---@return rainbow_delimiters.Set set The new set instance
function M.new(...)
---@type rainbow_delimiters.Set
local result = {
content = {},
size = size,
add = add,
contains = contains,
items = items,
}
for _, item in ipairs({...}) do
result.content[item] = true
end
setmetatable(result, mt)
return result
end

return M
Loading