Add neovim tree-sitter integration

This commit is contained in:
Andy K. Massimino
2021-02-23 01:08:12 -05:00
parent 0ec3160692
commit e3bc1fa821
11 changed files with 612 additions and 0 deletions

View 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_

View File

@@ -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

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View 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