mirror of
https://github.com/chenasraf/vim-matchup.git
synced 2026-05-17 17:38:01 +00:00
Add neovim tree-sitter integration
This commit is contained in:
25
after/queries/python/matchup.scm
Normal file
25
after/queries/python/matchup.scm
Normal file
@@ -0,0 +1,25 @@
|
||||
(if_statement
|
||||
"if" @open.if_
|
||||
alternative: (elif_clause "elif" @mid.if_.1)?
|
||||
alternative: (else_clause "else" @mid.if_.2)?) @scope.if_
|
||||
|
||||
(function_definition
|
||||
"def" @open.function) @scope.function
|
||||
(return_statement
|
||||
"return" @mid.function.1)
|
||||
|
||||
(for_statement
|
||||
("for" @open.loop)
|
||||
alternative: (else_clause "else" @mid.loop.1)?) @scope.loop
|
||||
(while_statement
|
||||
("while" @open.loop)
|
||||
alternative: (else_clause "else" @mid.loop.1)?) @scope.loop
|
||||
(break_statement
|
||||
"break" @mid.loop.2)
|
||||
(continue_statement
|
||||
"continue" @mid.loop.3)
|
||||
|
||||
(try_statement
|
||||
("try" @open.try_)
|
||||
(finally_clause "finally" @mid.try_.1)?
|
||||
(except_clause "except" @mid.try_.2)?) @scope.try_
|
||||
@@ -96,6 +96,7 @@ function! s:init_modules()
|
||||
call s:misc_init_module()
|
||||
call s:surround_init_module()
|
||||
call s:where_init_module()
|
||||
call s:treesitter_init_module()
|
||||
endfunction
|
||||
|
||||
function! s:init_oldstyle_ops() " {{{1
|
||||
@@ -357,6 +358,15 @@ function! s:where_init_module() " {{{1
|
||||
endfunction
|
||||
|
||||
" }}}1
|
||||
function! s:treesitter_init_module() " {{{1
|
||||
if !has('nvim-0.5.0')
|
||||
return
|
||||
endif
|
||||
|
||||
lua require'treesitter-matchup'.init()
|
||||
endfunction
|
||||
|
||||
"}}}1
|
||||
|
||||
let &cpo = s:save_cpo
|
||||
|
||||
|
||||
@@ -961,6 +961,10 @@ let s:engines = {
|
||||
\ 'delim_tex' : [ function('s:parser_delim_new'), ],
|
||||
\ },
|
||||
\ },
|
||||
\ 'tree_sitter': {
|
||||
\ 'get_delim' : function('matchup#ts_engine#get_delim'),
|
||||
\ 'get_matching' : function('matchup#ts_engine#get_matching'),
|
||||
\ },
|
||||
\}
|
||||
|
||||
let &cpo = s:save_cpo
|
||||
|
||||
@@ -35,7 +35,16 @@ function! matchup#loader#init_buffer() abort " {{{1
|
||||
" enable/disable for this buffer
|
||||
let b:matchup_delim_enabled = !empty(b:matchup_delim_lists.all.regex)
|
||||
|
||||
" enable matching engines
|
||||
let b:matchup_active_engines = {}
|
||||
|
||||
if has('nvim-0.5.0') && matchup#ts_engine#is_enabled(bufnr('%'))
|
||||
for l:t in ['all', 'delim_all', 'delim_py']
|
||||
let b:matchup_active_engines[l:t]
|
||||
\ = get(b:matchup_active_engines, l:t, []) + ['tree_sitter']
|
||||
endfor
|
||||
endif
|
||||
|
||||
if b:matchup_delim_enabled
|
||||
for l:t in ['all', 'delim_all', 'delim_tex']
|
||||
let b:matchup_active_engines[l:t]
|
||||
|
||||
@@ -15,6 +15,11 @@ if get(s:, 'reload_guard', 1)
|
||||
|
||||
call matchup#init()
|
||||
|
||||
if has('nvim-0.5.0')
|
||||
silent! lua require('treesitter-matchup.reload')
|
||||
\ .reload_module('treesitter-matchup')
|
||||
endif
|
||||
|
||||
unlet s:reload_guard
|
||||
endfunction
|
||||
endif
|
||||
|
||||
57
autoload/matchup/ts_engine.vim
Normal file
57
autoload/matchup/ts_engine.vim
Normal file
@@ -0,0 +1,57 @@
|
||||
" vim match-up - even better matching
|
||||
"
|
||||
" Maintainer: Andy Massimino
|
||||
" Email: a@normed.space
|
||||
"
|
||||
|
||||
let s:save_cpo = &cpo
|
||||
set cpo&vim
|
||||
|
||||
function! s:forward(fn, ...)
|
||||
let l:ret = luaeval(
|
||||
\ 'require"treesitter-matchup.internal".' . a:fn . '(unpack(_A))',
|
||||
\ a:000)
|
||||
return l:ret
|
||||
endfunction
|
||||
|
||||
function! matchup#ts_engine#is_enabled(bufnr) abort
|
||||
if !has('nvim-0.5.0')
|
||||
return 0
|
||||
endif
|
||||
return +s:forward('is_enabled', a:bufnr)
|
||||
endfunction
|
||||
|
||||
let s:attached = {}
|
||||
|
||||
function! matchup#ts_engine#attach(bufnr, lang) abort
|
||||
let s:attached[a:bufnr] = a:lang
|
||||
endfunction
|
||||
|
||||
function! matchup#ts_engine#detach(bufnr, lang) abort
|
||||
unlet s:attached[a:bufnr]
|
||||
endfunction
|
||||
|
||||
function! matchup#ts_engine#get_delim(opts) abort
|
||||
call matchup#perf#tic('ts_engine.get_delim')
|
||||
|
||||
let l:res = s:forward('get_delim', bufnr(), a:opts)
|
||||
if empty(l:res)
|
||||
call matchup#perf#toc('ts_engine.get_delim', 'fail')
|
||||
return {}
|
||||
endif
|
||||
|
||||
let l:res.get_matching = function('matchup#ts_engine#get_matching')
|
||||
|
||||
call matchup#perf#toc('ts_engine.get_delim', 'done')
|
||||
|
||||
return l:res
|
||||
endfunction
|
||||
|
||||
function! matchup#ts_engine#get_matching(down, _) dict abort
|
||||
let l:list = s:forward('get_matching', self, a:down, bufnr())
|
||||
return l:list
|
||||
endfunction
|
||||
|
||||
let &cpo = s:save_cpo
|
||||
|
||||
" vim: fdm=marker sw=2
|
||||
21
lua/treesitter-matchup.lua
Normal file
21
lua/treesitter-matchup.lua
Normal file
@@ -0,0 +1,21 @@
|
||||
if not pcall(require, 'nvim-treesitter') then
|
||||
return {init = function() end}
|
||||
end
|
||||
|
||||
local treesitter = require 'nvim-treesitter'
|
||||
local queries = require 'nvim-treesitter.query'
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.init()
|
||||
treesitter.define_modules {
|
||||
matchup = {
|
||||
module_path = 'treesitter-matchup.internal',
|
||||
is_supported = function(lang)
|
||||
return queries.get_query(lang, 'matchup') ~= nil
|
||||
end
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
return M
|
||||
281
lua/treesitter-matchup/internal.lua
Normal file
281
lua/treesitter-matchup/internal.lua
Normal file
@@ -0,0 +1,281 @@
|
||||
local vim = vim
|
||||
local api = vim.api
|
||||
local configs = require'nvim-treesitter.configs'
|
||||
local locals = require'nvim-treesitter.locals'
|
||||
local parsers = require'nvim-treesitter.parsers'
|
||||
local queries = require'nvim-treesitter.query'
|
||||
local ts_utils = require'nvim-treesitter.ts_utils'
|
||||
local lru = require'treesitter-matchup.lru'
|
||||
local util = require'treesitter-matchup.util'
|
||||
|
||||
local M = {}
|
||||
|
||||
local cache = lru.new(20)
|
||||
|
||||
function M.is_enabled(bufnr)
|
||||
local buf = bufnr or api.nvim_get_current_buf()
|
||||
local lang = parsers.get_buf_lang(buf)
|
||||
return configs.is_enabled('matchup', lang)
|
||||
end
|
||||
|
||||
function M.get_matches(bufnr)
|
||||
return queries.get_matches(bufnr, 'matchup')
|
||||
end
|
||||
|
||||
function M.get_scopes(bufnr)
|
||||
local matches = M.get_matches(bufnr)
|
||||
|
||||
local scopes = {}
|
||||
|
||||
for _, match in ipairs(matches) do
|
||||
if match.scope then
|
||||
for key, scope in pairs(match.scope) do
|
||||
if scope.node then
|
||||
if not scopes[key] then
|
||||
scopes[key] = {}
|
||||
end
|
||||
table.insert(scopes[key], scope.node)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return scopes
|
||||
end
|
||||
|
||||
function M.get_active_nodes(bufnr)
|
||||
local matches = M.get_matches(bufnr)
|
||||
|
||||
local nodes = { open = {}, mid = {}, close = {} }
|
||||
local symbols = {}
|
||||
|
||||
for _, match in ipairs(matches) do
|
||||
if match.open then
|
||||
for key, open in pairs(match.open) do
|
||||
if open.node and symbols[open.node:id()] == nil then
|
||||
table.insert(nodes.open, open.node)
|
||||
symbols[open.node:id()] = key
|
||||
end
|
||||
end
|
||||
end
|
||||
if match.close then
|
||||
for key, close in pairs(match.close) do
|
||||
if close.node and symbols[close.node:id()] == nil then
|
||||
table.insert(nodes.close, close.node)
|
||||
symbols[close.node:id()] = key
|
||||
end
|
||||
end
|
||||
end
|
||||
if match.mid then
|
||||
for key, mid_group in pairs(match.mid) do
|
||||
for _, mid in pairs(mid_group) do
|
||||
if mid.node and symbols[mid.node:id()] == nil then
|
||||
table.insert(nodes.mid, mid.node)
|
||||
symbols[mid.node:id()] = key
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nodes, symbols
|
||||
end
|
||||
|
||||
function M.containing_scope(node, bufnr, key)
|
||||
local bufnr = bufnr or api.nvim_get_current_buf()
|
||||
|
||||
local scopes = M.get_scopes(bufnr)
|
||||
if not node or not scopes then return end
|
||||
|
||||
local iter_node = node
|
||||
|
||||
while iter_node ~= nil do
|
||||
if scopes[key] and vim.tbl_contains(scopes[key], iter_node) then
|
||||
return iter_node
|
||||
end
|
||||
iter_node = iter_node:parent()
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function M.active_node(node, bufnr)
|
||||
local bufnr = bufnr or api.nvim_get_current_buf()
|
||||
|
||||
local scopes, symbols = M.get_active_nodes(bufnr)
|
||||
if not node or not scopes then return end
|
||||
|
||||
local iter_node = node
|
||||
|
||||
while iter_node ~= nil do
|
||||
for side, _ in pairs(scopes) do
|
||||
if vim.tbl_contains(scopes[side], iter_node) then
|
||||
return iter_node, side, symbols[iter_node:id()]
|
||||
end
|
||||
end
|
||||
iter_node = iter_node:parent()
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function M.get_node_at_cursor(winnr)
|
||||
if not parsers.has_parser() then return end
|
||||
local cursor = api.nvim_win_get_cursor(winnr or 0)
|
||||
local root = parsers.get_parser():parse()[1]:root()
|
||||
return root:descendant_for_range(cursor[1]-1, cursor[2],
|
||||
cursor[1]-1, cursor[2])
|
||||
end
|
||||
|
||||
function M.do_node_result(initial_node, bufnr, opts)
|
||||
local node, side, key = M.active_node(initial_node, bufnr)
|
||||
if not side then
|
||||
return nil
|
||||
end
|
||||
|
||||
local scope = M.containing_scope(initial_node, bufnr, key)
|
||||
if not scope then
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
row, col, _ = initial_node:start()
|
||||
|
||||
local result = {
|
||||
type = 'delim_py',
|
||||
match = ts_utils.get_node_text(initial_node, bufnr)[1],
|
||||
side = side,
|
||||
lnum = row + 1,
|
||||
cnum = col + 1,
|
||||
skip = 0,
|
||||
class = {key, 0},
|
||||
highlighting = opts['highlighting'],
|
||||
_id = util.uuid4(),
|
||||
}
|
||||
|
||||
local info = {
|
||||
bufnr = bufnr,
|
||||
initial_node = initial_node,
|
||||
row = row,
|
||||
key = key,
|
||||
scope = scope,
|
||||
search_range = {scope:range()},
|
||||
}
|
||||
|
||||
cache:set(result._id, info)
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local side_table = {
|
||||
open = {'open'},
|
||||
mid = {'mid'},
|
||||
close = {'close'},
|
||||
both = {'close', 'open'},
|
||||
both_all = {'close', 'mid', 'open'},
|
||||
open_mid = {'mid', 'open'},
|
||||
}
|
||||
|
||||
function M.get_delim(bufnr, opts)
|
||||
if opts.direction == 'current' then
|
||||
local node_at_cursor = M.get_node_at_cursor()
|
||||
|
||||
if node_at_cursor:named() then
|
||||
return nil
|
||||
end
|
||||
|
||||
return M.do_node_result(node_at_cursor, bufnr, opts)
|
||||
end
|
||||
|
||||
-- direction is next or prev
|
||||
local active_nodes = M.get_active_nodes(bufnr)
|
||||
|
||||
local cursor = api.nvim_win_get_cursor(0)
|
||||
local cur_pos = 1e5 * (cursor[1]-1) + cursor[2]
|
||||
local closest_node, closest_dist = nil, 1e31
|
||||
|
||||
for _, side in ipairs(side_table[opts.side]) do
|
||||
for _, node in ipairs(active_nodes[side]) do
|
||||
local row, col, _ = node:start()
|
||||
local pos = 1e5 * row + col
|
||||
|
||||
if opts.direction == 'next' and pos >= cur_pos
|
||||
or opts.direction == 'prev' and pos <= cur_pos then
|
||||
|
||||
dist = math.abs(pos - cur_pos)
|
||||
if dist < closest_dist then
|
||||
closest_dist = dist
|
||||
closest_node = node
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
if closest_node == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
return M.do_node_result(closest_node, bufnr, opts)
|
||||
|
||||
end
|
||||
|
||||
function M.get_matching(delim, down, bufnr)
|
||||
down = down > 0
|
||||
|
||||
local info = cache:get(delim._id)
|
||||
if info.bufnr ~= bufnr then
|
||||
return {}
|
||||
end
|
||||
|
||||
local matches = {}
|
||||
|
||||
local sides = down and {'mid', 'close'} or {'mid', 'open'}
|
||||
local active_nodes, symbols = M.get_active_nodes(bufnr)
|
||||
|
||||
local got_close = false
|
||||
|
||||
for _, side in ipairs(sides) do
|
||||
for _, node in ipairs(active_nodes[side]) do
|
||||
local row, col, _ = node:start()
|
||||
if info.initial_node ~= node and symbols[node:id()] == info.key
|
||||
and (down and row >= info.row
|
||||
or not down and row <= info.row)
|
||||
and (row >= info.search_range[1]
|
||||
and row <= info.search_range[3]) then
|
||||
|
||||
local scope = M.containing_scope(node, bufnr, info.key)
|
||||
|
||||
local text = ts_utils.get_node_text(node, bufnr)[1]
|
||||
table.insert(matches, {text, row + 1, col + 1})
|
||||
|
||||
if side == 'close' then
|
||||
got_close = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- sort by position
|
||||
table.sort(matches, function (a, b)
|
||||
return a[2] < b[2] or a[2] == b[2] and a[3] < b[3]
|
||||
end)
|
||||
|
||||
-- no stop marker is found, use enclosing scope
|
||||
if down and not got_close then
|
||||
row, col, _ = info.scope:end_()
|
||||
table.insert(matches, {'', row + 1, col + 1})
|
||||
end
|
||||
|
||||
return matches
|
||||
end
|
||||
|
||||
function M.attach(bufnr, lang)
|
||||
api.nvim_call_function('matchup#ts_engine#attach', {bufnr, lang})
|
||||
end
|
||||
|
||||
function M.detach(bufnr)
|
||||
api.nvim_call_function('matchup#ts_engine#detach', {bufnr, lang})
|
||||
end
|
||||
|
||||
return M
|
||||
159
lua/treesitter-matchup/lru.lua
Normal file
159
lua/treesitter-matchup/lru.lua
Normal file
@@ -0,0 +1,159 @@
|
||||
-- lua-lru, LRU cache in Lua
|
||||
-- Copyright (c) 2015 Boris Nagaev
|
||||
-- See the LICENSE file for terms of use.
|
||||
|
||||
local lru = {}
|
||||
|
||||
function lru.new(max_size, max_bytes)
|
||||
|
||||
assert(max_size >= 1, "max_size must be >= 1")
|
||||
assert(not max_bytes or max_bytes >= 1,
|
||||
"max_bytes must be >= 1")
|
||||
|
||||
-- current size
|
||||
local size = 0
|
||||
local bytes_used = 0
|
||||
|
||||
-- map is a hash map from keys to tuples
|
||||
-- tuple: value, prev, next, key
|
||||
-- prev and next are pointers to tuples
|
||||
local map = {}
|
||||
|
||||
-- indices of tuple
|
||||
local VALUE = 1
|
||||
local PREV = 2
|
||||
local NEXT = 3
|
||||
local KEY = 4
|
||||
local BYTES = 5
|
||||
|
||||
-- newest and oldest are ends of double-linked list
|
||||
local newest = nil -- first
|
||||
local oldest = nil -- last
|
||||
|
||||
local removed_tuple -- created in del(), removed in set()
|
||||
|
||||
-- remove a tuple from linked list
|
||||
local function cut(tuple)
|
||||
local tuple_prev = tuple[PREV]
|
||||
local tuple_next = tuple[NEXT]
|
||||
tuple[PREV] = nil
|
||||
tuple[NEXT] = nil
|
||||
if tuple_prev and tuple_next then
|
||||
tuple_prev[NEXT] = tuple_next
|
||||
tuple_next[PREV] = tuple_prev
|
||||
elseif tuple_prev then
|
||||
-- tuple is the oldest element
|
||||
tuple_prev[NEXT] = nil
|
||||
oldest = tuple_prev
|
||||
elseif tuple_next then
|
||||
-- tuple is the newest element
|
||||
tuple_next[PREV] = nil
|
||||
newest = tuple_next
|
||||
else
|
||||
-- tuple is the only element
|
||||
newest = nil
|
||||
oldest = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- insert a tuple to the newest end
|
||||
local function setNewest(tuple)
|
||||
if not newest then
|
||||
newest = tuple
|
||||
oldest = tuple
|
||||
else
|
||||
tuple[NEXT] = newest
|
||||
newest[PREV] = tuple
|
||||
newest = tuple
|
||||
end
|
||||
end
|
||||
|
||||
local function del(key, tuple)
|
||||
map[key] = nil
|
||||
cut(tuple)
|
||||
size = size - 1
|
||||
bytes_used = bytes_used - (tuple[BYTES] or 0)
|
||||
removed_tuple = tuple
|
||||
end
|
||||
|
||||
-- removes elemenets to provide enough memory
|
||||
-- returns last removed element or nil
|
||||
local function makeFreeSpace(bytes)
|
||||
while size + 1 > max_size or
|
||||
(max_bytes and bytes_used + bytes > max_bytes)
|
||||
do
|
||||
assert(oldest, "not enough storage for cache")
|
||||
del(oldest[KEY], oldest)
|
||||
end
|
||||
end
|
||||
|
||||
local function get(_, key)
|
||||
local tuple = map[key]
|
||||
if not tuple then
|
||||
return nil
|
||||
end
|
||||
cut(tuple)
|
||||
setNewest(tuple)
|
||||
return tuple[VALUE]
|
||||
end
|
||||
|
||||
local function set(_, key, value, bytes)
|
||||
local tuple = map[key]
|
||||
if tuple then
|
||||
del(key, tuple)
|
||||
end
|
||||
if value ~= nil then
|
||||
-- the value is not removed
|
||||
bytes = max_bytes and (bytes or #value) or 0
|
||||
makeFreeSpace(bytes)
|
||||
local tuple1 = removed_tuple or {}
|
||||
map[key] = tuple1
|
||||
tuple1[VALUE] = value
|
||||
tuple1[KEY] = key
|
||||
tuple1[BYTES] = max_bytes and bytes
|
||||
size = size + 1
|
||||
bytes_used = bytes_used + bytes
|
||||
setNewest(tuple1)
|
||||
else
|
||||
assert(key ~= nil, "Key may not be nil")
|
||||
end
|
||||
removed_tuple = nil
|
||||
end
|
||||
|
||||
local function delete(_, key)
|
||||
return set(_, key, nil)
|
||||
end
|
||||
|
||||
local function mynext(_, prev_key)
|
||||
local tuple
|
||||
if prev_key then
|
||||
tuple = map[prev_key][NEXT]
|
||||
else
|
||||
tuple = newest
|
||||
end
|
||||
if tuple then
|
||||
return tuple[KEY], tuple[VALUE]
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
-- returns iterator for keys and values
|
||||
local function lru_pairs()
|
||||
return mynext, nil, nil
|
||||
end
|
||||
|
||||
local mt = {
|
||||
__index = {
|
||||
get = get,
|
||||
set = set,
|
||||
delete = delete,
|
||||
pairs = lru_pairs,
|
||||
},
|
||||
__pairs = lru_pairs,
|
||||
}
|
||||
|
||||
return setmetatable({}, mt)
|
||||
end
|
||||
|
||||
return lru
|
||||
28
lua/treesitter-matchup/reload.lua
Normal file
28
lua/treesitter-matchup/reload.lua
Normal file
@@ -0,0 +1,28 @@
|
||||
-- From https://github.com/nvim-lua/plenary.nvim
|
||||
-- MIT License
|
||||
-- Copyright (c) 2020 TJ DeVries
|
||||
|
||||
|
||||
local reload = {}
|
||||
|
||||
reload.reload_module = function(module_name, starts_with_only)
|
||||
-- TODO: Might need to handle cpath / compiled lua packages? Not sure.
|
||||
local matcher
|
||||
if not starts_with_only then
|
||||
matcher = function(pack)
|
||||
return string.find(pack, module_name, 1, true)
|
||||
end
|
||||
else
|
||||
matcher = function(pack)
|
||||
return string.find(pack, '^' .. module_name)
|
||||
end
|
||||
end
|
||||
|
||||
for pack, _ in pairs(package.loaded) do
|
||||
if matcher(pack) then
|
||||
package.loaded[pack] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return reload
|
||||
13
lua/treesitter-matchup/util.lua
Normal file
13
lua/treesitter-matchup/util.lua
Normal file
@@ -0,0 +1,13 @@
|
||||
local random = math.random
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.uuid4()
|
||||
local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
|
||||
return string.gsub(template, '[xy]', function (c)
|
||||
local v = (c == 'x') and random(0, 0xf) or random(8, 0xb)
|
||||
return string.format('%x', v)
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user