diff --git a/lua/treesitter-matchup/internal.lua b/lua/treesitter-matchup/internal.lua index 551418c..c225b5b 100644 --- a/lua/treesitter-matchup/internal.lua +++ b/lua/treesitter-matchup/internal.lua @@ -1,7 +1,6 @@ local vim = vim local api = vim.api local ts = vim.treesitter -local memoize = require'treesitter-matchup.third-party.ts-utils'.memoize local lru = require'treesitter-matchup.third-party.lru' local util = require'treesitter-matchup.util' @@ -49,35 +48,23 @@ function M.is_enabled(bufnr) return is_enabled(lang, bufnr) end ----@param bufnr integer ----@param root TSNode ----@param lang string ----@return string -local function buf_root_lang_hash(bufnr, root, lang) - return tostring(bufnr) .. root:id() .. '_' .. lang -end +---@alias matchup.treesitter.Type 'scope' | 'open' | 'mid' | 'close' | 'skip' ----@class matchup.treesitter.MatchInfo +---@class matchup.treesitter.Match +---@field identifier string +---@field type matchup.treesitter.Type ---@field range Range4 ---@field length integer ---@field last_node TSNode ---@field text string ----@class matchup.treesitter.MatchInfoWrapper ----@field info matchup.treesitter.MatchInfo - ----@class matchup.treesitter.Match ----@field scope? table ----@field open? table ----@field mid? table> ----@field close? table ----@field skip? matchup.treesitter.MatchInfoWrapper - ---@param bufnr integer ---@param root TSNode ---@param lang string +---@param srow integer +---@param erow integer ---@return matchup.treesitter.Match[] -local get_memoized_matches = memoize(function(bufnr, root, lang) +local get_lang_matches = function(bufnr, root, lang, srow, erow) local query = ts.query.get(lang, 'matchup') if not query then @@ -85,14 +72,12 @@ local get_memoized_matches = memoize(function(bufnr, root, lang) end local out = {} ---@type matchup.treesitter.Match[] - for _, match, metadata in query:iter_matches(root, bufnr) do - local match_info = {} + for _, match, metadata in query:iter_matches(root, bufnr, srow, erow) do for id, nodes in pairs(match) do local first = nodes[1] local last = nodes[#nodes] - ---@type integer, integer, integer - local start_row, start_col , start_byte = unpack(ts.get_range(first, bufnr, metadata)) + local start_row, start_col , start_byte = first:range(true) ---@type integer, integer, integer, integer, integer, integer local _, _, _, end_row, end_col , end_byte = unpack(ts.get_range(last, bufnr, metadata)) local range = { start_row, start_col, end_row, end_col } @@ -107,31 +92,23 @@ local get_memoized_matches = memoize(function(bufnr, root, lang) end_row = end_row - 1 end local lines = api.nvim_buf_get_text(bufnr, start_row, start_col, end_row, end_col, {}) - local text = table.concat(lines, '\n') local name = query.captures[id] - local path = {} ---@type string[] - for part in name:gmatch('[^.]+') do - table.insert(path, part) - end + local type, identifier = name:match('^([^.]+)%.([^.]+)%.?') - local current = match_info ---@type table> - for _, segment in ipairs(path) do - current[segment] = current[segment] or {} - current = current[segment] - end - current.info = { + out[#out+1] = { + identifier = identifier, + type = type, range = range, length = length, last_node = last, - text = text, + text = lines[1], } end - table.insert(out, match_info) end return out -end, buf_root_lang_hash) +end ---@param bufnr integer ---@return matchup.treesitter.Match[] @@ -154,7 +131,7 @@ M.get_matches = function(bufnr) local lang = nested_lang_tree:lang() if lang ~= 'comment' then for _, tree in ipairs(nested_lang_tree:trees()) do - local group_results = get_memoized_matches(bufnr, tree:root(), lang) + local group_results = get_lang_matches(bufnr, tree:root(), lang, start_row, end_row) vim.list_extend(matches, group_results) end end @@ -171,30 +148,17 @@ local function _time() return s * 1000 + u * 1e-3 end ---- Returns a (mostly) unique id for this range ----@param range Range4 ----@return string -function M.range_id(range) - return ('range_%d_%d_%d_%d'):format(unpack(range)) -end - --- Get all nodes belonging to defined scopes (organized by key) ----@param bufnr integer +---@param matches matchup.treesitter.Match[] ---@return table> -M.get_scopes = function(bufnr) - local matches = M.get_matches(bufnr) - +M.get_scopes = function(matches) local scopes = {} ---@type table> for _, match in ipairs(matches) do - if match.scope then - for key, scope in pairs(match.scope) do - if scope.info then - local id = M.range_id(scope.info.range) - scopes[key] = scopes[key] or {} - scopes[key][id] = true - end - end + if match.type == 'scope' then + local id = ('range_%d_%d_%d_%d'):format(unpack(match.range)) + scopes[match.identifier] = scopes[match.identifier] or {} + scopes[match.identifier][id] = true end end @@ -202,15 +166,13 @@ M.get_scopes = function(bufnr) end ---@class matchup.treesitter.Matches ----@field open matchup.treesitter.MatchInfo[] ----@field mid matchup.treesitter.MatchInfo[] ----@field close matchup.treesitter.MatchInfo[] - ----@param bufnr integer ----@return [matchup.treesitter.Matches, table] -M.get_active_matches = function(bufnr) - local matches = M.get_matches(bufnr) +---@field open matchup.treesitter.Match[] +---@field mid matchup.treesitter.Match[] +---@field close matchup.treesitter.Match[] +---@param matches matchup.treesitter.Match[] +---@return matchup.treesitter.Matches, table +M.get_active_matches = function(matches) ---@type matchup.treesitter.Matches local info = { open = {}, mid = {}, close = {} } ---@type table @@ -218,58 +180,46 @@ M.get_active_matches = function(bufnr) local enable_quotes = vim.g.matchup_treesitter_enable_quotes for _, match in ipairs(matches) do - if match.open then - for key, open in pairs(match.open) do - local reject = key:find('quote') and not enable_quotes - local id = M.range_id(open.info.range) - if not reject and open.info and symbols[id] == nil then - table.insert(info.open, open.info) - symbols[id] = key - end + if match.type == 'open' then + local reject = not enable_quotes and match.identifier:find('quote') + local id = ('range_%d_%d_%d_%d'):format(unpack(match.range)) + if not reject and symbols[id] == nil then + table.insert(info.open, match) + symbols[id] = match.identifier end - end - if match.close then - for key, close in pairs(match.close) do - local reject = key:find('quote') and not enable_quotes - local id = M.range_id(close.info.range) - if not reject and close.info and symbols[id] == nil then - table.insert(info.close, close.info) - symbols[id] = key - end + elseif match.type == 'close' then + local reject = match.identifier:find('quote') and not enable_quotes + local id = ('range_%d_%d_%d_%d'):format(unpack(match.range)) + if not reject and symbols[id] == nil then + table.insert(info.close, match) + symbols[id] = match.identifier end - end - if match.mid then - for key, mid_group in pairs(match.mid) do - for _, mid in pairs(mid_group) do - local id = M.range_id(mid.info.range) - if mid.info and symbols[id] == nil then - table.insert(info.mid, mid.info) - symbols[id] = key - end - end + elseif match.type == 'mid' then + local id = ('range_%d_%d_%d_%d'):format(unpack(match.range)) + if symbols[id] == nil then + table.insert(info.mid, match) + symbols[id] = match.identifier end end end - return {info, symbols} + return info, symbols end ----@param info matchup.treesitter.MatchInfo? ----@param bufnr integer? +---@param info matchup.treesitter.Match? ---@param key string +---@param matches matchup.treesitter.Match[] ---@return TSNode|nil -function M.containing_scope(info, bufnr, key) - bufnr = bufnr or api.nvim_get_current_buf() - - local scopes = M.get_scopes(bufnr) +function M.containing_scope(info, key, matches) + local scopes = M.get_scopes(matches) if not info or not scopes or not scopes[key] then return end ---@type TSNode|nil local iter_node = info.last_node while iter_node ~= nil do - ---@diagnostic disable-next-line: missing-fields LuaLS bug - if scopes[key][M.range_id({iter_node:range()})] then + local id = ('range_%d_%d_%d_%d'):format(iter_node:range()) + if scopes[key][id] then return iter_node end iter_node = iter_node:parent() @@ -278,25 +228,19 @@ function M.containing_scope(info, bufnr, key) return nil end ----@param info matchup.treesitter.MatchInfo ----@return string -local function text_until_newline(info) - local text = info.text - return text:match("([^\n]+).*") -end - --- Fill in a match result based on a seed node ----@param info matchup.treesitter.MatchInfo +---@param info matchup.treesitter.Match ---@param bufnr integer ---@param opts table ---@param side matchup.Side? ---@param key string? -function M.do_match_result(info, bufnr, opts, side, key) +---@param matches matchup.treesitter.Match[] +function M.do_match_result(info, bufnr, opts, side, key, matches) if not side or not key then return nil end - local scope = M.containing_scope(info, bufnr, key) + local scope = M.containing_scope(info, key, matches) if not scope then return nil end @@ -307,13 +251,13 @@ function M.do_match_result(info, bufnr, opts, side, key) ---@class matchup.Delim local result = { type = 'delim_py', - match = text_until_newline(info), + match = info.text, side = side, lnum = row + 1, cnum = col + 1, skip = 0, class = {key, 0}, - highlighting = opts['highlighting'], + highlighting = opts.highlighting, _id = util.uuid4(), } @@ -332,7 +276,7 @@ function M.do_match_result(info, bufnr, opts, side, key) return result end ----@param info matchup.treesitter.MatchInfo +---@param info matchup.treesitter.Match ---@param line integer ---@param col integer ---@return boolean @@ -373,25 +317,27 @@ local side_table = { ---@param bufnr integer ---@param opts {direction: matchup.Direction, side: matchup.Side, type: matchup.Type} function M.get_delim(bufnr, opts) + local matches = M.get_matches(bufnr) + local active_matches, symbols = M.get_active_matches(matches) if opts.direction == 'current' then -- get current by query - local active_matches, symbols = unpack(M.get_active_matches(bufnr)) local cursor = api.nvim_win_get_cursor(0) local smallest_len = 1e31 - ---@type {info: matchup.treesitter.MatchInfo, side: matchup.Side, key: string}|nil + ---@type {info: matchup.treesitter.Match, side: matchup.Side, key: string}|nil local result_info = nil for _, side in ipairs(side_table[opts.side]) do if not(side == 'mid' and vim.g.matchup_delim_nomids > 0) then - for _, info in ipairs(active_matches[side] --[=[@as matchup.treesitter.MatchInfo[]]=]) do + for _, info in ipairs(active_matches[side] --[=[@as matchup.treesitter.Match[]]=]) do if is_in_range(info, cursor[1] - 1, cursor[2]) then local len = info.length if len < smallest_len then smallest_len = len + local id = ('range_%d_%d_%d_%d'):format(unpack(info.range)) result_info = { info = info, side = side, - key = symbols[M.range_id(info.range)] + key = symbols[id] } end end @@ -401,7 +347,7 @@ function M.get_delim(bufnr, opts) if result_info then return M.do_match_result(result_info.info, bufnr, opts, - result_info.side, result_info.key) + result_info.side, result_info.key, matches) end return @@ -411,15 +357,13 @@ function M.get_delim(bufnr, opts) -- look forwards or backwards for an active node local max_col = 1e5 - local active_matches, symbols = unpack(M.get_active_matches(bufnr)) - local cursor = api.nvim_win_get_cursor(0) local cur_pos = max_col * (cursor[1]-1) + cursor[2] local closest_match, closest_dist = nil, 1e31 local result_info = {} for _, side in ipairs(side_table[opts.side]) do - for _, info in ipairs(active_matches[side]--[=[@as matchup.treesitter.MatchInfo[]]=]) do + for _, info in ipairs(active_matches[side]--[=[@as matchup.treesitter.Match[]]=]) do ---@type integer, integer local row, col = unpack(info.range) local pos = max_col * row + col @@ -429,9 +373,11 @@ function M.get_delim(bufnr, opts) local dist = math.abs(pos - cur_pos) if dist < closest_dist then + closest_dist = dist closest_match = info - result_info = { side=side, key=symbols[M.range_id(info.range)] } + local id = ('range_%d_%d_%d_%d'):format(unpack(info.range)) + result_info = { side=side, key=symbols[id] } end end end @@ -442,7 +388,7 @@ function M.get_delim(bufnr, opts) end return M.do_match_result(closest_match, bufnr, opts, - result_info.side, result_info.key) + result_info.side, result_info.key, matches) end ---@param delim matchup.Delim @@ -457,7 +403,7 @@ function M.get_matching(delim, down, bufnr) return {} end - local matches = {} ---@type [string, integer, integer][] + local out = {} ---@type [string, integer, integer][] local sides ---@type ('open'|'mid'|'close')[] if vim.g.matchup_delim_nomids > 0 then @@ -466,14 +412,16 @@ function M.get_matching(delim, down, bufnr) sides = is_down and {'mid', 'close'} or {'mid', 'open'} end - local active_matches, symbols = unpack(M.get_active_matches(bufnr)) + + local matches = M.get_matches(bufnr) + local active_matches, symbols = M.get_active_matches(matches) local got_close = false local stop_time = _time() + vim.fn['matchup#perf#timeout']() ---@type number for _, side in ipairs(sides) do - for _, info in ipairs(active_matches[side]--[=[@as matchup.treesitter.MatchInfo[]]=]) do + for _, info in ipairs(active_matches[side]--[=[@as matchup.treesitter.Match[]]=]) do ---@type integer, integer local row, col = unpack(info.range) @@ -481,16 +429,17 @@ function M.get_matching(delim, down, bufnr) return {} end - if cached_info.info ~= info and symbols[M.range_id(info.range)] == cached_info.key + local id = ('range_%d_%d_%d_%d'):format(unpack(info.range)) + if cached_info.info ~= info and symbols[id] == cached_info.key and (is_down and (row > cached_info.row or row == cached_info.row and col > cached_info.col) or not is_down and (row < cached_info.row or row == cached_info.row and col < cached_info.col)) and (row >= cached_info.search_range[1] and row <= cached_info.search_range[3]) then - local target_scope = M.containing_scope(info, bufnr, cached_info.key) + local target_scope = M.containing_scope(info, cached_info.key, matches) if cached_info.scope == target_scope then - local text = text_until_newline(info) or '' - table.insert(matches, {text, row + 1, col + 1}) + local text = info.text or '' + table.insert(out, {text, row + 1, col + 1}) if side == 'close' then got_close = true @@ -501,17 +450,17 @@ function M.get_matching(delim, down, bufnr) end -- sort by position - table.sort(matches, function (a, b) + table.sort(out, 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 is_down and not got_close then local row, col, _ = cached_info.scope:end_() - table.insert(matches, {'', row + 1, col + 1}) + table.insert(out, {'', row + 1, col + 1}) end - return matches + return out end return M diff --git a/lua/treesitter-matchup/syntax.lua b/lua/treesitter-matchup/syntax.lua index c42967a..978f5ef 100644 --- a/lua/treesitter-matchup/syntax.lua +++ b/lua/treesitter-matchup/syntax.lua @@ -2,6 +2,7 @@ local api = vim.api local vts = vim.treesitter local hl_info = require'treesitter-matchup.third-party.hl-info' local internal = require'treesitter-matchup.internal' +local unpack = unpack or table.unpack local M = {} @@ -18,11 +19,11 @@ end function M.get_skips(bufnr) local matches = internal.get_matches(bufnr) - local skips = {} ---@type table - + local skips = {} ---@type table for _, match in ipairs(matches) do - if match.skip then - skips[internal.range_id(match.skip.info.range)] = 1 + if match.type == 'skip' then + local id = ('range_%d_%d_%d_%d'):format(unpack(match.range)) + skips[id] = true end end @@ -44,8 +45,8 @@ function M.lang_skip(lnum, col) if not success or not node then return false end - ---@diagnostic disable-next-line: missing-fields LuaLS bug - if skips[internal.range_id({node:range()})] then + local id = ('range_%d_%d_%d_%d'):format(node:range()) + if skips[id] then return true end diff --git a/lua/treesitter-matchup/third-party/ts-utils.lua b/lua/treesitter-matchup/third-party/ts-utils.lua deleted file mode 100644 index 41db334..0000000 --- a/lua/treesitter-matchup/third-party/ts-utils.lua +++ /dev/null @@ -1,28 +0,0 @@ --- From https://github.com/nvim-treesitter/nvim-treesitter --- Copyright 2021 --- licensed under the Apache License 2.0 --- See nvim-treesitter.LICENSE-APACHE-2.0 - -local M = {} - ----Memoize a function using hash_fn to hash the arguments. ----@generic F: function ----@param fn F ----@param hash_fn fun(...): any ----@return F -function M.memoize(fn, hash_fn) - local cache = setmetatable({}, { __mode = 'kv' }) ---@type table - - return function(...) - local key = hash_fn(...) - if cache[key] == nil then - local v = fn(...) ---@type any - cache[key] = v ~= nil and v or vim.NIL - end - - local v = cache[key] - return v ~= vim.NIL and v or nil - end -end - -return M