neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

dev.lua (22838B)


      1 local api = vim.api
      2 
      3 local Range = require('vim.treesitter._range')
      4 
      5 local M = {}
      6 
      7 ---@class (private) vim.treesitter.dev.TSTreeView
      8 ---@field ns integer API namespace
      9 ---@field opts vim.treesitter.dev.TSTreeViewOpts
     10 ---@field nodes vim.treesitter.dev.Node[]
     11 ---@field named vim.treesitter.dev.Node[]
     12 local TSTreeView = {}
     13 
     14 ---@private
     15 ---@class (private) vim.treesitter.dev.TSTreeViewOpts
     16 ---@field anon boolean If true, display anonymous nodes.
     17 ---@field lang boolean If true, display the language alongside each node.
     18 ---@field indent integer Number of spaces to indent nested lines.
     19 
     20 ---@class (private) vim.treesitter.dev.Node
     21 ---@field node TSNode Treesitter node
     22 ---@field field string? Node field
     23 ---@field depth integer Depth of this node in the tree
     24 ---@field text string? Text displayed in the inspector for this node. Not computed until the
     25 ---                    inspector is drawn.
     26 ---@field lang string Source language of this node
     27 
     28 ---@class (private) vim.treesitter.dev.Injection
     29 ---@field lang string Source language of this injection
     30 ---@field root TSNode Root node of the injection
     31 
     32 --- Traverse all child nodes starting at {node}.
     33 ---
     34 --- This is a recursive function. The {depth} parameter indicates the current recursion level.
     35 --- {lang} is a string indicating the language of the tree currently being traversed. Each traversed
     36 --- node is added to {tree}. When recursion completes, {tree} is an array of all nodes in the order
     37 --- they were visited.
     38 ---
     39 --- {injections} is a table mapping node ids from the primary tree to language tree injections. Each
     40 --- injected language has a series of trees nested within the primary language's tree, and the root
     41 --- node of each of these trees is contained within a node in the primary tree. The {injections}
     42 --- table maps nodes in the primary tree to root nodes of injected trees.
     43 ---
     44 ---@param node TSNode Starting node to begin traversal |tsnode|
     45 ---@param depth integer Current recursion depth
     46 ---@param field string|nil The field of the current node
     47 ---@param lang string Language of the tree currently being traversed
     48 ---@param injections table<string, vim.treesitter.dev.Injection[]> Mapping of node ids to root nodes
     49 ---                  of injected language trees (see explanation above)
     50 ---@param tree vim.treesitter.dev.Node[] Output table containing a list of tables each representing a node in the tree
     51 local function traverse(node, depth, field, lang, injections, tree)
     52  table.insert(tree, {
     53    node = node,
     54    depth = depth,
     55    lang = lang,
     56    field = field,
     57  })
     58 
     59  for _, injection in ipairs(injections[node:id()] or {}) do
     60    traverse(injection.root, depth + 1, nil, injection.lang, injections, tree)
     61  end
     62 
     63  for child, child_field in node:iter_children() do
     64    traverse(child, depth + 1, child_field, lang, injections, tree)
     65  end
     66 
     67  return tree
     68 end
     69 
     70 --- Create a new treesitter view.
     71 ---
     72 ---@param bufnr integer Source buffer number
     73 ---@param lang string|nil Language of source buffer
     74 ---
     75 ---@return vim.treesitter.dev.TSTreeView|nil
     76 ---@return string|nil Error message, if any
     77 ---
     78 ---@package
     79 function TSTreeView:new(bufnr, lang)
     80  bufnr = bufnr or 0
     81  lang = lang or vim.treesitter.language.get_lang(vim.bo[bufnr].filetype)
     82  local parser = vim.treesitter.get_parser(bufnr, lang, { error = false })
     83  if not parser then
     84    return nil,
     85      string.format(
     86        'Failed to create TSTreeView for buffer %s: no parser for lang "%s"',
     87        bufnr,
     88        lang
     89      )
     90  end
     91 
     92  -- For each child tree (injected language), find the root of the tree and locate the node within
     93  -- the primary tree that contains that root. Add a mapping from the node in the primary tree to
     94  -- the root in the child tree to the {injections} table.
     95  local root = parser:parse(true)[1]:root()
     96  local injections = {} ---@type table<string, table<string, TSNode>>
     97 
     98  parser:for_each_tree(function(parent_tree, parent_ltree)
     99    local parent = parent_tree:root()
    100    local parent_range = { parent:range() }
    101    for _, child in pairs(parent_ltree:children()) do
    102      for _, tree in pairs(child:trees()) do
    103        local r = tree:root()
    104        local r_range = { r:range() }
    105        if Range.contains(parent_range, r_range) then
    106          local node = assert(parent:named_descendant_for_range(r:range()))
    107          local id = node:id()
    108          local ilang = child:lang()
    109          injections[id] = injections[id] or {}
    110          local injection = injections[id][ilang]
    111          if not injection or r:byte_length() > injection:byte_length() then
    112            injections[id][ilang] = r
    113          end
    114        end
    115      end
    116    end
    117  end)
    118 
    119  local sorted_injections = {} ---@type table<string, vim.treesitter.dev.Injection[]>
    120  for id, lang_injections in pairs(injections) do
    121    local langs = vim.tbl_keys(lang_injections)
    122    ---@param a string
    123    ---@param b string
    124    table.sort(langs, function(a, b)
    125      return lang_injections[a]:byte_length() > lang_injections[b]:byte_length()
    126    end)
    127    ---@param ilang string
    128    sorted_injections[id] = vim.tbl_map(function(ilang)
    129      return { lang = ilang, root = lang_injections[ilang] }
    130    end, langs)
    131  end
    132 
    133  local nodes = traverse(root, 0, nil, parser:lang(), sorted_injections, {})
    134 
    135  local named = {} ---@type vim.treesitter.dev.Node[]
    136  for _, v in ipairs(nodes) do
    137    if v.node:named() then
    138      named[#named + 1] = v
    139    end
    140  end
    141 
    142  local t = {
    143    ns = api.nvim_create_namespace('nvim.treesitter.dev_inspect'),
    144    nodes = nodes,
    145    named = named,
    146    ---@type vim.treesitter.dev.TSTreeViewOpts
    147    opts = {
    148      anon = false,
    149      lang = false,
    150      indent = 2,
    151    },
    152  }
    153 
    154  setmetatable(t, self)
    155  self.__index = self
    156  return t
    157 end
    158 
    159 local decor_ns = api.nvim_create_namespace('nvim.treesitter.dev')
    160 
    161 ---@param w integer
    162 ---@return boolean closed Whether the window was closed.
    163 local function close_win(w)
    164  if api.nvim_win_is_valid(w) then
    165    api.nvim_win_close(w, true)
    166    return true
    167  end
    168 
    169  return false
    170 end
    171 
    172 ---@param w integer
    173 ---@param b integer
    174 ---@param opts nil|{ indent?: integer }
    175 local function set_dev_options(w, b, opts)
    176  vim.wo[w][0].scrolloff = 5
    177  vim.wo[w][0].wrap = false
    178  vim.wo[w][0].foldmethod = 'expr'
    179  vim.wo[w][0].foldexpr = 'v:lua.vim.treesitter.foldexpr()' -- explicitly set foldexpr
    180  vim.wo[w][0].foldenable = false -- Don't fold on first open InspectTree
    181  vim.wo[w][0].foldlevel = 99
    182  vim.bo[b].buflisted = false
    183  vim.bo[b].buftype = 'nofile'
    184  vim.bo[b].bufhidden = 'wipe'
    185  vim.bo[b].filetype = 'query'
    186  vim.bo[b].swapfile = false
    187 
    188  opts = opts or {}
    189  if opts.indent then
    190    vim.bo[b].shiftwidth = opts.indent
    191  end
    192 end
    193 
    194 --- Updates the cursor position in the inspector to match the node under the cursor.
    195 ---
    196 --- @param treeview vim.treesitter.dev.TSTreeView
    197 --- @param lang string
    198 --- @param source_buf integer
    199 --- @param inspect_buf integer
    200 --- @param inspect_win integer
    201 --- @param pos? [integer, integer]
    202 local function set_inspector_cursor(treeview, lang, source_buf, inspect_buf, inspect_win, pos)
    203  api.nvim_buf_clear_namespace(inspect_buf, treeview.ns, 0, -1)
    204 
    205  local cursor_node = vim.treesitter.get_node({
    206    bufnr = source_buf,
    207    lang = lang,
    208    pos = pos,
    209    ignore_injections = false,
    210    include_anonymous = treeview.opts.anon,
    211  })
    212  if not cursor_node then
    213    return
    214  end
    215 
    216  local cursor_node_id = cursor_node:id()
    217  for i, v in treeview:iter() do
    218    if v.node:id() == cursor_node_id then
    219      local start = v.depth * treeview.opts.indent ---@type integer
    220      local end_col = start + #v.text
    221      api.nvim_buf_set_extmark(inspect_buf, treeview.ns, i - 1, start, {
    222        end_col = end_col,
    223        hl_group = 'Visual',
    224      })
    225      api.nvim_win_set_cursor(inspect_win, { i, 0 })
    226      break
    227    end
    228  end
    229 end
    230 
    231 --- Write the contents of this View into {bufnr}.
    232 ---
    233 --- Calling this function computes the text that is displayed for each node.
    234 ---
    235 ---@param bufnr integer Buffer number to write into.
    236 ---@package
    237 function TSTreeView:draw(bufnr)
    238  vim.bo[bufnr].modifiable = true
    239  local lines = {} ---@type string[]
    240  local lang_hl_marks = {} ---@type table[]
    241 
    242  for i, item in self:iter() do
    243    local range_str = ('[%d, %d] - [%d, %d]'):format(item.node:range())
    244    local lang_str = self.opts.lang and string.format(' %s', item.lang) or ''
    245 
    246    local text ---@type string
    247    if item.node:named() then
    248      text = string.format('(%s%s', item.node:missing() and 'MISSING ' or '', item.node:type())
    249    else
    250      text = string.format('%q', item.node:type()):gsub('\n', 'n')
    251      if item.node:missing() then
    252        text = string.format('(MISSING %s)', text)
    253      end
    254    end
    255    if item.field then
    256      text = string.format('%s: %s', item.field, text)
    257    end
    258 
    259    local next = self:get(i + 1)
    260    if not next or next.depth <= item.depth then
    261      local parens = item.depth - (next and next.depth or 0) + (item.node:named() and 1 or 0)
    262      if parens > 0 then
    263        text = string.format('%s%s', text, string.rep(')', parens))
    264      end
    265    end
    266 
    267    item.text = text
    268 
    269    local line = string.format(
    270      '%s%s ; %s%s',
    271      string.rep(' ', item.depth * self.opts.indent),
    272      text,
    273      range_str,
    274      lang_str
    275    )
    276 
    277    if self.opts.lang then
    278      lang_hl_marks[#lang_hl_marks + 1] = {
    279        col = #line - #lang_str,
    280        end_col = #line,
    281      }
    282    end
    283 
    284    lines[i] = line
    285  end
    286 
    287  api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
    288 
    289  api.nvim_buf_clear_namespace(bufnr, decor_ns, 0, -1)
    290 
    291  for i, m in ipairs(lang_hl_marks) do
    292    api.nvim_buf_set_extmark(bufnr, decor_ns, i - 1, m.col, {
    293      hl_group = 'Title',
    294      end_col = m.end_col,
    295    })
    296  end
    297 
    298  vim.bo[bufnr].modifiable = false
    299 end
    300 
    301 --- Get node {i} from this View.
    302 ---
    303 --- The node number is dependent on whether or not anonymous nodes are displayed.
    304 ---
    305 ---@param i integer Node number to get
    306 ---@return vim.treesitter.dev.Node?
    307 ---@package
    308 function TSTreeView:get(i)
    309  local t = self.opts.anon and self.nodes or self.named
    310  return t[i]
    311 end
    312 
    313 --- Iterate over all of the nodes in this View.
    314 ---
    315 ---@return (fun(): integer, vim.treesitter.dev.Node) Iterator over all nodes in this View
    316 ---@return table
    317 ---@return integer
    318 ---@package
    319 function TSTreeView:iter()
    320  return ipairs(self.opts.anon and self.nodes or self.named)
    321 end
    322 
    323 --- @class vim.treesitter.dev.inspect_tree.Opts
    324 --- @inlinedoc
    325 ---
    326 --- The language of the source buffer. If omitted, the filetype of the source
    327 --- buffer is used.
    328 --- @field lang string?
    329 ---
    330 --- Buffer to draw the tree into. If omitted, a new buffer is created.
    331 --- @field bufnr integer?
    332 ---
    333 --- Window id to display the tree buffer in. If omitted, a new window is
    334 --- created with {command}.
    335 --- @field winid integer?
    336 ---
    337 --- Vimscript command to create the window. Default value is "60vnew".
    338 --- Only used when {winid} is nil.
    339 --- @field command string?
    340 ---
    341 --- Title of the window. If a function, it accepts the buffer number of the
    342 --- source buffer as its only argument and should return a string.
    343 --- @field title (string|fun(bufnr:integer):string|nil)
    344 
    345 --- @nodoc
    346 --- @param opts vim.treesitter.dev.inspect_tree.Opts?
    347 function M.inspect_tree(opts)
    348  vim.validate('opts', opts, 'table', true)
    349 
    350  opts = opts or {}
    351 
    352  -- source buffer
    353  local buf = api.nvim_get_current_buf()
    354 
    355  -- window id for source buffer
    356  local win = api.nvim_get_current_win()
    357  local treeview, err = TSTreeView:new(buf, opts.lang)
    358  if err and err:match('no parser for lang') then
    359    api.nvim_echo({ { err, 'WarningMsg' } }, true, {})
    360    return
    361  elseif not treeview then
    362    error(err)
    363  end
    364 
    365  -- Close any existing inspector window
    366  if vim.b[buf].dev_inspect then
    367    close_win(vim.b[buf].dev_inspect)
    368  end
    369 
    370  -- window id for tree buffer
    371  local w = opts.winid
    372  if not w then
    373    vim.cmd(opts.command or '60vnew')
    374    w = api.nvim_get_current_win()
    375  end
    376 
    377  -- tree buffer
    378  local b = opts.bufnr
    379  if b then
    380    api.nvim_win_set_buf(w, b)
    381  else
    382    b = api.nvim_win_get_buf(w)
    383  end
    384 
    385  vim.b[buf].dev_inspect = w
    386  vim.b[b].dev_base = win -- base window handle
    387  vim.b[b].disable_query_linter = true
    388  set_dev_options(w, b, { indent = treeview.opts.indent })
    389 
    390  local title --- @type string?
    391  local opts_title = opts.title
    392  if not opts_title then
    393    local bufname = api.nvim_buf_get_name(buf)
    394    title = ('Syntax tree for %s'):format(vim.fs.relpath('.', bufname) or bufname)
    395  elseif type(opts_title) == 'function' then
    396    title = opts_title(buf)
    397  end
    398 
    399  assert(type(title) == 'string', 'Window title must be a string')
    400  api.nvim_buf_set_name(b, title)
    401 
    402  treeview:draw(b)
    403 
    404  local cursor = api.nvim_win_get_cursor(win)
    405  set_inspector_cursor(treeview, opts.lang, buf, b, w, { cursor[1] - 1, cursor[2] })
    406 
    407  api.nvim_buf_clear_namespace(buf, treeview.ns, 0, -1)
    408  api.nvim_buf_set_keymap(b, 'n', '<CR>', '', {
    409    desc = 'Jump to the node under the cursor in the source buffer',
    410    nowait = true,
    411    callback = function()
    412      local row = api.nvim_win_get_cursor(w)[1]
    413      local lnum, col = treeview:get(row).node:start()
    414 
    415      -- update source window if original was closed
    416      if not api.nvim_win_is_valid(win) then
    417        win = assert(vim.fn.win_findbuf(buf)[1])
    418      end
    419 
    420      api.nvim_set_current_win(win)
    421      api.nvim_win_set_cursor(win, { lnum + 1, col })
    422    end,
    423  })
    424  api.nvim_buf_set_keymap(b, 'n', 'a', '', {
    425    desc = 'Toggle anonymous nodes',
    426    nowait = true,
    427    callback = function()
    428      local row, col = unpack(api.nvim_win_get_cursor(w)) ---@type integer, integer
    429      local curnode = treeview:get(row)
    430      while curnode and not curnode.node:named() do
    431        row = row - 1
    432        curnode = treeview:get(row)
    433      end
    434 
    435      treeview.opts.anon = not treeview.opts.anon
    436      treeview:draw(b)
    437 
    438      if not curnode then
    439        return
    440      end
    441 
    442      local id = curnode.node:id()
    443      for i, node in treeview:iter() do
    444        if node.node:id() == id then
    445          api.nvim_win_set_cursor(w, { i, col })
    446          break
    447        end
    448      end
    449    end,
    450  })
    451  api.nvim_buf_set_keymap(b, 'n', 'I', '', {
    452    desc = 'Toggle language display',
    453    nowait = true,
    454    callback = function()
    455      treeview.opts.lang = not treeview.opts.lang
    456      treeview:draw(b)
    457    end,
    458  })
    459  api.nvim_buf_set_keymap(b, 'n', 'o', '', {
    460    desc = 'Toggle query editor',
    461    nowait = true,
    462    callback = function()
    463      local edit_w = vim.b[buf].dev_edit
    464      if not edit_w or not close_win(edit_w) then
    465        M.edit_query()
    466      end
    467    end,
    468  })
    469  api.nvim_buf_set_keymap(b, 'n', 'q', '<Cmd>wincmd c<CR>', {
    470    desc = 'Close language tree window',
    471    nowait = true,
    472  })
    473 
    474  local group = api.nvim_create_augroup('nvim.treesitter.dev', {})
    475 
    476  api.nvim_create_autocmd('CursorMoved', {
    477    group = group,
    478    buffer = b,
    479    callback = function()
    480      if not api.nvim_buf_is_loaded(buf) then
    481        return true
    482      end
    483 
    484      w = api.nvim_get_current_win()
    485      api.nvim_buf_clear_namespace(buf, treeview.ns, 0, -1)
    486      local row = api.nvim_win_get_cursor(w)[1]
    487      local lnum, col, end_lnum, end_col = treeview:get(row).node:range()
    488      api.nvim_buf_set_extmark(buf, treeview.ns, lnum, col, {
    489        end_row = end_lnum,
    490        end_col = math.max(0, end_col),
    491        hl_group = 'Visual',
    492      })
    493 
    494      -- update source window if original was closed
    495      if not api.nvim_win_is_valid(win) then
    496        win = assert(vim.fn.win_findbuf(buf)[1])
    497      end
    498 
    499      local topline, botline = vim.fn.line('w0', win), vim.fn.line('w$', win)
    500 
    501      -- Move the cursor if highlighted range is completely out of view
    502      if lnum < topline and end_lnum < topline then
    503        api.nvim_win_set_cursor(win, { end_lnum + 1, 0 })
    504      elseif lnum > botline and end_lnum > botline then
    505        api.nvim_win_set_cursor(win, { lnum + 1, 0 })
    506      end
    507    end,
    508  })
    509 
    510  api.nvim_create_autocmd('CursorMoved', {
    511    group = group,
    512    buffer = buf,
    513    callback = function()
    514      if not api.nvim_buf_is_loaded(b) then
    515        return true
    516      end
    517 
    518      set_inspector_cursor(treeview, opts.lang, buf, b, w)
    519    end,
    520  })
    521 
    522  api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, {
    523    group = group,
    524    buffer = buf,
    525    callback = function()
    526      if not api.nvim_buf_is_loaded(b) then
    527        return true
    528      end
    529 
    530      local treeview_opts = treeview.opts
    531      treeview = assert(TSTreeView:new(buf, opts.lang))
    532      treeview.opts = treeview_opts
    533      treeview:draw(b)
    534    end,
    535  })
    536 
    537  api.nvim_create_autocmd('BufLeave', {
    538    group = group,
    539    buffer = b,
    540    callback = function()
    541      if not api.nvim_buf_is_loaded(buf) then
    542        return true
    543      end
    544      api.nvim_buf_clear_namespace(buf, treeview.ns, 0, -1)
    545    end,
    546  })
    547 
    548  api.nvim_create_autocmd('BufLeave', {
    549    group = group,
    550    buffer = buf,
    551    callback = function()
    552      if not api.nvim_buf_is_loaded(b) then
    553        return true
    554      end
    555      api.nvim_buf_clear_namespace(b, treeview.ns, 0, -1)
    556    end,
    557  })
    558 
    559  api.nvim_create_autocmd({ 'BufHidden', 'BufUnload', 'QuitPre' }, {
    560    group = group,
    561    buffer = buf,
    562    callback = function()
    563      -- don't close inpector window if source buffer
    564      -- has more than one open window
    565      if #vim.fn.win_findbuf(buf) > 1 then
    566        return
    567      end
    568 
    569      -- close all tree windows
    570      for _, window in pairs(vim.fn.win_findbuf(b)) do
    571        close_win(window)
    572      end
    573 
    574      return true
    575    end,
    576  })
    577 end
    578 
    579 local edit_ns = api.nvim_create_namespace('nvim.treesitter.dev_edit')
    580 
    581 ---@param query_win integer
    582 ---@param base_win integer
    583 ---@param lang string
    584 local function update_editor_highlights(query_win, base_win, lang)
    585  local base_buf = api.nvim_win_get_buf(base_win)
    586  local query_buf = api.nvim_win_get_buf(query_win)
    587  local root_lang = vim.treesitter.language.get_lang(vim.bo[base_buf].filetype)
    588  local parser = assert(vim.treesitter.get_parser(base_buf, root_lang, { error = false }))
    589  api.nvim_buf_clear_namespace(base_buf, edit_ns, 0, -1)
    590  local query_content = table.concat(api.nvim_buf_get_lines(query_buf, 0, -1, false), '\n')
    591 
    592  local ok_query, query = pcall(vim.treesitter.query.parse, lang, query_content)
    593  if not ok_query then
    594    return
    595  end
    596 
    597  local cursor_word = vim.fn.expand('<cword>') --[[@as string]]
    598  -- Only highlight captures if the cursor is on a capture name
    599  if cursor_word:find('^@') == nil then
    600    return
    601  end
    602  -- Remove the '@' from the cursor word
    603  cursor_word = cursor_word:sub(2)
    604  -- Parse buffer including injected languages.
    605  parser:parse(true)
    606  -- Query on the trees of the language requested to highlight captures.
    607  parser:for_each_tree(function(tree, ltree)
    608    if ltree:lang() ~= lang then
    609      return
    610    end
    611    local root = tree:root()
    612    local topline, botline = vim.fn.line('w0', base_win), vim.fn.line('w$', base_win)
    613    for id, node, metadata in query:iter_captures(root, base_buf, topline - 1, botline) do
    614      local capture_name = query.captures[id]
    615      if capture_name == cursor_word then
    616        local lnum, col, end_lnum, end_col =
    617          Range.unpack4(vim.treesitter.get_range(node, base_buf, metadata[id]))
    618 
    619        api.nvim_buf_set_extmark(base_buf, edit_ns, lnum, col, {
    620          end_row = end_lnum,
    621          end_col = end_col,
    622          hl_group = 'Visual',
    623          virt_text = {
    624            { capture_name, 'DiagnosticVirtualTextHint' },
    625          },
    626        })
    627      end
    628    end
    629  end)
    630 end
    631 
    632 --- @nodoc
    633 --- @param lang? string language to open the query editor for.
    634 --- @return boolean? `true` on success, `nil` on failure
    635 --- @return string? error message, if applicable
    636 function M.edit_query(lang)
    637  local buf = api.nvim_get_current_buf()
    638  local win = api.nvim_get_current_win()
    639 
    640  -- Close any existing editor window
    641  if vim.b[buf].dev_edit then
    642    close_win(vim.b[buf].dev_edit)
    643  end
    644 
    645  local cmd = '60vnew'
    646  -- If the inspector is open, place the editor above it.
    647  local base_win = vim.b[buf].dev_base ---@type integer?
    648  local base_buf = base_win and api.nvim_win_get_buf(base_win)
    649  local inspect_win = base_buf and vim.b[base_buf].dev_inspect
    650  if base_win and base_buf and api.nvim_win_is_valid(inspect_win) then
    651    api.nvim_set_current_win(inspect_win)
    652    buf = base_buf
    653    win = base_win
    654    cmd = 'new'
    655  end
    656  vim.cmd(cmd)
    657 
    658  local parser = vim.treesitter.get_parser(buf, lang, { error = false })
    659  if not parser then
    660    return nil,
    661      string.format('Failed to show query editor for buffer %s: no parser for lang "%s"', buf, lang)
    662  end
    663  lang = parser:lang()
    664 
    665  local query_win = api.nvim_get_current_win()
    666  local query_buf = api.nvim_win_get_buf(query_win)
    667 
    668  vim.b[buf].dev_edit = query_win
    669  vim.bo[query_buf].omnifunc = 'v:lua.vim.treesitter.query.omnifunc'
    670  set_dev_options(query_win, query_buf)
    671 
    672  -- Note that omnifunc guesses the language based on the containing folder,
    673  -- so we add the parser's language to the buffer's name so that omnifunc
    674  -- can infer the language later.
    675  api.nvim_buf_set_name(query_buf, string.format('%s/query_editor.scm', lang))
    676 
    677  local group = api.nvim_create_augroup('nvim.treesitter.dev_edit', {})
    678  api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, {
    679    group = group,
    680    buffer = query_buf,
    681    desc = 'Update query editor diagnostics when the query changes',
    682    callback = function()
    683      vim.treesitter.query.lint(query_buf, { langs = lang, clear = false })
    684    end,
    685  })
    686  api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave', 'CursorMoved', 'BufEnter' }, {
    687    group = group,
    688    buffer = query_buf,
    689    desc = 'Update query editor highlights when the cursor moves',
    690    callback = function()
    691      if api.nvim_win_is_valid(win) then
    692        update_editor_highlights(query_win, win, lang)
    693      end
    694    end,
    695  })
    696  api.nvim_create_autocmd('BufLeave', {
    697    group = group,
    698    buffer = query_buf,
    699    desc = 'Clear highlights when leaving the query editor',
    700    callback = function()
    701      api.nvim_buf_clear_namespace(buf, edit_ns, 0, -1)
    702    end,
    703  })
    704  api.nvim_create_autocmd('BufLeave', {
    705    group = group,
    706    buffer = buf,
    707    desc = 'Clear the query editor highlights when leaving the source buffer',
    708    callback = function()
    709      if not api.nvim_buf_is_loaded(query_buf) then
    710        return true
    711      end
    712 
    713      api.nvim_buf_clear_namespace(query_buf, edit_ns, 0, -1)
    714    end,
    715  })
    716  api.nvim_create_autocmd({ 'BufHidden', 'BufUnload' }, {
    717    group = group,
    718    buffer = buf,
    719    desc = 'Close the editor window when the source buffer is hidden or unloaded',
    720    once = true,
    721    callback = function()
    722      close_win(query_win)
    723    end,
    724  })
    725 
    726  api.nvim_buf_set_lines(query_buf, 0, -1, false, {
    727    ';; Write queries here (see $VIMRUNTIME/queries/ for examples).',
    728    ';; Move cursor to a capture ("@foo") to highlight matches in the source buffer.',
    729    ';; Completion for grammar nodes is available (:help compl-omni)',
    730    '',
    731    '',
    732  })
    733  vim.cmd('normal! G')
    734  vim.cmd.startinsert()
    735 
    736  return true
    737 end
    738 
    739 return M