feat(treesitter): add a query editor (#24703)

This commit is contained in:
Maria José Solano
2023-08-25 11:17:36 -07:00
committed by GitHub
parent ecd99e7dd7
commit 5d8ab32f38
6 changed files with 206 additions and 25 deletions

View File

@@ -133,6 +133,8 @@ The following new APIs and features were added.
`vim.treesitter.language.register`. `vim.treesitter.language.register`.
• The `#set!` directive now supports `injection.self` and `injection.parent` for injecting either the current node's language • The `#set!` directive now supports `injection.self` and `injection.parent` for injecting either the current node's language
or the parent LanguageTree's language, respectively. or the parent LanguageTree's language, respectively.
• Added `vim.treesitter.preview_query()`, for live editing of treesitter
queries.
• |vim.ui.open()| opens URIs using the system default handler (macOS `open`, • |vim.ui.open()| opens URIs using the system default handler (macOS `open`,
Windows `explorer`, Linux `xdg-open`, etc.) Windows `explorer`, Linux `xdg-open`, etc.)

View File

@@ -676,8 +676,9 @@ inspect_tree({opts}) *vim.treesitter.inspect_tree()*
language tree. language tree.
While in the window, press "a" to toggle display of anonymous nodes, "I" While in the window, press "a" to toggle display of anonymous nodes, "I"
to toggle the display of the source language of each node, and press to toggle the display of the source language of each node, "o" to toggle
<Enter> to jump to the node under the cursor in the source buffer. the query previewer, and press <Enter> to jump to the node under the
cursor in the source buffer.
Can also be shown with `:InspectTree`. *:InspectTree* Can also be shown with `:InspectTree`. *:InspectTree*
@@ -730,6 +731,11 @@ node_contains({node}, {range}) *vim.treesitter.node_contains()*
Return: ~ Return: ~
(boolean) True if the {node} contains the {range} (boolean) True if the {node} contains the {range}
preview_query() *vim.treesitter.preview_query()*
Open a window for live editing of a treesitter query.
Can also be shown with `:PreviewQuery`. *:PreviewQuery*
start({bufnr}, {lang}) *vim.treesitter.start()* start({bufnr}, {lang}) *vim.treesitter.start()*
Starts treesitter highlighting for a buffer Starts treesitter highlighting for a buffer

View File

@@ -1,6 +1,6 @@
-- Neovim filetype plugin file -- Neovim filetype plugin file
-- Language: Tree-sitter query -- Language: Tree-sitter query
-- Last Change: 2022 Apr 25 -- Last Change: 2023 Aug 23
if vim.b.did_ftplugin == 1 then if vim.b.did_ftplugin == 1 then
return return
@@ -14,6 +14,8 @@ vim.treesitter.start()
-- set omnifunc -- set omnifunc
vim.bo.omnifunc = 'v:lua.vim.treesitter.query.omnifunc' vim.bo.omnifunc = 'v:lua.vim.treesitter.query.omnifunc'
vim.opt_local.iskeyword:append('.')
-- query linter -- query linter
local buf = vim.api.nvim_get_current_buf() local buf = vim.api.nvim_get_current_buf()
local query_lint_on = vim.g.query_lint_on or { 'BufEnter', 'BufWrite' } local query_lint_on = vim.g.query_lint_on or { 'BufEnter', 'BufWrite' }

View File

@@ -472,8 +472,8 @@ end
--- Open a window that displays a textual representation of the nodes in the language tree. --- Open a window that displays a textual representation of the nodes in the language tree.
--- ---
--- While in the window, press "a" to toggle display of anonymous nodes, "I" to toggle the --- While in the window, press "a" to toggle display of anonymous nodes, "I" to toggle the
--- display of the source language of each node, and press <Enter> to jump to the node under the --- display of the source language of each node, "o" to toggle the query previewer, and press
--- cursor in the source buffer. --- <Enter> to jump to the node under the cursor in the source buffer.
--- ---
--- Can also be shown with `:InspectTree`. *:InspectTree* --- Can also be shown with `:InspectTree`. *:InspectTree*
--- ---
@@ -494,6 +494,13 @@ function M.inspect_tree(opts)
require('vim.treesitter.dev').inspect_tree(opts) require('vim.treesitter.dev').inspect_tree(opts)
end end
--- Open a window for live editing of a treesitter query.
---
--- Can also be shown with `:PreviewQuery`. *:PreviewQuery*
function M.preview_query()
require('vim.treesitter.dev').preview_query()
end
--- Returns the fold level for {lnum} in the current buffer. Can be set directly to 'foldexpr': --- Returns the fold level for {lnum} in the current buffer. Can be set directly to 'foldexpr':
--- <pre>lua --- <pre>lua
--- vim.wo.foldexpr = 'v:lua.vim.treesitter.foldexpr()' --- vim.wo.foldexpr = 'v:lua.vim.treesitter.foldexpr()'

View File

@@ -124,7 +124,7 @@ function TSTreeView:new(bufnr, lang)
end end
local t = { local t = {
ns = api.nvim_create_namespace(''), ns = api.nvim_create_namespace('treesitter/dev-inspect'),
nodes = nodes, nodes = nodes,
named = named, named = named,
opts = { opts = {
@@ -158,6 +158,29 @@ local function escape_quotes(text)
return string.format('"%s"', text:sub(2, #text - 1):gsub('"', '\\"')) return string.format('"%s"', text:sub(2, #text - 1):gsub('"', '\\"'))
end end
---@param w integer
---@return boolean closed Whether the window was closed.
local function close_win(w)
if api.nvim_win_is_valid(w) then
api.nvim_win_close(w, true)
return true
end
return false
end
---@param w integer
---@param b integer
local function set_dev_properties(w, b)
vim.wo[w].scrolloff = 5
vim.wo[w].wrap = false
vim.wo[w].foldmethod = 'manual' -- disable folding
vim.bo[b].buflisted = false
vim.bo[b].buftype = 'nofile'
vim.bo[b].bufhidden = 'wipe'
vim.bo[b].filetype = 'query'
end
--- Write the contents of this View into {bufnr}. --- Write the contents of this View into {bufnr}.
--- ---
---@param bufnr integer Buffer number to write into. ---@param bufnr integer Buffer number to write into.
@@ -247,12 +270,9 @@ function M.inspect_tree(opts)
local win = api.nvim_get_current_win() local win = api.nvim_get_current_win()
local pg = assert(TSTreeView:new(buf, opts.lang)) local pg = assert(TSTreeView:new(buf, opts.lang))
-- Close any existing dev window -- Close any existing inspector window
if vim.b[buf].dev then if vim.b[buf].dev_inspect then
local w = vim.b[buf].dev close_win(vim.b[buf].dev_inspect)
if api.nvim_win_is_valid(w) then
api.nvim_win_close(w, true)
end
end end
local w = opts.winid local w = opts.winid
@@ -268,16 +288,10 @@ function M.inspect_tree(opts)
b = api.nvim_win_get_buf(w) b = api.nvim_win_get_buf(w)
end end
vim.b[buf].dev = w vim.b[buf].dev_inspect = w
vim.b[b].dev_base = win -- base window handle
vim.wo[w].scrolloff = 5
vim.wo[w].wrap = false
vim.wo[w].foldmethod = 'manual' -- disable folding
vim.bo[b].buflisted = false
vim.bo[b].buftype = 'nofile'
vim.bo[b].bufhidden = 'wipe'
vim.b[b].disable_query_linter = true vim.b[b].disable_query_linter = true
vim.bo[b].filetype = 'query' set_dev_properties(w, b)
local title --- @type string? local title --- @type string?
local opts_title = opts.title local opts_title = opts.title
@@ -306,7 +320,7 @@ function M.inspect_tree(opts)
api.nvim_buf_set_keymap(b, 'n', 'a', '', { api.nvim_buf_set_keymap(b, 'n', 'a', '', {
desc = 'Toggle anonymous nodes', desc = 'Toggle anonymous nodes',
callback = function() callback = function()
local row, col = unpack(api.nvim_win_get_cursor(w)) local row, col = unpack(api.nvim_win_get_cursor(w)) ---@type integer, integer
local curnode = pg:get(row) local curnode = pg:get(row)
while curnode and not curnode.named do while curnode and not curnode.named do
row = row - 1 row = row - 1
@@ -336,6 +350,15 @@ function M.inspect_tree(opts)
pg:draw(b) pg:draw(b)
end, end,
}) })
api.nvim_buf_set_keymap(b, 'n', 'o', '', {
desc = 'Toggle query previewer',
callback = function()
local preview_w = vim.b[buf].dev_preview
if not preview_w or not close_win(preview_w) then
M.preview_query()
end
end,
})
local group = api.nvim_create_augroup('treesitter/dev', {}) local group = api.nvim_create_augroup('treesitter/dev', {})
@@ -436,11 +459,148 @@ function M.inspect_tree(opts)
buffer = buf, buffer = buf,
once = true, once = true,
callback = function() callback = function()
if api.nvim_win_is_valid(w) then close_win(w)
api.nvim_win_close(w, true)
end
end, end,
}) })
end end
local preview_ns = api.nvim_create_namespace('treesitter/dev-preview')
---@param query_win integer
---@param base_win integer
local function update_preview_highlights(query_win, base_win)
local base_buf = api.nvim_win_get_buf(base_win)
local query_buf = api.nvim_win_get_buf(query_win)
local parser = vim.treesitter.get_parser(base_buf)
local lang = parser:lang()
api.nvim_buf_clear_namespace(base_buf, preview_ns, 0, -1)
local query_content = table.concat(api.nvim_buf_get_lines(query_buf, 0, -1, false), '\n')
local ok_query, query = pcall(vim.treesitter.query.parse, lang, query_content)
if not ok_query then
return
end
local cursor_word = vim.fn.expand('<cword>') --[[@as string]]
-- Only highlight captures if the cursor is on a capture name
if cursor_word:find('^@') == nil then
return
end
-- Remove the '@' from the cursor word
cursor_word = cursor_word:sub(2)
local topline, botline = vim.fn.line('w0', base_win), vim.fn.line('w$', base_win)
for id, node in query:iter_captures(parser:trees()[1]:root(), base_buf, topline - 1, botline) do
local capture_name = query.captures[id]
if capture_name == cursor_word then
local lnum, col, end_lnum, end_col = node:range()
api.nvim_buf_set_extmark(base_buf, preview_ns, lnum, col, {
end_row = end_lnum,
end_col = end_col,
hl_group = 'Visual',
virt_text = {
{ capture_name, 'Title' },
},
})
end
end
end
--- @private
function M.preview_query()
local buf = api.nvim_get_current_buf()
local win = api.nvim_get_current_win()
-- Close any existing previewer window
if vim.b[buf].dev_preview then
close_win(vim.b[buf].dev_preview)
end
local cmd = '60vnew'
-- If the inspector is open, place the previewer above it.
local base_win = vim.b[buf].dev_base ---@type integer?
local base_buf = base_win and api.nvim_win_get_buf(base_win)
local inspect_win = base_buf and vim.b[base_buf].dev_inspect
if base_win and base_buf and api.nvim_win_is_valid(inspect_win) then
vim.api.nvim_set_current_win(inspect_win)
buf = base_buf
win = base_win
cmd = 'new'
end
vim.cmd(cmd)
local ok, parser = pcall(vim.treesitter.get_parser, buf)
if not ok then
return nil, 'No parser available for the given buffer'
end
local lang = parser:lang()
local query_win = api.nvim_get_current_win()
local query_buf = api.nvim_win_get_buf(query_win)
vim.b[buf].dev_preview = query_win
vim.bo[query_buf].omnifunc = 'v:lua.vim.treesitter.query.omnifunc'
set_dev_properties(query_win, query_buf)
-- Note that omnifunc guesses the language based on the containing folder,
-- so we add the parser's language to the buffer's name so that omnifunc
-- can infer the language later.
api.nvim_buf_set_name(query_buf, string.format('%s/query_previewer.scm', lang))
local group = api.nvim_create_augroup('treesitter/dev-preview', {})
api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, {
group = group,
buffer = query_buf,
desc = 'Update query previewer diagnostics when the query changes',
callback = function()
vim.treesitter.query.lint(query_buf, { langs = lang, clear = false })
end,
})
api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave', 'CursorMoved', 'BufEnter' }, {
group = group,
buffer = query_buf,
desc = 'Update query previewer highlights when the cursor moves',
callback = function()
update_preview_highlights(query_win, win)
end,
})
api.nvim_create_autocmd('BufLeave', {
group = group,
buffer = query_buf,
desc = 'Clear the query previewer highlights when leaving the previewer',
callback = function()
api.nvim_buf_clear_namespace(buf, preview_ns, 0, -1)
end,
})
api.nvim_create_autocmd('BufLeave', {
group = group,
buffer = buf,
desc = 'Clear the query previewer highlights when leaving the source buffer',
callback = function()
if not api.nvim_buf_is_loaded(query_buf) then
return true
end
api.nvim_buf_clear_namespace(query_buf, preview_ns, 0, -1)
end,
})
api.nvim_create_autocmd('BufHidden', {
group = group,
buffer = buf,
desc = 'Close the previewer window when the source buffer is hidden',
once = true,
callback = function()
close_win(query_win)
end,
})
api.nvim_buf_set_lines(query_buf, 0, -1, false, {
';; Write your query here. Use @captures to highlight matches in the source buffer.',
';; Completion for grammar nodes is available (see :h compl-omni)',
'',
'',
})
vim.cmd('normal! G')
vim.cmd.startinsert()
end
return M return M

View File

@@ -18,3 +18,7 @@ vim.api.nvim_create_user_command('InspectTree', function(cmd)
vim.treesitter.inspect_tree() vim.treesitter.inspect_tree()
end end
end, { desc = 'Inspect treesitter language tree for buffer', count = true }) end, { desc = 'Inspect treesitter language tree for buffer', count = true })
vim.api.nvim_create_user_command('PreviewQuery', function()
vim.treesitter.preview_query()
end, { desc = 'Preview treesitter query' })