feat(treesitter): #trim! can trim all whitespace

This commit also implements more generic trimming, acting on all
whitespace (charwise) rather than just empty lines.

It will unblock
https://github.com/nvim-treesitter/nvim-treesitter/pull/3442 and allow
for properly concealing markdown bullet markers regardless of indent
width, e.g.
This commit is contained in:
Riley Bruins
2024-08-17 21:05:09 -07:00
parent 1077843b9b
commit b8c75a31e6
4 changed files with 160 additions and 19 deletions

View File

@ -280,6 +280,8 @@ TREESITTER
• |LanguageTree:node_for_range()| gets anonymous and named nodes for a range
• |vim.treesitter.get_node()| now takes an option `include_anonymous`, default
false, which allows it to return anonymous nodes as well as named nodes.
• |treesitter-directive-trim!| can trim all whitespace (not just empty lines)
from both sides of a node.
TUI

View File

@ -245,15 +245,32 @@ The following directives are built in:
(#gsub! @_node ".*%.(.*)" "%1")
<
`trim!` *treesitter-directive-trim!*
Trim blank lines from the end of the node. This will set a new
`metadata[capture_id].range`.
Trims whitespace from the node. Sets a new
`metadata[capture_id].range`. Takes a capture ID and, optionally, four
integers to customize trimming behavior (`1` meaning trim, `0` meaning
don't trim). When only given a capture ID, trims blank lines (lines
that contain only whitespace, or are empty) from the end of the node
(for backwards compatibility). Can trim all whitespace from both sides
of the node if parameters are given.
Examples: >query
; only trim blank lines from the end of the node
; (equivalent to (#trim! @fold 0 0 1 0))
(#trim! @fold)
; trim blank lines from both sides of the node
(#trim! @fold 1 0 1 0)
; trim all whitespace around the node
(#trim! @fold 1 1 1 1)
<
Parameters: ~
{capture_id}
{trim_start_linewise}
{trim_start_charwise}
{trim_end_linewise} (default `1` if only given {capture_id})
{trim_end_charwise}
Example: >query
(#trim! @fold)
<
Further directives can be added via |vim.treesitter.query.add_directive()|.
Use |vim.treesitter.query.list_directives()| to list all available directives.

View File

@ -572,13 +572,17 @@ local directive_handlers = {
metadata[id].text = text:gsub(pattern, replacement)
end,
-- Trim blank lines from end of the node
-- Example: (#trim! @fold)
-- TODO(clason): generalize to arbitrary whitespace removal
-- Trim whitespace from both sides of the node
-- Example: (#trim! @fold 1 1 1 1)
['trim!'] = function(match, _, bufnr, pred, metadata)
local capture_id = pred[2]
assert(type(capture_id) == 'number')
local trim_start_lines = pred[3] == '1'
local trim_start_cols = pred[4] == '1'
local trim_end_lines = pred[5] == '1' or not pred[3] -- default true for backwards compatibility
local trim_end_cols = pred[6] == '1'
local nodes = match[capture_id]
if not nodes or #nodes == 0 then
return
@ -588,20 +592,36 @@ local directive_handlers = {
local start_row, start_col, end_row, end_col = node:range()
-- Don't trim if region ends in middle of a line
if end_col ~= 0 then
return
local node_text = vim.split(vim.treesitter.get_node_text(node, bufnr), '\n')
local end_idx = #node_text
local start_idx = 1
if trim_end_lines then
while end_idx > 0 and node_text[end_idx]:find('^%s*$') do
end_idx = end_idx - 1
end_row = end_row - 1
end
end
if trim_end_cols then
if end_idx == 0 then
end_row = start_row
end_col = start_col
else
local whitespace_start = node_text[end_idx]:find('(%s*)$')
end_col = (whitespace_start - 1) + (end_idx == 1 and start_col or 0)
end
end
while end_row >= start_row do
-- As we only care when end_col == 0, always inspect one line above end_row.
local end_line = api.nvim_buf_get_lines(bufnr, end_row - 1, end_row, true)[1]
if end_line ~= '' then
break
if trim_start_lines then
while start_idx <= end_idx and node_text[start_idx]:find('^%s*$') do
start_idx = start_idx + 1
start_row = start_row + 1
end
end_row = end_row - 1
end
if trim_start_cols and node_text[start_idx] then
local _, whitespace_end = node_text[start_idx]:find('^(%s*)')
whitespace_end = whitespace_end or 0
start_col = (start_idx == 1 and start_col or 0) + whitespace_end
end
-- If this produces an invalid range, we just skip it.

View File

@ -644,6 +644,108 @@ print()
end)
end)
describe('trim! directive', function()
it('can trim all whitespace', function()
-- luacheck: push ignore 611 613
insert([=[
print([[
f
helllo
there
asdf
asdfassd
]])
print([[
]])
print([[]])
print([[
]])
print([[ hello 😃 ]])
]=])
-- luacheck: pop
local query_text = [[
; query
((string_content) @str
(#trim! @str 1 1 1 1))
]]
exec_lua(function()
vim.treesitter.start(0, 'lua')
end)
local function run_query()
return exec_lua(function(query_str)
local query = vim.treesitter.query.parse('lua', query_str)
local parser = vim.treesitter.get_parser()
local tree = parser:parse()[1]
local res = {}
for id, _, metadata in query:iter_captures(tree:root(), 0) do
table.insert(res, { query.captures[id], metadata[id].range })
end
return res
end, query_text)
end
eq({
{ 'str', { 2, 12, 6, 10 } },
{ 'str', { 11, 10, 11, 10 } },
{ 'str', { 17, 10, 17, 10 } },
{ 'str', { 19, 10, 19, 10 } },
{ 'str', { 22, 15, 22, 25 } },
}, run_query())
end)
it('trims only empty lines by default (backwards compatible)', function()
insert [[
## Heading
With some text
## And another
With some more]]
local query_text = [[
; query
((section) @fold
(#trim! @fold))
]]
exec_lua(function()
vim.treesitter.start(0, 'markdown')
end)
local function run_query()
return exec_lua(function(query_str)
local query = vim.treesitter.query.parse('markdown', query_str)
local parser = vim.treesitter.get_parser()
local tree = parser:parse()[1]
local res = {}
for id, _, metadata in query:iter_captures(tree:root(), 0) do
table.insert(res, { query.captures[id], metadata[id].range })
end
return res
end, query_text)
end
eq({
{ 'fold', { 0, 0, 3, 0 } },
{ 'fold', { 4, 0, 7, 0 } },
}, run_query())
end)
end)
it('tracks the root range properly (#22911)', function()
insert([[
int main() {