mirror of
https://github.com/neovim/neovim.git
synced 2025-09-06 11:28:22 +00:00
feat(treesitter): add a query editor (#24703)
This commit is contained in:

committed by
GitHub

parent
ecd99e7dd7
commit
5d8ab32f38
@@ -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.)
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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' }
|
||||||
|
@@ -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()'
|
||||||
|
@@ -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
|
||||||
|
@@ -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' })
|
||||||
|
Reference in New Issue
Block a user