Regenerate nvim config
This commit is contained in:
22
config/neovim/store/nvim-treesitter/lua/nvim-treesitter.lua
Normal file
22
config/neovim/store/nvim-treesitter/lua/nvim-treesitter.lua
Normal file
@ -0,0 +1,22 @@
|
||||
local install = require "nvim-treesitter.install"
|
||||
local utils = require "nvim-treesitter.utils"
|
||||
local info = require "nvim-treesitter.info"
|
||||
local configs = require "nvim-treesitter.configs"
|
||||
local statusline = require "nvim-treesitter.statusline"
|
||||
|
||||
-- Registers all query predicates
|
||||
require "nvim-treesitter.query_predicates"
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.setup()
|
||||
utils.setup_commands("install", install.commands)
|
||||
utils.setup_commands("info", info.commands)
|
||||
utils.setup_commands("configs", configs.commands)
|
||||
configs.init()
|
||||
end
|
||||
|
||||
M.define_modules = configs.define_modules
|
||||
M.statusline = statusline.statusline
|
||||
|
||||
return M
|
||||
@ -0,0 +1,71 @@
|
||||
local api = vim.api
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Creates a cache table for buffers keyed by a type name.
|
||||
-- Cache entries attach to the buffer and cleanup entries
|
||||
-- as buffers are detached.
|
||||
function M.create_buffer_cache()
|
||||
local cache = {}
|
||||
|
||||
---@type table<integer, table<string, any>>
|
||||
local items = setmetatable({}, {
|
||||
__index = function(tbl, key)
|
||||
rawset(tbl, key, {})
|
||||
return rawget(tbl, key)
|
||||
end,
|
||||
})
|
||||
|
||||
---@type table<integer, boolean>
|
||||
local loaded_buffers = {}
|
||||
|
||||
---@param type_name string
|
||||
---@param bufnr integer
|
||||
---@param value any
|
||||
function cache.set(type_name, bufnr, value)
|
||||
if not loaded_buffers[bufnr] then
|
||||
loaded_buffers[bufnr] = true
|
||||
-- Clean up the cache if the buffer is detached
|
||||
-- to avoid memory leaks
|
||||
api.nvim_buf_attach(bufnr, false, {
|
||||
on_detach = function()
|
||||
cache.clear_buffer(bufnr)
|
||||
loaded_buffers[bufnr] = nil
|
||||
return true
|
||||
end,
|
||||
on_reload = function() end, -- this is needed to prevent on_detach being called on buffer reload
|
||||
})
|
||||
end
|
||||
|
||||
items[bufnr][type_name] = value
|
||||
end
|
||||
|
||||
---@param type_name string
|
||||
---@param bufnr integer
|
||||
---@return any
|
||||
function cache.get(type_name, bufnr)
|
||||
return items[bufnr][type_name]
|
||||
end
|
||||
|
||||
---@param type_name string
|
||||
---@param bufnr integer
|
||||
---@return boolean
|
||||
function cache.has(type_name, bufnr)
|
||||
return cache.get(type_name, bufnr) ~= nil
|
||||
end
|
||||
|
||||
---@param type_name string
|
||||
---@param bufnr integer
|
||||
function cache.remove(type_name, bufnr)
|
||||
items[bufnr][type_name] = nil
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
function cache.clear_buffer(bufnr)
|
||||
items[bufnr] = nil
|
||||
end
|
||||
|
||||
return cache
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,39 @@
|
||||
-- Shim module to address deprecations across nvim versions
|
||||
local ts = vim.treesitter
|
||||
local tsq = ts.query
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.get_query_files(lang, query_group, is_included)
|
||||
return (tsq.get_files or tsq.get_query_files)(lang, query_group, is_included)
|
||||
end
|
||||
|
||||
function M.get_query(lang, query_name)
|
||||
return (tsq.get or tsq.get_query)(lang, query_name)
|
||||
end
|
||||
|
||||
function M.parse_query(lang, query)
|
||||
return (tsq.parse or tsq.parse_query)(lang, query)
|
||||
end
|
||||
|
||||
function M.get_range(node, source, metadata)
|
||||
return (ts.get_range or tsq.get_range)(node, source, metadata)
|
||||
end
|
||||
|
||||
function M.get_node_text(node, bufnr)
|
||||
return (ts.get_node_text or tsq.get_node_text)(node, bufnr)
|
||||
end
|
||||
|
||||
function M.require_language(lang, opts)
|
||||
return (ts.language.add or ts.language.require_language)(lang, opts)
|
||||
end
|
||||
|
||||
function M.flatten(t)
|
||||
if vim.fn.has "nvim-0.11" == 1 then
|
||||
return vim.iter(t):flatten():totable()
|
||||
else
|
||||
return vim.tbl_flatten(t)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,616 @@
|
||||
local api = vim.api
|
||||
|
||||
local queries = require "nvim-treesitter.query"
|
||||
local ts = require "nvim-treesitter.compat"
|
||||
local parsers = require "nvim-treesitter.parsers"
|
||||
local utils = require "nvim-treesitter.utils"
|
||||
local caching = require "nvim-treesitter.caching"
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class TSConfig
|
||||
---@field modules {[string]:TSModule}
|
||||
---@field sync_install boolean
|
||||
---@field ensure_installed string[]|string
|
||||
---@field ignore_install string[]
|
||||
---@field auto_install boolean
|
||||
---@field parser_install_dir string|nil
|
||||
|
||||
---@type TSConfig
|
||||
local config = {
|
||||
modules = {},
|
||||
sync_install = false,
|
||||
ensure_installed = {},
|
||||
auto_install = false,
|
||||
ignore_install = {},
|
||||
parser_install_dir = nil,
|
||||
}
|
||||
|
||||
-- List of modules that need to be setup on initialization.
|
||||
---@type TSModule[][]
|
||||
local queued_modules_defs = {}
|
||||
-- Whether we've initialized the plugin yet.
|
||||
local is_initialized = false
|
||||
|
||||
---@class TSModule
|
||||
---@field module_path string
|
||||
---@field enable boolean|string[]|function(string): boolean
|
||||
---@field disable boolean|string[]|function(string): boolean
|
||||
---@field keymaps table<string, string>
|
||||
---@field is_supported function(string): boolean
|
||||
---@field attach function(string)
|
||||
---@field detach function(string)
|
||||
---@field enabled_buffers table<integer, boolean>
|
||||
---@field additional_vim_regex_highlighting boolean|string[]
|
||||
|
||||
---@type {[string]: TSModule}
|
||||
local builtin_modules = {
|
||||
highlight = {
|
||||
module_path = "nvim-treesitter.highlight",
|
||||
-- @deprecated: use `highlight.set_custom_captures` instead
|
||||
custom_captures = {},
|
||||
enable = false,
|
||||
is_supported = function(lang)
|
||||
return queries.has_highlights(lang)
|
||||
end,
|
||||
additional_vim_regex_highlighting = false,
|
||||
},
|
||||
incremental_selection = {
|
||||
module_path = "nvim-treesitter.incremental_selection",
|
||||
enable = false,
|
||||
keymaps = {
|
||||
init_selection = "gnn", -- set to `false` to disable one of the mappings
|
||||
node_incremental = "grn",
|
||||
scope_incremental = "grc",
|
||||
node_decremental = "grm",
|
||||
},
|
||||
is_supported = function()
|
||||
return true
|
||||
end,
|
||||
},
|
||||
indent = {
|
||||
module_path = "nvim-treesitter.indent",
|
||||
enable = false,
|
||||
is_supported = queries.has_indents,
|
||||
},
|
||||
}
|
||||
|
||||
local attached_buffers_by_module = caching.create_buffer_cache()
|
||||
|
||||
---Resolves a module by requiring the `module_path` or using the module definition.
|
||||
---@param mod_name string
|
||||
---@return TSModule|nil
|
||||
local function resolve_module(mod_name)
|
||||
local config_mod = M.get_module(mod_name)
|
||||
|
||||
if not config_mod then
|
||||
return
|
||||
end
|
||||
|
||||
if type(config_mod.attach) == "function" and type(config_mod.detach) == "function" then
|
||||
return config_mod
|
||||
elseif type(config_mod.module_path) == "string" then
|
||||
return require(config_mod.module_path)
|
||||
end
|
||||
end
|
||||
|
||||
---Enables and attaches the module to a buffer for lang.
|
||||
---@param mod string path to module
|
||||
---@param bufnr integer|nil buffer number, defaults to current buffer
|
||||
---@param lang string|nil language, defaults to current language
|
||||
local function enable_module(mod, bufnr, lang)
|
||||
local module = M.get_module(mod)
|
||||
if not module then
|
||||
return
|
||||
end
|
||||
|
||||
bufnr = bufnr or api.nvim_get_current_buf()
|
||||
lang = lang or parsers.get_buf_lang(bufnr)
|
||||
|
||||
if not module.enable then
|
||||
if module.enabled_buffers then
|
||||
module.enabled_buffers[bufnr] = true
|
||||
else
|
||||
module.enabled_buffers = { [bufnr] = true }
|
||||
end
|
||||
end
|
||||
|
||||
M.attach_module(mod, bufnr, lang)
|
||||
end
|
||||
|
||||
---Enables autocomands for the module.
|
||||
---After the module is loaded `loaded` will be set to true for the module.
|
||||
---@param mod string path to module
|
||||
local function enable_mod_conf_autocmd(mod)
|
||||
local config_mod = M.get_module(mod)
|
||||
if not config_mod or config_mod.loaded then
|
||||
return
|
||||
end
|
||||
|
||||
api.nvim_create_autocmd("FileType", {
|
||||
group = api.nvim_create_augroup("NvimTreesitter-" .. mod, {}),
|
||||
callback = function(args)
|
||||
require("nvim-treesitter.configs").reattach_module(mod, args.buf)
|
||||
end,
|
||||
desc = "Reattach module",
|
||||
})
|
||||
|
||||
config_mod.loaded = true
|
||||
end
|
||||
|
||||
---Enables the module globally and for all current buffers.
|
||||
---After enabled, `enable` will be set to true for the module.
|
||||
---@param mod string path to module
|
||||
local function enable_all(mod)
|
||||
local config_mod = M.get_module(mod)
|
||||
if not config_mod then
|
||||
return
|
||||
end
|
||||
|
||||
enable_mod_conf_autocmd(mod)
|
||||
config_mod.enable = true
|
||||
config_mod.enabled_buffers = nil
|
||||
|
||||
for _, bufnr in pairs(api.nvim_list_bufs()) do
|
||||
enable_module(mod, bufnr)
|
||||
end
|
||||
end
|
||||
|
||||
---Disables and detaches the module for a buffer.
|
||||
---@param mod string path to module
|
||||
---@param bufnr integer buffer number, defaults to current buffer
|
||||
local function disable_module(mod, bufnr)
|
||||
local module = M.get_module(mod)
|
||||
if not module then
|
||||
return
|
||||
end
|
||||
|
||||
bufnr = bufnr or api.nvim_get_current_buf()
|
||||
if module.enabled_buffers then
|
||||
module.enabled_buffers[bufnr] = false
|
||||
end
|
||||
M.detach_module(mod, bufnr)
|
||||
end
|
||||
|
||||
---Disables autocomands for the module.
|
||||
---After the module is unloaded `loaded` will be set to false for the module.
|
||||
---@param mod string path to module
|
||||
local function disable_mod_conf_autocmd(mod)
|
||||
local config_mod = M.get_module(mod)
|
||||
if not config_mod or not config_mod.loaded then
|
||||
return
|
||||
end
|
||||
api.nvim_clear_autocmds { event = "FileType", group = "NvimTreesitter-" .. mod }
|
||||
config_mod.loaded = false
|
||||
end
|
||||
|
||||
---Disables the module globally and for all current buffers.
|
||||
---After disabled, `enable` will be set to false for the module.
|
||||
---@param mod string path to module
|
||||
local function disable_all(mod)
|
||||
local config_mod = M.get_module(mod)
|
||||
if not config_mod then
|
||||
return
|
||||
end
|
||||
|
||||
config_mod.enabled_buffers = nil
|
||||
disable_mod_conf_autocmd(mod)
|
||||
config_mod.enable = false
|
||||
|
||||
for _, bufnr in pairs(api.nvim_list_bufs()) do
|
||||
disable_module(mod, bufnr)
|
||||
end
|
||||
end
|
||||
|
||||
---Toggles a module for a buffer
|
||||
---@param mod string path to module
|
||||
---@param bufnr integer buffer number, defaults to current buffer
|
||||
---@param lang string language, defaults to current language
|
||||
local function toggle_module(mod, bufnr, lang)
|
||||
bufnr = bufnr or api.nvim_get_current_buf()
|
||||
lang = lang or parsers.get_buf_lang(bufnr)
|
||||
|
||||
if attached_buffers_by_module.has(mod, bufnr) then
|
||||
disable_module(mod, bufnr)
|
||||
else
|
||||
enable_module(mod, bufnr, lang)
|
||||
end
|
||||
end
|
||||
|
||||
-- Toggles the module globally and for all current buffers.
|
||||
-- @param mod path to module
|
||||
local function toggle_all(mod)
|
||||
local config_mod = M.get_module(mod)
|
||||
if not config_mod then
|
||||
return
|
||||
end
|
||||
|
||||
if config_mod.enable then
|
||||
disable_all(mod)
|
||||
else
|
||||
enable_all(mod)
|
||||
end
|
||||
end
|
||||
|
||||
---Recurses through all modules including submodules
|
||||
---@param accumulator function called for each module
|
||||
---@param root {[string]: TSModule}|nil root configuration table to start at
|
||||
---@param path string|nil prefix path
|
||||
local function recurse_modules(accumulator, root, path)
|
||||
root = root or config.modules
|
||||
|
||||
for name, module in pairs(root) do
|
||||
local new_path = path and (path .. "." .. name) or name
|
||||
|
||||
if M.is_module(module) then
|
||||
accumulator(name, module, new_path, root)
|
||||
elseif type(module) == "table" then
|
||||
recurse_modules(accumulator, module, new_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Shows current configuration of all nvim-treesitter modules
|
||||
---@param process_function function used as the `process` parameter
|
||||
--- for vim.inspect (https://github.com/kikito/inspect.lua#optionsprocess)
|
||||
local function config_info(process_function)
|
||||
process_function = process_function
|
||||
or function(item, path)
|
||||
if path[#path] == vim.inspect.METATABLE then
|
||||
return
|
||||
end
|
||||
if path[#path] == "is_supported" then
|
||||
return
|
||||
end
|
||||
return item
|
||||
end
|
||||
print(vim.inspect(config, { process = process_function }))
|
||||
end
|
||||
|
||||
---@param query_group string
|
||||
---@param lang string
|
||||
function M.edit_query_file(query_group, lang)
|
||||
lang = lang or parsers.get_buf_lang()
|
||||
local files = ts.get_query_files(lang, query_group, true)
|
||||
if #files == 0 then
|
||||
utils.notify "No query file found! Creating a new one!"
|
||||
M.edit_query_file_user_after(query_group, lang)
|
||||
elseif #files == 1 then
|
||||
vim.cmd(":edit " .. files[1])
|
||||
else
|
||||
vim.ui.select(files, { prompt = "Select a file:" }, function(file)
|
||||
if file then
|
||||
vim.cmd(":edit " .. file)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---@param query_group string
|
||||
---@param lang string
|
||||
function M.edit_query_file_user_after(query_group, lang)
|
||||
lang = lang or parsers.get_buf_lang()
|
||||
local folder = utils.join_path(vim.fn.stdpath "config", "after", "queries", lang)
|
||||
local file = utils.join_path(folder, query_group .. ".scm")
|
||||
if vim.fn.isdirectory(folder) ~= 1 then
|
||||
vim.ui.select({ "Yes", "No" }, { prompt = '"' .. folder .. '" does not exist. Create it?' }, function(choice)
|
||||
if choice == "Yes" then
|
||||
vim.fn.mkdir(folder, "p", "0755")
|
||||
vim.cmd(":edit " .. file)
|
||||
end
|
||||
end)
|
||||
else
|
||||
vim.cmd(":edit " .. file)
|
||||
end
|
||||
end
|
||||
|
||||
M.commands = {
|
||||
TSBufEnable = {
|
||||
run = enable_module,
|
||||
args = {
|
||||
"-nargs=1",
|
||||
"-complete=custom,nvim_treesitter#available_modules",
|
||||
},
|
||||
},
|
||||
TSBufDisable = {
|
||||
run = disable_module,
|
||||
args = {
|
||||
"-nargs=1",
|
||||
"-complete=custom,nvim_treesitter#available_modules",
|
||||
},
|
||||
},
|
||||
TSBufToggle = {
|
||||
run = toggle_module,
|
||||
args = {
|
||||
"-nargs=1",
|
||||
"-complete=custom,nvim_treesitter#available_modules",
|
||||
},
|
||||
},
|
||||
TSEnable = {
|
||||
run = enable_all,
|
||||
args = {
|
||||
"-nargs=+",
|
||||
"-complete=custom,nvim_treesitter#available_modules",
|
||||
},
|
||||
},
|
||||
TSDisable = {
|
||||
run = disable_all,
|
||||
args = {
|
||||
"-nargs=+",
|
||||
"-complete=custom,nvim_treesitter#available_modules",
|
||||
},
|
||||
},
|
||||
TSToggle = {
|
||||
run = toggle_all,
|
||||
args = {
|
||||
"-nargs=+",
|
||||
"-complete=custom,nvim_treesitter#available_modules",
|
||||
},
|
||||
},
|
||||
TSConfigInfo = {
|
||||
run = config_info,
|
||||
args = {
|
||||
"-nargs=0",
|
||||
},
|
||||
},
|
||||
TSEditQuery = {
|
||||
run = M.edit_query_file,
|
||||
args = {
|
||||
"-nargs=+",
|
||||
"-complete=custom,nvim_treesitter#available_query_groups",
|
||||
},
|
||||
},
|
||||
TSEditQueryUserAfter = {
|
||||
run = M.edit_query_file_user_after,
|
||||
args = {
|
||||
"-nargs=+",
|
||||
"-complete=custom,nvim_treesitter#available_query_groups",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
---@param mod string module
|
||||
---@param lang string the language of the buffer
|
||||
---@param bufnr integer the buffer
|
||||
function M.is_enabled(mod, lang, bufnr)
|
||||
if not parsers.has_parser(lang) then
|
||||
return false
|
||||
end
|
||||
|
||||
local module_config = M.get_module(mod)
|
||||
if not module_config then
|
||||
return false
|
||||
end
|
||||
|
||||
local buffer_enabled = module_config.enabled_buffers and module_config.enabled_buffers[bufnr]
|
||||
local config_enabled = module_config.enable or buffer_enabled
|
||||
if not config_enabled or not module_config.is_supported(lang) then
|
||||
return false
|
||||
end
|
||||
|
||||
local disable = module_config.disable
|
||||
if type(disable) == "function" then
|
||||
if disable(lang, bufnr) then
|
||||
return false
|
||||
end
|
||||
elseif type(disable) == "table" then
|
||||
-- Otherwise it's a list of languages
|
||||
for _, parser in pairs(disable) do
|
||||
if lang == parser then
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
---Setup call for users to override module configurations.
|
||||
---@param user_data TSConfig module overrides
|
||||
function M.setup(user_data)
|
||||
config.modules = vim.tbl_deep_extend("force", config.modules, user_data)
|
||||
config.ignore_install = user_data.ignore_install or {}
|
||||
config.parser_install_dir = user_data.parser_install_dir or nil
|
||||
if config.parser_install_dir then
|
||||
config.parser_install_dir = vim.fn.expand(config.parser_install_dir, ":p")
|
||||
end
|
||||
|
||||
config.auto_install = user_data.auto_install or false
|
||||
if config.auto_install then
|
||||
require("nvim-treesitter.install").setup_auto_install()
|
||||
end
|
||||
|
||||
local ensure_installed = user_data.ensure_installed or {}
|
||||
if #ensure_installed > 0 then
|
||||
if user_data.sync_install then
|
||||
require("nvim-treesitter.install").ensure_installed_sync(ensure_installed)
|
||||
else
|
||||
require("nvim-treesitter.install").ensure_installed(ensure_installed)
|
||||
end
|
||||
end
|
||||
|
||||
config.modules.ensure_installed = nil
|
||||
config.ensure_installed = ensure_installed
|
||||
|
||||
recurse_modules(function(_, _, new_path)
|
||||
local data = utils.get_at_path(config.modules, new_path)
|
||||
if data.enable then
|
||||
enable_all(new_path)
|
||||
end
|
||||
end, config.modules)
|
||||
end
|
||||
|
||||
-- Defines a table of modules that can be attached/detached to buffers
|
||||
-- based on language support. A module consist of the following properties:
|
||||
---* @enable Whether the modules is enabled. Can be true or false.
|
||||
---* @disable A list of languages to disable the module for. Only relevant if enable is true.
|
||||
---* @keymaps A list of user mappings for a given module if relevant.
|
||||
---* @is_supported A function which, given a ft, will return true if the ft works on the module.
|
||||
---* @module_path A string path to a module file using `require`. The exported module must contain
|
||||
--- an `attach` and `detach` function. This path is not required if `attach` and `detach`
|
||||
--- functions are provided directly on the module definition.
|
||||
---* @attach An attach function that is called for each buffer that the module is enabled for. This is required
|
||||
--- if a `module_path` is not specified.
|
||||
---* @detach A detach function that is called for each buffer that the module is enabled for. This is required
|
||||
--- if a `module_path` is not specified.
|
||||
--
|
||||
-- Modules are not setup until `init` is invoked by the plugin. This allows modules to be defined in any order
|
||||
-- and can be loaded lazily.
|
||||
--
|
||||
---* @example
|
||||
---require"nvim-treesitter".define_modules {
|
||||
--- my_cool_module = {
|
||||
--- attach = function()
|
||||
--- do_some_cool_setup()
|
||||
--- end,
|
||||
--- detach = function()
|
||||
--- do_some_cool_teardown()
|
||||
--- end
|
||||
--- }
|
||||
---}
|
||||
---@param mod_defs TSModule[]
|
||||
function M.define_modules(mod_defs)
|
||||
if not is_initialized then
|
||||
table.insert(queued_modules_defs, mod_defs)
|
||||
return
|
||||
end
|
||||
|
||||
recurse_modules(function(key, mod, _, group)
|
||||
group[key] = vim.tbl_extend("keep", mod, {
|
||||
enable = false,
|
||||
disable = {},
|
||||
is_supported = function()
|
||||
return true
|
||||
end,
|
||||
})
|
||||
end, mod_defs)
|
||||
|
||||
config.modules = vim.tbl_deep_extend("keep", config.modules, mod_defs)
|
||||
|
||||
for _, mod in ipairs(M.available_modules(mod_defs)) do
|
||||
local module_config = M.get_module(mod)
|
||||
if module_config and module_config.enable then
|
||||
enable_mod_conf_autocmd(mod)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Attaches a module to a buffer
|
||||
---@param mod_name string the module name
|
||||
---@param bufnr integer the buffer
|
||||
---@param lang string the language of the buffer
|
||||
function M.attach_module(mod_name, bufnr, lang)
|
||||
bufnr = bufnr or api.nvim_get_current_buf()
|
||||
lang = lang or parsers.get_buf_lang(bufnr)
|
||||
local resolved_mod = resolve_module(mod_name)
|
||||
|
||||
if resolved_mod and not attached_buffers_by_module.has(mod_name, bufnr) and M.is_enabled(mod_name, lang, bufnr) then
|
||||
attached_buffers_by_module.set(mod_name, bufnr, true)
|
||||
resolved_mod.attach(bufnr, lang)
|
||||
end
|
||||
end
|
||||
|
||||
-- Detaches a module to a buffer
|
||||
---@param mod_name string the module name
|
||||
---@param bufnr integer the buffer
|
||||
function M.detach_module(mod_name, bufnr)
|
||||
local resolved_mod = resolve_module(mod_name)
|
||||
bufnr = bufnr or api.nvim_get_current_buf()
|
||||
|
||||
if resolved_mod and attached_buffers_by_module.has(mod_name, bufnr) then
|
||||
attached_buffers_by_module.remove(mod_name, bufnr)
|
||||
resolved_mod.detach(bufnr)
|
||||
end
|
||||
end
|
||||
|
||||
-- Same as attach_module, but if the module is already attached, detach it first.
|
||||
---@param mod_name string the module name
|
||||
---@param bufnr integer the buffer
|
||||
---@param lang string the language of the buffer
|
||||
function M.reattach_module(mod_name, bufnr, lang)
|
||||
M.detach_module(mod_name, bufnr)
|
||||
M.attach_module(mod_name, bufnr, lang)
|
||||
end
|
||||
|
||||
-- Gets available modules
|
||||
---@param root {[string]:TSModule}|nil table to find modules
|
||||
---@return string[] modules list of module paths
|
||||
function M.available_modules(root)
|
||||
local modules = {}
|
||||
|
||||
recurse_modules(function(_, _, path)
|
||||
table.insert(modules, path)
|
||||
end, root)
|
||||
|
||||
return modules
|
||||
end
|
||||
|
||||
---Gets a module config by path
|
||||
---@param mod_path string path to the module
|
||||
---@return TSModule|nil: the module or nil
|
||||
function M.get_module(mod_path)
|
||||
local mod = utils.get_at_path(config.modules, mod_path)
|
||||
|
||||
return M.is_module(mod) and mod or nil
|
||||
end
|
||||
|
||||
-- Determines whether the provided table is a module.
|
||||
-- A module should contain an attach and detach function.
|
||||
---@param mod table|nil the module table
|
||||
---@return boolean
|
||||
function M.is_module(mod)
|
||||
return type(mod) == "table"
|
||||
and ((type(mod.attach) == "function" and type(mod.detach) == "function") or type(mod.module_path) == "string")
|
||||
end
|
||||
|
||||
-- Initializes built-in modules and any queued modules
|
||||
-- registered by plugins or the user.
|
||||
function M.init()
|
||||
is_initialized = true
|
||||
M.define_modules(builtin_modules)
|
||||
|
||||
for _, mod_def in ipairs(queued_modules_defs) do
|
||||
M.define_modules(mod_def)
|
||||
end
|
||||
end
|
||||
|
||||
-- If parser_install_dir is not nil is used or created.
|
||||
-- If parser_install_dir is nil try the package dir of the nvim-treesitter
|
||||
-- plugin first, followed by the "site" dir from "runtimepath". "site" dir will
|
||||
-- be created if it doesn't exist. Using only the package dir won't work when
|
||||
-- the plugin is installed with Nix, since the "/nix/store" is read-only.
|
||||
---@param folder_name string|nil
|
||||
---@return string|nil, string|nil
|
||||
function M.get_parser_install_dir(folder_name)
|
||||
folder_name = folder_name or "parser"
|
||||
|
||||
local install_dir = config.parser_install_dir or utils.get_package_path()
|
||||
local parser_dir = utils.join_path(install_dir, folder_name)
|
||||
|
||||
return utils.create_or_reuse_writable_dir(
|
||||
parser_dir,
|
||||
utils.join_space("Could not create parser dir '", parser_dir, "': "),
|
||||
utils.join_space(
|
||||
"Parser dir '",
|
||||
parser_dir,
|
||||
"' should be read/write (see README on how to configure an alternative install location)"
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
function M.get_parser_info_dir()
|
||||
return M.get_parser_install_dir "parser-info"
|
||||
end
|
||||
|
||||
function M.get_ignored_parser_installs()
|
||||
return config.ignore_install or {}
|
||||
end
|
||||
|
||||
function M.get_ensure_installed_parsers()
|
||||
if type(config.ensure_installed) == "string" then
|
||||
return { config.ensure_installed }
|
||||
end
|
||||
return config.ensure_installed or {}
|
||||
end
|
||||
|
||||
return M
|
||||
123
config/neovim/store/nvim-treesitter/lua/nvim-treesitter/fold.lua
Normal file
123
config/neovim/store/nvim-treesitter/lua/nvim-treesitter/fold.lua
Normal file
@ -0,0 +1,123 @@
|
||||
local api = vim.api
|
||||
local tsutils = require "nvim-treesitter.ts_utils"
|
||||
local query = require "nvim-treesitter.query"
|
||||
local parsers = require "nvim-treesitter.parsers"
|
||||
|
||||
local M = {}
|
||||
|
||||
-- This is cached on buf tick to avoid computing that multiple times
|
||||
-- Especially not for every line in the file when `zx` is hit
|
||||
local folds_levels = tsutils.memoize_by_buf_tick(function(bufnr)
|
||||
local max_fold_level = api.nvim_win_get_option(0, "foldnestmax")
|
||||
local trim_level = function(level)
|
||||
if level > max_fold_level then
|
||||
return max_fold_level
|
||||
end
|
||||
return level
|
||||
end
|
||||
|
||||
local parser = parsers.get_parser(bufnr)
|
||||
|
||||
if not parser then
|
||||
return {}
|
||||
end
|
||||
|
||||
local matches = query.get_capture_matches_recursively(bufnr, function(lang)
|
||||
if query.has_folds(lang) then
|
||||
return "@fold", "folds"
|
||||
elseif query.has_locals(lang) then
|
||||
return "@scope", "locals"
|
||||
end
|
||||
end)
|
||||
|
||||
-- start..stop is an inclusive range
|
||||
|
||||
---@type table<number, number>
|
||||
local start_counts = {}
|
||||
---@type table<number, number>
|
||||
local stop_counts = {}
|
||||
|
||||
local prev_start = -1
|
||||
local prev_stop = -1
|
||||
|
||||
local min_fold_lines = api.nvim_win_get_option(0, "foldminlines")
|
||||
|
||||
for _, match in ipairs(matches) do
|
||||
local start, stop, stop_col ---@type integer, integer, integer
|
||||
if match.metadata and match.metadata.range then
|
||||
start, _, stop, stop_col = unpack(match.metadata.range) ---@type integer, integer, integer, integer
|
||||
else
|
||||
start, _, stop, stop_col = match.node:range() ---@type integer, integer, integer, integer
|
||||
end
|
||||
|
||||
if stop_col == 0 then
|
||||
stop = stop - 1
|
||||
end
|
||||
|
||||
local fold_length = stop - start + 1
|
||||
local should_fold = fold_length > min_fold_lines
|
||||
|
||||
-- Fold only multiline nodes that are not exactly the same as previously met folds
|
||||
-- Checking against just the previously found fold is sufficient if nodes
|
||||
-- are returned in preorder or postorder when traversing tree
|
||||
if should_fold and not (start == prev_start and stop == prev_stop) then
|
||||
start_counts[start] = (start_counts[start] or 0) + 1
|
||||
stop_counts[stop] = (stop_counts[stop] or 0) + 1
|
||||
prev_start = start
|
||||
prev_stop = stop
|
||||
end
|
||||
end
|
||||
|
||||
---@type string[]
|
||||
local levels = {}
|
||||
local current_level = 0
|
||||
|
||||
-- We now have the list of fold opening and closing, fill the gaps and mark where fold start
|
||||
for lnum = 0, api.nvim_buf_line_count(bufnr) do
|
||||
local prefix = ""
|
||||
|
||||
local last_trimmed_level = trim_level(current_level)
|
||||
current_level = current_level + (start_counts[lnum] or 0)
|
||||
local trimmed_level = trim_level(current_level)
|
||||
current_level = current_level - (stop_counts[lnum] or 0)
|
||||
local next_trimmed_level = trim_level(current_level)
|
||||
|
||||
-- Determine if it's the start/end of a fold
|
||||
-- NB: vim's fold-expr interface does not have a mechanism to indicate that
|
||||
-- two (or more) folds start at this line, so it cannot distinguish between
|
||||
-- ( \n ( \n )) \n (( \n ) \n )
|
||||
-- versus
|
||||
-- ( \n ( \n ) \n ( \n ) \n )
|
||||
-- If it did have such a mechanism, (trimmed_level - last_trimmed_level)
|
||||
-- would be the correct number of starts to pass on.
|
||||
if trimmed_level - last_trimmed_level > 0 then
|
||||
prefix = ">"
|
||||
elseif trimmed_level - next_trimmed_level > 0 then
|
||||
-- Ending marks tend to confuse vim more than it helps, particularly when
|
||||
-- the fold level changes by at least 2; we can uncomment this if
|
||||
-- vim's behavior gets fixed.
|
||||
-- prefix = "<"
|
||||
prefix = ""
|
||||
end
|
||||
|
||||
levels[lnum + 1] = prefix .. tostring(trimmed_level)
|
||||
end
|
||||
|
||||
return levels
|
||||
end)
|
||||
|
||||
---@param lnum integer
|
||||
---@return string
|
||||
function M.get_fold_indic(lnum)
|
||||
if not parsers.has_parser() or not lnum then
|
||||
return "0"
|
||||
end
|
||||
|
||||
local buf = api.nvim_get_current_buf()
|
||||
|
||||
local levels = folds_levels(buf) or {}
|
||||
|
||||
return levels[lnum] or "0"
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,176 @@
|
||||
local api = vim.api
|
||||
local fn = vim.fn
|
||||
|
||||
local queries = require "nvim-treesitter.query"
|
||||
local info = require "nvim-treesitter.info"
|
||||
local shell = require "nvim-treesitter.shell_command_selectors"
|
||||
local install = require "nvim-treesitter.install"
|
||||
local utils = require "nvim-treesitter.utils"
|
||||
local ts = require "nvim-treesitter.compat"
|
||||
|
||||
local health = vim.health or require "health"
|
||||
|
||||
-- "report_" prefix has been deprecated, use the recommended replacements if they exist.
|
||||
local _start = health.start or health.report_start
|
||||
local _ok = health.ok or health.report_ok
|
||||
local _warn = health.warn or health.report_warn
|
||||
local _error = health.error or health.report_error
|
||||
|
||||
local M = {}
|
||||
|
||||
local NVIM_TREESITTER_MINIMUM_ABI = 13
|
||||
|
||||
local function install_health()
|
||||
_start "Installation"
|
||||
|
||||
if fn.has "nvim-0.9.2" ~= 1 then
|
||||
_error "Nvim-treesitter requires Nvim 0.9.2 or newer"
|
||||
end
|
||||
|
||||
if fn.executable "tree-sitter" == 0 then
|
||||
_warn(
|
||||
"`tree-sitter` executable not found (parser generator, only needed for :TSInstallFromGrammar,"
|
||||
.. " not required for :TSInstall)"
|
||||
)
|
||||
else
|
||||
_ok(
|
||||
"`tree-sitter` found "
|
||||
.. (utils.ts_cli_version() or "(unknown version)")
|
||||
.. " (parser generator, only needed for :TSInstallFromGrammar)"
|
||||
)
|
||||
end
|
||||
|
||||
if fn.executable "node" == 0 then
|
||||
_warn("`node` executable not found (only needed for :TSInstallFromGrammar," .. " not required for :TSInstall)")
|
||||
else
|
||||
local handle = io.popen "node --version"
|
||||
local result = handle:read "*a"
|
||||
handle:close()
|
||||
local version = vim.split(result, "\n")[1]
|
||||
_ok("`node` found " .. version .. " (only needed for :TSInstallFromGrammar)")
|
||||
end
|
||||
|
||||
if fn.executable "git" == 0 then
|
||||
_error("`git` executable not found.", {
|
||||
"Install it with your package manager.",
|
||||
"Check that your `$PATH` is set correctly.",
|
||||
})
|
||||
else
|
||||
_ok "`git` executable found."
|
||||
end
|
||||
|
||||
local cc = shell.select_executable(install.compilers)
|
||||
if not cc then
|
||||
_error("`cc` executable not found.", {
|
||||
"Check that any of "
|
||||
.. vim.inspect(install.compilers)
|
||||
.. " is in your $PATH"
|
||||
.. ' or set the environment variable CC or `require"nvim-treesitter.install".compilers` explicitly!',
|
||||
})
|
||||
else
|
||||
local version = vim.fn.systemlist(cc .. (cc == "cl" and "" or " --version"))[1]
|
||||
_ok(
|
||||
"`"
|
||||
.. cc
|
||||
.. "` executable found. Selected from "
|
||||
.. vim.inspect(install.compilers)
|
||||
.. (version and ("\nVersion: " .. version) or "")
|
||||
)
|
||||
end
|
||||
if vim.treesitter.language_version then
|
||||
if vim.treesitter.language_version >= NVIM_TREESITTER_MINIMUM_ABI then
|
||||
_ok(
|
||||
"Neovim was compiled with tree-sitter runtime ABI version "
|
||||
.. vim.treesitter.language_version
|
||||
.. " (required >="
|
||||
.. NVIM_TREESITTER_MINIMUM_ABI
|
||||
.. "). Parsers must be compatible with runtime ABI."
|
||||
)
|
||||
else
|
||||
_error(
|
||||
"Neovim was compiled with tree-sitter runtime ABI version "
|
||||
.. vim.treesitter.language_version
|
||||
.. ".\n"
|
||||
.. "nvim-treesitter expects at least ABI version "
|
||||
.. NVIM_TREESITTER_MINIMUM_ABI
|
||||
.. "\n"
|
||||
.. "Please make sure that Neovim is linked against are recent tree-sitter runtime when building"
|
||||
.. " or raise an issue at your Neovim packager. Parsers must be compatible with runtime ABI."
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
_start("OS Info:\n" .. vim.inspect(vim.loop.os_uname()))
|
||||
end
|
||||
|
||||
local function query_status(lang, query_group)
|
||||
local ok, err = pcall(queries.get_query, lang, query_group)
|
||||
if not ok then
|
||||
return "x", err
|
||||
elseif not err then
|
||||
return "."
|
||||
else
|
||||
return "✓"
|
||||
end
|
||||
end
|
||||
|
||||
function M.check()
|
||||
local error_collection = {}
|
||||
-- Installation dependency checks
|
||||
install_health()
|
||||
queries.invalidate_query_cache()
|
||||
-- Parser installation checks
|
||||
local parser_installation = { "Parser/Features" .. string.rep(" ", 9) .. "H L F I J" }
|
||||
for _, parser_name in pairs(info.installed_parsers()) do
|
||||
local installed = #api.nvim_get_runtime_file("parser/" .. parser_name .. ".so", false)
|
||||
|
||||
-- Only append information about installed parsers
|
||||
if installed >= 1 then
|
||||
local multiple_parsers = installed > 1 and "+" or ""
|
||||
local out = " - " .. parser_name .. multiple_parsers .. string.rep(" ", 20 - (#parser_name + #multiple_parsers))
|
||||
for _, query_group in pairs(queries.built_in_query_groups) do
|
||||
local status, err = query_status(parser_name, query_group)
|
||||
out = out .. status .. " "
|
||||
if err then
|
||||
table.insert(error_collection, { parser_name, query_group, err })
|
||||
end
|
||||
end
|
||||
table.insert(parser_installation, vim.fn.trim(out, " ", 2))
|
||||
end
|
||||
end
|
||||
local legend = [[
|
||||
|
||||
Legend: H[ighlight], L[ocals], F[olds], I[ndents], In[j]ections
|
||||
+) multiple parsers found, only one will be used
|
||||
x) errors found in the query, try to run :TSUpdate {lang}]]
|
||||
table.insert(parser_installation, legend)
|
||||
-- Finally call the report function
|
||||
_start(table.concat(parser_installation, "\n"))
|
||||
if #error_collection > 0 then
|
||||
_start "The following errors have been detected:"
|
||||
for _, p in ipairs(error_collection) do
|
||||
local lang, type, err = unpack(p)
|
||||
local lines = {}
|
||||
table.insert(lines, lang .. "(" .. type .. "): " .. err)
|
||||
local files = ts.get_query_files(lang, type)
|
||||
if #files > 0 then
|
||||
table.insert(lines, lang .. "(" .. type .. ") is concatenated from the following files:")
|
||||
for _, file in ipairs(files) do
|
||||
local fd = io.open(file, "r")
|
||||
if fd then
|
||||
local ok, file_err = pcall(ts.parse_query, lang, fd:read "*a")
|
||||
if ok then
|
||||
table.insert(lines, '| [OK]:"' .. file .. '"')
|
||||
else
|
||||
table.insert(lines, '| [ERROR]:"' .. file .. '", failed to load: ' .. file_err)
|
||||
end
|
||||
fd:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
_error(table.concat(lines, "\n"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,49 @@
|
||||
local configs = require "nvim-treesitter.configs"
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param config TSModule
|
||||
---@param lang string
|
||||
---@return boolean
|
||||
local function should_enable_vim_regex(config, lang)
|
||||
local additional_hl = config.additional_vim_regex_highlighting
|
||||
local is_table = type(additional_hl) == "table"
|
||||
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
return additional_hl and (not is_table or vim.tbl_contains(additional_hl, lang))
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param lang string
|
||||
function M.attach(bufnr, lang)
|
||||
local config = configs.get_module "highlight"
|
||||
vim.treesitter.start(bufnr, lang)
|
||||
if config and should_enable_vim_regex(config, lang) then
|
||||
vim.bo[bufnr].syntax = "ON"
|
||||
end
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
function M.detach(bufnr)
|
||||
vim.treesitter.stop(bufnr)
|
||||
end
|
||||
|
||||
---@deprecated
|
||||
function M.start(...)
|
||||
vim.notify(
|
||||
"`nvim-treesitter.highlight.start` is deprecated: use `nvim-treesitter.highlight.attach` or `vim.treesitter.start`",
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
M.attach(...)
|
||||
end
|
||||
|
||||
---@deprecated
|
||||
function M.stop(...)
|
||||
vim.notify(
|
||||
"`nvim-treesitter.highlight.stop` is deprecated: use `nvim-treesitter.highlight.detach` or `vim.treesitter.stop`",
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
M.detach(...)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,167 @@
|
||||
local api = vim.api
|
||||
|
||||
local configs = require "nvim-treesitter.configs"
|
||||
local ts_utils = require "nvim-treesitter.ts_utils"
|
||||
local locals = require "nvim-treesitter.locals"
|
||||
local parsers = require "nvim-treesitter.parsers"
|
||||
local queries = require "nvim-treesitter.query"
|
||||
local utils = require "nvim-treesitter.utils"
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type table<integer, table<TSNode|nil>>
|
||||
local selections = {}
|
||||
|
||||
function M.init_selection()
|
||||
local buf = api.nvim_get_current_buf()
|
||||
local node = ts_utils.get_node_at_cursor()
|
||||
selections[buf] = { [1] = node }
|
||||
ts_utils.update_selection(buf, node)
|
||||
end
|
||||
|
||||
-- Get the range of the current visual selection.
|
||||
--
|
||||
-- The range starts with 1 and the ending is inclusive.
|
||||
---@return integer, integer, integer, integer
|
||||
local function visual_selection_range()
|
||||
local _, csrow, cscol, _ = unpack(vim.fn.getpos "v") ---@type integer, integer, integer, integer
|
||||
local _, cerow, cecol, _ = unpack(vim.fn.getpos ".") ---@type integer, integer, integer, integer
|
||||
|
||||
local start_row, start_col, end_row, end_col ---@type integer, integer, integer, integer
|
||||
|
||||
if csrow < cerow or (csrow == cerow and cscol <= cecol) then
|
||||
start_row = csrow
|
||||
start_col = cscol
|
||||
end_row = cerow
|
||||
end_col = cecol
|
||||
else
|
||||
start_row = cerow
|
||||
start_col = cecol
|
||||
end_row = csrow
|
||||
end_col = cscol
|
||||
end
|
||||
|
||||
return start_row, start_col, end_row, end_col
|
||||
end
|
||||
|
||||
---@param node TSNode
|
||||
---@return boolean
|
||||
local function range_matches(node)
|
||||
local csrow, cscol, cerow, cecol = visual_selection_range()
|
||||
local srow, scol, erow, ecol = ts_utils.get_vim_range { node:range() }
|
||||
return srow == csrow and scol == cscol and erow == cerow and ecol == cecol
|
||||
end
|
||||
|
||||
---@param get_parent fun(node: TSNode): TSNode|nil
|
||||
---@return fun():nil
|
||||
local function select_incremental(get_parent)
|
||||
return function()
|
||||
local buf = api.nvim_get_current_buf()
|
||||
local nodes = selections[buf]
|
||||
|
||||
local csrow, cscol, cerow, cecol = visual_selection_range()
|
||||
-- Initialize incremental selection with current selection
|
||||
if not nodes or #nodes == 0 or not range_matches(nodes[#nodes]) then
|
||||
local root = parsers.get_parser():parse()[1]:root()
|
||||
local node = root:named_descendant_for_range(csrow - 1, cscol - 1, cerow - 1, cecol)
|
||||
ts_utils.update_selection(buf, node)
|
||||
if nodes and #nodes > 0 then
|
||||
table.insert(selections[buf], node)
|
||||
else
|
||||
selections[buf] = { [1] = node }
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Find a node that changes the current selection.
|
||||
local node = nodes[#nodes] ---@type TSNode
|
||||
while true do
|
||||
local parent = get_parent(node)
|
||||
if not parent or parent == node then
|
||||
-- Keep searching in the main tree
|
||||
-- TODO: we should search on the parent tree of the current node.
|
||||
local root = parsers.get_parser():parse()[1]:root()
|
||||
parent = root:named_descendant_for_range(csrow - 1, cscol - 1, cerow - 1, cecol)
|
||||
if not parent or root == node or parent == node then
|
||||
ts_utils.update_selection(buf, node)
|
||||
return
|
||||
end
|
||||
end
|
||||
node = parent
|
||||
local srow, scol, erow, ecol = ts_utils.get_vim_range { node:range() }
|
||||
local same_range = (srow == csrow and scol == cscol and erow == cerow and ecol == cecol)
|
||||
if not same_range then
|
||||
table.insert(selections[buf], node)
|
||||
if node ~= nodes[#nodes] then
|
||||
table.insert(nodes, node)
|
||||
end
|
||||
ts_utils.update_selection(buf, node)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
M.node_incremental = select_incremental(function(node)
|
||||
return node:parent() or node
|
||||
end)
|
||||
|
||||
M.scope_incremental = select_incremental(function(node)
|
||||
local lang = parsers.get_buf_lang()
|
||||
if queries.has_locals(lang) then
|
||||
return locals.containing_scope(node:parent() or node)
|
||||
else
|
||||
return node
|
||||
end
|
||||
end)
|
||||
|
||||
function M.node_decremental()
|
||||
local buf = api.nvim_get_current_buf()
|
||||
local nodes = selections[buf]
|
||||
if not nodes or #nodes < 2 then
|
||||
return
|
||||
end
|
||||
|
||||
table.remove(selections[buf])
|
||||
local node = nodes[#nodes] ---@type TSNode
|
||||
ts_utils.update_selection(buf, node)
|
||||
end
|
||||
|
||||
local FUNCTION_DESCRIPTIONS = {
|
||||
init_selection = "Start selecting nodes with nvim-treesitter",
|
||||
node_incremental = "Increment selection to named node",
|
||||
scope_incremental = "Increment selection to surrounding scope",
|
||||
node_decremental = "Shrink selection to previous named node",
|
||||
}
|
||||
|
||||
---@param bufnr integer
|
||||
function M.attach(bufnr)
|
||||
local config = configs.get_module "incremental_selection"
|
||||
for funcname, mapping in pairs(config.keymaps) do
|
||||
if mapping then
|
||||
local mode = funcname == "init_selection" and "n" or "x"
|
||||
local rhs = M[funcname] ---@type function
|
||||
|
||||
if not rhs then
|
||||
utils.notify("Unknown keybinding: " .. funcname .. debug.traceback(), vim.log.levels.ERROR)
|
||||
else
|
||||
vim.keymap.set(mode, mapping, rhs, { buffer = bufnr, silent = true, desc = FUNCTION_DESCRIPTIONS[funcname] })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.detach(bufnr)
|
||||
local config = configs.get_module "incremental_selection"
|
||||
for f, mapping in pairs(config.keymaps) do
|
||||
if mapping then
|
||||
local mode = f == "init_selection" and "n" or "x"
|
||||
local ok, err = pcall(vim.keymap.del, mode, mapping, { buffer = bufnr })
|
||||
if not ok then
|
||||
utils.notify(string.format('%s "%s" for mode %s', err, mapping, mode), vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,359 @@
|
||||
local ts = vim.treesitter
|
||||
local parsers = require "nvim-treesitter.parsers"
|
||||
|
||||
local M = {}
|
||||
|
||||
M.avoid_force_reparsing = {
|
||||
yaml = true,
|
||||
}
|
||||
|
||||
M.comment_parsers = {
|
||||
comment = true,
|
||||
jsdoc = true,
|
||||
phpdoc = true,
|
||||
}
|
||||
|
||||
local function getline(lnum)
|
||||
return vim.api.nvim_buf_get_lines(0, lnum - 1, lnum, false)[1] or ""
|
||||
end
|
||||
|
||||
---@param root TSNode
|
||||
---@param lnum integer
|
||||
---@param col? integer
|
||||
---@return TSNode
|
||||
local function get_first_node_at_line(root, lnum, col)
|
||||
col = col or vim.fn.indent(lnum)
|
||||
return root:descendant_for_range(lnum - 1, col, lnum - 1, col + 1)
|
||||
end
|
||||
|
||||
---@param root TSNode
|
||||
---@param lnum integer
|
||||
---@param col? integer
|
||||
---@return TSNode
|
||||
local function get_last_node_at_line(root, lnum, col)
|
||||
col = col or (#getline(lnum) - 1)
|
||||
return root:descendant_for_range(lnum - 1, col, lnum - 1, col + 1)
|
||||
end
|
||||
|
||||
---@param node TSNode
|
||||
---@return number
|
||||
local function node_length(node)
|
||||
local _, _, start_byte = node:start()
|
||||
local _, _, end_byte = node:end_()
|
||||
return end_byte - start_byte
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param node TSNode
|
||||
---@param delimiter string
|
||||
---@return TSNode|nil child
|
||||
---@return boolean|nil is_end
|
||||
local function find_delimiter(bufnr, node, delimiter)
|
||||
for child, _ in node:iter_children() do
|
||||
if child:type() == delimiter then
|
||||
local linenr = child:start()
|
||||
local line = vim.api.nvim_buf_get_lines(bufnr, linenr, linenr + 1, false)[1]
|
||||
local end_char = { child:end_() }
|
||||
local trimmed_after_delim
|
||||
local escaped_delimiter = delimiter:gsub("[%-%.%+%[%]%(%)%$%^%%%?%*]", "%%%1")
|
||||
trimmed_after_delim, _ = line:sub(end_char[2] + 1):gsub("[%s" .. escaped_delimiter .. "]*", "")
|
||||
return child, #trimmed_after_delim == 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Memoize a function using hash_fn to hash the arguments.
|
||||
---@generic F: function
|
||||
---@param fn F
|
||||
---@param hash_fn fun(...): any
|
||||
---@return F
|
||||
local function memoize(fn, hash_fn)
|
||||
local cache = setmetatable({}, { __mode = "kv" }) ---@type table<any,any>
|
||||
|
||||
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
|
||||
|
||||
local get_indents = memoize(function(bufnr, root, lang)
|
||||
local map = {
|
||||
["indent.auto"] = {},
|
||||
["indent.begin"] = {},
|
||||
["indent.end"] = {},
|
||||
["indent.dedent"] = {},
|
||||
["indent.branch"] = {},
|
||||
["indent.ignore"] = {},
|
||||
["indent.align"] = {},
|
||||
["indent.zero"] = {},
|
||||
}
|
||||
|
||||
--TODO(clason): remove when dropping Nvim 0.8 compat
|
||||
local query = (ts.query.get or ts.get_query)(lang, "indents")
|
||||
if not query then
|
||||
return map
|
||||
end
|
||||
for id, node, metadata in query:iter_captures(root, bufnr) do
|
||||
if query.captures[id]:sub(1, 1) ~= "_" then
|
||||
map[query.captures[id]][node:id()] = metadata or {}
|
||||
end
|
||||
end
|
||||
|
||||
return map
|
||||
end, function(bufnr, root, lang)
|
||||
return tostring(bufnr) .. root:id() .. "_" .. lang
|
||||
end)
|
||||
|
||||
---@param lnum number (1-indexed)
|
||||
function M.get_indent(lnum)
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local parser = parsers.get_parser(bufnr)
|
||||
if not parser or not lnum then
|
||||
return -1
|
||||
end
|
||||
|
||||
--TODO(clason): replace when dropping Nvim 0.8 compat
|
||||
local root_lang = parsers.get_buf_lang(bufnr)
|
||||
|
||||
-- some languages like Python will actually have worse results when re-parsing at opened new line
|
||||
if not M.avoid_force_reparsing[root_lang] then
|
||||
-- Reparse in case we got triggered by ":h indentkeys"
|
||||
parser:parse { vim.fn.line "w0" - 1, vim.fn.line "w$" }
|
||||
end
|
||||
|
||||
-- Get language tree with smallest range around node that's not a comment parser
|
||||
local root, lang_tree ---@type TSNode, LanguageTree
|
||||
parser:for_each_tree(function(tstree, tree)
|
||||
if not tstree or M.comment_parsers[tree:lang()] then
|
||||
return
|
||||
end
|
||||
local local_root = tstree:root()
|
||||
if ts.is_in_node_range(local_root, lnum - 1, 0) then
|
||||
if not root or node_length(root) >= node_length(local_root) then
|
||||
root = local_root
|
||||
lang_tree = tree
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Not likely, but just in case...
|
||||
if not root then
|
||||
return 0
|
||||
end
|
||||
|
||||
local q = get_indents(vim.api.nvim_get_current_buf(), root, lang_tree:lang())
|
||||
local is_empty_line = string.match(getline(lnum), "^%s*$") ~= nil
|
||||
local node ---@type TSNode
|
||||
if is_empty_line then
|
||||
local prevlnum = vim.fn.prevnonblank(lnum)
|
||||
local indent = vim.fn.indent(prevlnum)
|
||||
local prevline = vim.trim(getline(prevlnum))
|
||||
-- The final position can be trailing spaces, which should not affect indentation
|
||||
node = get_last_node_at_line(root, prevlnum, indent + #prevline - 1)
|
||||
if node:type():match "comment" then
|
||||
-- The final node we capture of the previous line can be a comment node, which should also be ignored
|
||||
-- Unless the last line is an entire line of comment, ignore the comment range and find the last node again
|
||||
local first_node = get_first_node_at_line(root, prevlnum, indent)
|
||||
local _, scol, _, _ = node:range()
|
||||
if first_node:id() ~= node:id() then
|
||||
-- In case the last captured node is a trailing comment node, re-trim the string
|
||||
prevline = vim.trim(prevline:sub(1, scol - indent))
|
||||
-- Add back indent as indent of prevline was trimmed away
|
||||
local col = indent + #prevline - 1
|
||||
node = get_last_node_at_line(root, prevlnum, col)
|
||||
end
|
||||
end
|
||||
if q["indent.end"][node:id()] then
|
||||
node = get_first_node_at_line(root, lnum)
|
||||
end
|
||||
else
|
||||
node = get_first_node_at_line(root, lnum)
|
||||
end
|
||||
|
||||
local indent_size = vim.fn.shiftwidth()
|
||||
local indent = 0
|
||||
local _, _, root_start = root:start()
|
||||
if root_start ~= 0 then
|
||||
-- injected tree
|
||||
indent = vim.fn.indent(root:start() + 1)
|
||||
end
|
||||
|
||||
-- tracks to ensure multiple indent levels are not applied for same line
|
||||
local is_processed_by_row = {}
|
||||
|
||||
if q["indent.zero"][node:id()] then
|
||||
return 0
|
||||
end
|
||||
|
||||
while node do
|
||||
-- do 'autoindent' if not marked as @indent
|
||||
if
|
||||
not q["indent.begin"][node:id()]
|
||||
and not q["indent.align"][node:id()]
|
||||
and q["indent.auto"][node:id()]
|
||||
and node:start() < lnum - 1
|
||||
and lnum - 1 <= node:end_()
|
||||
then
|
||||
return -1
|
||||
end
|
||||
|
||||
-- Do not indent if we are inside an @ignore block.
|
||||
-- If a node spans from L1,C1 to L2,C2, we know that lines where L1 < line <= L2 would
|
||||
-- have their indentations contained by the node.
|
||||
if
|
||||
not q["indent.begin"][node:id()]
|
||||
and q["indent.ignore"][node:id()]
|
||||
and node:start() < lnum - 1
|
||||
and lnum - 1 <= node:end_()
|
||||
then
|
||||
return 0
|
||||
end
|
||||
|
||||
local srow, _, erow = node:range()
|
||||
|
||||
local is_processed = false
|
||||
|
||||
if
|
||||
not is_processed_by_row[srow]
|
||||
and ((q["indent.branch"][node:id()] and srow == lnum - 1) or (q["indent.dedent"][node:id()] and srow ~= lnum - 1))
|
||||
then
|
||||
indent = indent - indent_size
|
||||
is_processed = true
|
||||
end
|
||||
|
||||
-- do not indent for nodes that starts-and-ends on same line and starts on target line (lnum)
|
||||
local should_process = not is_processed_by_row[srow]
|
||||
local is_in_err = false
|
||||
if should_process then
|
||||
local parent = node:parent()
|
||||
is_in_err = parent and parent:has_error()
|
||||
end
|
||||
if
|
||||
should_process
|
||||
and (
|
||||
q["indent.begin"][node:id()]
|
||||
and (srow ~= erow or is_in_err or q["indent.begin"][node:id()]["indent.immediate"])
|
||||
and (srow ~= lnum - 1 or q["indent.begin"][node:id()]["indent.start_at_same_line"])
|
||||
)
|
||||
then
|
||||
indent = indent + indent_size
|
||||
is_processed = true
|
||||
end
|
||||
|
||||
if is_in_err and not q["indent.align"][node:id()] then
|
||||
-- only when the node is in error, promote the
|
||||
-- first child's aligned indent to the error node
|
||||
-- to work around ((ERROR "X" . (_)) @aligned_indent (#set! "delimiter" "AB"))
|
||||
-- matching for all X, instead set do
|
||||
-- (ERROR "X" @aligned_indent (#set! "delimiter" "AB") . (_))
|
||||
-- and we will fish it out here.
|
||||
for c in node:iter_children() do
|
||||
if q["indent.align"][c:id()] then
|
||||
q["indent.align"][node:id()] = q["indent.align"][c:id()]
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
-- do not indent for nodes that starts-and-ends on same line and starts on target line (lnum)
|
||||
if should_process and q["indent.align"][node:id()] and (srow ~= erow or is_in_err) and (srow ~= lnum - 1) then
|
||||
local metadata = q["indent.align"][node:id()]
|
||||
local o_delim_node, o_is_last_in_line ---@type TSNode|nil, boolean|nil
|
||||
local c_delim_node, c_is_last_in_line ---@type TSNode|nil, boolean|nil, boolean|nil
|
||||
local indent_is_absolute = false
|
||||
if metadata["indent.open_delimiter"] then
|
||||
o_delim_node, o_is_last_in_line = find_delimiter(bufnr, node, metadata["indent.open_delimiter"])
|
||||
else
|
||||
o_delim_node = node
|
||||
end
|
||||
if metadata["indent.close_delimiter"] then
|
||||
c_delim_node, c_is_last_in_line = find_delimiter(bufnr, node, metadata["indent.close_delimiter"])
|
||||
else
|
||||
c_delim_node = node
|
||||
end
|
||||
|
||||
if o_delim_node then
|
||||
local o_srow, o_scol = o_delim_node:start()
|
||||
local c_srow = nil
|
||||
if c_delim_node then
|
||||
c_srow, _ = c_delim_node:start()
|
||||
end
|
||||
if o_is_last_in_line then
|
||||
-- hanging indent (previous line ended with starting delimiter)
|
||||
-- should be processed like indent
|
||||
if should_process then
|
||||
indent = indent + indent_size * 1
|
||||
if c_is_last_in_line then
|
||||
-- If current line is outside the range of a node marked with `@aligned_indent`
|
||||
-- Then its indent level shouldn't be affected by `@aligned_indent` node
|
||||
if c_srow and c_srow < lnum - 1 then
|
||||
indent = math.max(indent - indent_size, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
-- aligned indent
|
||||
if c_is_last_in_line and c_srow and o_srow ~= c_srow and c_srow < lnum - 1 then
|
||||
-- If current line is outside the range of a node marked with `@aligned_indent`
|
||||
-- Then its indent level shouldn't be affected by `@aligned_indent` node
|
||||
indent = math.max(indent - indent_size, 0)
|
||||
else
|
||||
indent = o_scol + (metadata["indent.increment"] or 1)
|
||||
indent_is_absolute = true
|
||||
end
|
||||
end
|
||||
-- deal with the final line
|
||||
local avoid_last_matching_next = false
|
||||
if c_srow and c_srow ~= o_srow and c_srow == lnum - 1 then
|
||||
-- delims end on current line, and are not open and closed same line.
|
||||
-- then this last line may need additional indent to avoid clashes
|
||||
-- with the next. `indent.avoid_last_matching_next` controls this behavior,
|
||||
-- for example this is needed for function parameters.
|
||||
avoid_last_matching_next = metadata["indent.avoid_last_matching_next"] or false
|
||||
end
|
||||
if avoid_last_matching_next then
|
||||
-- last line must be indented more in cases where
|
||||
-- it would be same indent as next line (we determine this as one
|
||||
-- width more than the open indent to avoid confusing with any
|
||||
-- hanging indents)
|
||||
if indent <= vim.fn.indent(o_srow + 1) + indent_size then
|
||||
indent = indent + indent_size * 1
|
||||
else
|
||||
indent = indent
|
||||
end
|
||||
end
|
||||
is_processed = true
|
||||
if indent_is_absolute then
|
||||
-- don't allow further indenting by parent nodes, this is an absolute position
|
||||
return indent
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
is_processed_by_row[srow] = is_processed_by_row[srow] or is_processed
|
||||
|
||||
node = node:parent()
|
||||
end
|
||||
|
||||
return indent
|
||||
end
|
||||
|
||||
---@type table<integer, string>
|
||||
local indent_funcs = {}
|
||||
|
||||
---@param bufnr integer
|
||||
function M.attach(bufnr)
|
||||
indent_funcs[bufnr] = vim.bo.indentexpr
|
||||
vim.bo.indentexpr = "nvim_treesitter#indent()"
|
||||
end
|
||||
|
||||
function M.detach(bufnr)
|
||||
vim.bo.indentexpr = indent_funcs[bufnr]
|
||||
end
|
||||
|
||||
return M
|
||||
190
config/neovim/store/nvim-treesitter/lua/nvim-treesitter/info.lua
Normal file
190
config/neovim/store/nvim-treesitter/lua/nvim-treesitter/info.lua
Normal file
@ -0,0 +1,190 @@
|
||||
local api = vim.api
|
||||
local configs = require "nvim-treesitter.configs"
|
||||
local parsers = require "nvim-treesitter.parsers"
|
||||
|
||||
local M = {}
|
||||
|
||||
local function install_info()
|
||||
local max_len = 0
|
||||
for _, ft in pairs(parsers.available_parsers()) do
|
||||
if #ft > max_len then
|
||||
max_len = #ft
|
||||
end
|
||||
end
|
||||
|
||||
local parser_list = parsers.available_parsers()
|
||||
table.sort(parser_list)
|
||||
for _, lang in pairs(parser_list) do
|
||||
local is_installed = #api.nvim_get_runtime_file("parser/" .. lang .. ".so", false) > 0
|
||||
api.nvim_out_write(lang .. string.rep(" ", max_len - #lang + 1))
|
||||
if is_installed then
|
||||
api.nvim_out_write "[✓] installed\n"
|
||||
elseif pcall(vim.treesitter.inspect_lang, lang) then
|
||||
api.nvim_out_write "[✗] not installed (but still loaded. Restart Neovim!)\n"
|
||||
else
|
||||
api.nvim_out_write "[✗] not installed\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Sort a list of modules into namespaces.
|
||||
-- {'mod1', 'mod2.sub1', 'mod2.sub2', 'mod3'}
|
||||
-- ->
|
||||
-- { default = {'mod1', 'mod3'}, mod2 = {'sub1', 'sub2'}}
|
||||
---@param modulelist string[]
|
||||
---@return table
|
||||
local function namespace_modules(modulelist)
|
||||
local modules = {}
|
||||
for _, module in ipairs(modulelist) do
|
||||
if module:find "%." then
|
||||
local namespace, submodule = module:match "^(.*)%.(.*)$"
|
||||
if not modules[namespace] then
|
||||
modules[namespace] = {}
|
||||
end
|
||||
table.insert(modules[namespace], submodule)
|
||||
else
|
||||
if not modules.default then
|
||||
modules.default = {}
|
||||
end
|
||||
table.insert(modules.default, module)
|
||||
end
|
||||
end
|
||||
return modules
|
||||
end
|
||||
|
||||
---@param list string[]
|
||||
---@return integer length
|
||||
local function longest_string_length(list)
|
||||
local length = 0
|
||||
for _, value in ipairs(list) do
|
||||
if #value > length then
|
||||
length = #value
|
||||
end
|
||||
end
|
||||
return length
|
||||
end
|
||||
|
||||
---@param curbuf integer
|
||||
---@param origbuf integer
|
||||
---@param parserlist string[]
|
||||
---@param namespace string
|
||||
---@param modulelist string[]
|
||||
local function append_module_table(curbuf, origbuf, parserlist, namespace, modulelist)
|
||||
local maxlen_parser = longest_string_length(parserlist)
|
||||
table.sort(modulelist)
|
||||
|
||||
-- header
|
||||
local header = ">> " .. namespace .. string.rep(" ", maxlen_parser - #namespace - 1)
|
||||
for _, module in pairs(modulelist) do
|
||||
header = header .. module .. " "
|
||||
end
|
||||
api.nvim_buf_set_lines(curbuf, -1, -1, true, { header })
|
||||
|
||||
-- actual table
|
||||
for _, parser in ipairs(parserlist) do
|
||||
local padding = string.rep(" ", maxlen_parser - #parser + 2)
|
||||
local line = parser .. padding
|
||||
local namespace_prefix = (namespace == "default") and "" or namespace .. "."
|
||||
for _, module in pairs(modulelist) do
|
||||
local modlen = #module
|
||||
module = namespace_prefix .. module
|
||||
if configs.is_enabled(module, parser, origbuf) then
|
||||
line = line .. "✓"
|
||||
else
|
||||
line = line .. "✗"
|
||||
end
|
||||
line = line .. string.rep(" ", modlen + 1)
|
||||
end
|
||||
api.nvim_buf_set_lines(curbuf, -1, -1, true, { line })
|
||||
end
|
||||
|
||||
api.nvim_buf_set_lines(curbuf, -1, -1, true, { "" })
|
||||
end
|
||||
|
||||
local function print_info_modules(parserlist, module)
|
||||
local origbuf = api.nvim_get_current_buf()
|
||||
api.nvim_command "enew"
|
||||
local curbuf = api.nvim_get_current_buf()
|
||||
|
||||
local modules
|
||||
if module then
|
||||
modules = namespace_modules { module }
|
||||
else
|
||||
modules = namespace_modules(configs.available_modules())
|
||||
end
|
||||
|
||||
---@type string[]
|
||||
local namespaces = {}
|
||||
for k, _ in pairs(modules) do
|
||||
table.insert(namespaces, k)
|
||||
end
|
||||
table.sort(namespaces)
|
||||
|
||||
table.sort(parserlist)
|
||||
for _, namespace in ipairs(namespaces) do
|
||||
append_module_table(curbuf, origbuf, parserlist, namespace, modules[namespace])
|
||||
end
|
||||
|
||||
api.nvim_buf_set_option(curbuf, "modified", false)
|
||||
api.nvim_buf_set_option(curbuf, "buftype", "nofile")
|
||||
vim.cmd [[
|
||||
syntax match TSModuleInfoGood /✓/
|
||||
syntax match TSModuleInfoBad /✗/
|
||||
syntax match TSModuleInfoHeader /^>>.*$/ contains=TSModuleInfoNamespace
|
||||
syntax match TSModuleInfoNamespace /^>> \w*/ contained
|
||||
syntax match TSModuleInfoParser /^[^> ]*\ze /
|
||||
]]
|
||||
|
||||
local highlights = {
|
||||
TSModuleInfoGood = { fg = "LightGreen", bold = true, default = true },
|
||||
TSModuleInfoBad = { fg = "Crimson", default = true },
|
||||
TSModuleInfoHeader = { link = "Type", default = true },
|
||||
TSModuleInfoNamespace = { link = "Statement", default = true },
|
||||
TSModuleInfoParser = { link = "Identifier", default = true },
|
||||
}
|
||||
for k, v in pairs(highlights) do
|
||||
api.nvim_set_hl(0, k, v)
|
||||
end
|
||||
end
|
||||
|
||||
local function module_info(module)
|
||||
if module and not configs.get_module(module) then
|
||||
return
|
||||
end
|
||||
|
||||
local parserlist = parsers.available_parsers()
|
||||
if module then
|
||||
print_info_modules(parserlist, module)
|
||||
else
|
||||
print_info_modules(parserlist)
|
||||
end
|
||||
end
|
||||
|
||||
---@return string[]
|
||||
function M.installed_parsers()
|
||||
local installed = {}
|
||||
for _, p in pairs(parsers.available_parsers()) do
|
||||
if parsers.has_parser(p) then
|
||||
table.insert(installed, p)
|
||||
end
|
||||
end
|
||||
return installed
|
||||
end
|
||||
|
||||
M.commands = {
|
||||
TSInstallInfo = {
|
||||
run = install_info,
|
||||
args = {
|
||||
"-nargs=0",
|
||||
},
|
||||
},
|
||||
TSModuleInfo = {
|
||||
run = module_info,
|
||||
args = {
|
||||
"-nargs=?",
|
||||
"-complete=custom,nvim_treesitter#available_modules",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return M
|
||||
@ -0,0 +1,777 @@
|
||||
local api = vim.api
|
||||
local fn = vim.fn
|
||||
local luv = vim.loop
|
||||
|
||||
local utils = require "nvim-treesitter.utils"
|
||||
local parsers = require "nvim-treesitter.parsers"
|
||||
local info = require "nvim-treesitter.info"
|
||||
local configs = require "nvim-treesitter.configs"
|
||||
local shell = require "nvim-treesitter.shell_command_selectors"
|
||||
local compat = require "nvim-treesitter.compat"
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class LockfileInfo
|
||||
---@field revision string
|
||||
|
||||
---@type table<string, LockfileInfo>
|
||||
local lockfile = {}
|
||||
|
||||
M.compilers = { vim.fn.getenv "CC", "cc", "gcc", "clang", "cl", "zig" }
|
||||
M.prefer_git = fn.has "win32" == 1
|
||||
M.command_extra_args = {}
|
||||
M.ts_generate_args = nil
|
||||
|
||||
local started_commands = 0
|
||||
local finished_commands = 0
|
||||
local failed_commands = 0
|
||||
local complete_std_output = {}
|
||||
local complete_error_output = {}
|
||||
|
||||
local function reset_progress_counter()
|
||||
if started_commands ~= finished_commands then
|
||||
return
|
||||
end
|
||||
started_commands = 0
|
||||
finished_commands = 0
|
||||
failed_commands = 0
|
||||
complete_std_output = {}
|
||||
complete_error_output = {}
|
||||
end
|
||||
|
||||
local function get_job_status()
|
||||
return "[nvim-treesitter] ["
|
||||
.. finished_commands
|
||||
.. "/"
|
||||
.. started_commands
|
||||
.. (failed_commands > 0 and ", failed: " .. failed_commands or "")
|
||||
.. "]"
|
||||
end
|
||||
|
||||
---@param lang string
|
||||
---@return function
|
||||
local function reattach_if_possible_fn(lang, error_on_fail)
|
||||
return function()
|
||||
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
|
||||
if parsers.get_buf_lang(buf) == lang then
|
||||
vim._ts_remove_language(lang)
|
||||
local ok, err
|
||||
if vim.treesitter.language.add then
|
||||
local ft = vim.bo[buf].filetype
|
||||
ok, err = pcall(vim.treesitter.language.add, lang, { filetype = ft })
|
||||
else
|
||||
ok, err = pcall(compat.require_language, lang)
|
||||
end
|
||||
if not ok and error_on_fail then
|
||||
vim.notify("Could not load parser for " .. lang .. ": " .. vim.inspect(err))
|
||||
end
|
||||
for _, mod in ipairs(require("nvim-treesitter.configs").available_modules()) do
|
||||
if ok then
|
||||
require("nvim-treesitter.configs").reattach_module(mod, buf, lang)
|
||||
else
|
||||
require("nvim-treesitter.configs").detach_module(mod, buf)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param lang string
|
||||
---@param validate boolean|nil
|
||||
---@return InstallInfo
|
||||
local function get_parser_install_info(lang, validate)
|
||||
local parser_config = parsers.get_parser_configs()[lang]
|
||||
|
||||
if not parser_config then
|
||||
error('Parser not available for language "' .. lang .. '"')
|
||||
end
|
||||
|
||||
local install_info = parser_config.install_info
|
||||
|
||||
if validate then
|
||||
vim.validate {
|
||||
url = { install_info.url, "string" },
|
||||
files = { install_info.files, "table" },
|
||||
}
|
||||
end
|
||||
|
||||
return install_info
|
||||
end
|
||||
|
||||
local function load_lockfile()
|
||||
local filename = utils.join_path(utils.get_package_path(), "lockfile.json")
|
||||
lockfile = vim.fn.filereadable(filename) == 1 and vim.fn.json_decode(vim.fn.readfile(filename)) or {}
|
||||
end
|
||||
|
||||
local function is_ignored_parser(lang)
|
||||
return vim.tbl_contains(configs.get_ignored_parser_installs(), lang)
|
||||
end
|
||||
|
||||
---@param lang string
|
||||
---@return string|nil
|
||||
local function get_revision(lang)
|
||||
if #lockfile == 0 then
|
||||
load_lockfile()
|
||||
end
|
||||
|
||||
local install_info = get_parser_install_info(lang)
|
||||
if install_info.revision then
|
||||
return install_info.revision
|
||||
end
|
||||
|
||||
if lockfile[lang] then
|
||||
return lockfile[lang].revision
|
||||
end
|
||||
end
|
||||
|
||||
---@param lang string
|
||||
---@return string|nil
|
||||
local function get_installed_revision(lang)
|
||||
local lang_file = utils.join_path(configs.get_parser_info_dir(), lang .. ".revision")
|
||||
if vim.fn.filereadable(lang_file) == 1 then
|
||||
return vim.fn.readfile(lang_file)[1]
|
||||
end
|
||||
end
|
||||
|
||||
-- Clean path for use in a prefix comparison
|
||||
---@param input string
|
||||
---@return string
|
||||
local function clean_path(input)
|
||||
local pth = vim.fn.fnamemodify(input, ":p")
|
||||
if fn.has "win32" == 1 then
|
||||
pth = pth:gsub("/", "\\")
|
||||
end
|
||||
return pth
|
||||
end
|
||||
|
||||
-- Checks if parser is installed with nvim-treesitter
|
||||
---@param lang string
|
||||
---@return boolean
|
||||
local function is_installed(lang)
|
||||
local matched_parsers = vim.api.nvim_get_runtime_file("parser/" .. lang .. ".so", true) or {}
|
||||
local install_dir = configs.get_parser_install_dir()
|
||||
if not install_dir then
|
||||
return false
|
||||
end
|
||||
install_dir = clean_path(install_dir)
|
||||
for _, path in ipairs(matched_parsers) do
|
||||
local abspath = clean_path(path)
|
||||
if vim.startswith(abspath, install_dir) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---@param lang string
|
||||
---@return boolean
|
||||
local function needs_update(lang)
|
||||
local revision = get_revision(lang)
|
||||
return not revision or revision ~= get_installed_revision(lang)
|
||||
end
|
||||
|
||||
---@return string[]
|
||||
local function outdated_parsers()
|
||||
return vim.tbl_filter(function(lang) ---@param lang string
|
||||
return is_installed(lang) and needs_update(lang)
|
||||
end, info.installed_parsers())
|
||||
end
|
||||
|
||||
---@param handle userdata
|
||||
---@param is_stderr boolean
|
||||
local function onread(handle, is_stderr)
|
||||
return function(_, data)
|
||||
if data then
|
||||
if is_stderr then
|
||||
complete_error_output[handle] = (complete_error_output[handle] or "") .. data
|
||||
else
|
||||
complete_std_output[handle] = (complete_std_output[handle] or "") .. data
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.iter_cmd(cmd_list, i, lang, success_message)
|
||||
if i == 1 then
|
||||
started_commands = started_commands + 1
|
||||
end
|
||||
if i == #cmd_list + 1 then
|
||||
finished_commands = finished_commands + 1
|
||||
return print(get_job_status() .. " " .. success_message)
|
||||
end
|
||||
|
||||
local attr = cmd_list[i]
|
||||
if attr.info then
|
||||
print(get_job_status() .. " " .. attr.info)
|
||||
end
|
||||
|
||||
if attr.opts and attr.opts.args and M.command_extra_args[attr.cmd] then
|
||||
vim.list_extend(attr.opts.args, M.command_extra_args[attr.cmd])
|
||||
end
|
||||
|
||||
if type(attr.cmd) == "function" then
|
||||
local ok, err = pcall(attr.cmd)
|
||||
if ok then
|
||||
M.iter_cmd(cmd_list, i + 1, lang, success_message)
|
||||
else
|
||||
failed_commands = failed_commands + 1
|
||||
finished_commands = finished_commands + 1
|
||||
return api.nvim_err_writeln(
|
||||
(attr.err or ("Failed to execute the following command:\n" .. vim.inspect(attr))) .. "\n" .. vim.inspect(err)
|
||||
)
|
||||
end
|
||||
else
|
||||
local handle
|
||||
local stdout = luv.new_pipe(false)
|
||||
local stderr = luv.new_pipe(false)
|
||||
attr.opts.stdio = { nil, stdout, stderr }
|
||||
---@type userdata
|
||||
handle = luv.spawn(
|
||||
attr.cmd,
|
||||
attr.opts,
|
||||
vim.schedule_wrap(function(code)
|
||||
if code ~= 0 then
|
||||
stdout:read_stop()
|
||||
stderr:read_stop()
|
||||
end
|
||||
stdout:close()
|
||||
stderr:close()
|
||||
handle:close()
|
||||
if code ~= 0 then
|
||||
failed_commands = failed_commands + 1
|
||||
finished_commands = finished_commands + 1
|
||||
if complete_std_output[handle] and complete_std_output[handle] ~= "" then
|
||||
print(complete_std_output[handle])
|
||||
end
|
||||
|
||||
local err_msg = complete_error_output[handle] or ""
|
||||
api.nvim_err_writeln(
|
||||
"nvim-treesitter["
|
||||
.. lang
|
||||
.. "]: "
|
||||
.. (attr.err or ("Failed to execute the following command:\n" .. vim.inspect(attr)))
|
||||
.. "\n"
|
||||
.. err_msg
|
||||
)
|
||||
return
|
||||
end
|
||||
M.iter_cmd(cmd_list, i + 1, lang, success_message)
|
||||
end)
|
||||
)
|
||||
luv.read_start(stdout, onread(handle, false))
|
||||
luv.read_start(stderr, onread(handle, true))
|
||||
end
|
||||
end
|
||||
|
||||
---@param cmd Command
|
||||
---@return string command
|
||||
local function get_command(cmd)
|
||||
local options = ""
|
||||
if cmd.opts and cmd.opts.args then
|
||||
if M.command_extra_args[cmd.cmd] then
|
||||
vim.list_extend(cmd.opts.args, M.command_extra_args[cmd.cmd])
|
||||
end
|
||||
for _, opt in ipairs(cmd.opts.args) do
|
||||
options = string.format("%s %s", options, opt)
|
||||
end
|
||||
end
|
||||
|
||||
local command = string.format("%s %s", cmd.cmd, options)
|
||||
if cmd.opts and cmd.opts.cwd then
|
||||
command = shell.make_directory_change_for_command(cmd.opts.cwd, command)
|
||||
end
|
||||
return command
|
||||
end
|
||||
|
||||
---@param cmd_list Command[]
|
||||
---@return boolean
|
||||
local function iter_cmd_sync(cmd_list)
|
||||
for _, cmd in ipairs(cmd_list) do
|
||||
if cmd.info then
|
||||
print(cmd.info)
|
||||
end
|
||||
|
||||
if type(cmd.cmd) == "function" then
|
||||
cmd.cmd()
|
||||
else
|
||||
local ret = vim.fn.system(get_command(cmd))
|
||||
if vim.v.shell_error ~= 0 then
|
||||
print(ret)
|
||||
api.nvim_err_writeln(
|
||||
(cmd.err and cmd.err .. "\n" or "") .. "Failed to execute the following command:\n" .. vim.inspect(cmd)
|
||||
)
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
---@param cache_folder string
|
||||
---@param install_folder string
|
||||
---@param lang string
|
||||
---@param repo InstallInfo
|
||||
---@param with_sync boolean
|
||||
---@param generate_from_grammar boolean
|
||||
local function run_install(cache_folder, install_folder, lang, repo, with_sync, generate_from_grammar)
|
||||
parsers.reset_cache()
|
||||
|
||||
local path_sep = utils.get_path_sep()
|
||||
|
||||
local project_name = "tree-sitter-" .. lang
|
||||
local maybe_local_path = vim.fn.expand(repo.url)
|
||||
local from_local_path = vim.fn.isdirectory(maybe_local_path) == 1
|
||||
if from_local_path then
|
||||
repo.url = maybe_local_path
|
||||
end
|
||||
|
||||
---@type string compile_location only needed for typescript installs.
|
||||
local compile_location
|
||||
if from_local_path then
|
||||
compile_location = repo.url
|
||||
if repo.location then
|
||||
compile_location = utils.join_path(compile_location, repo.location)
|
||||
end
|
||||
else
|
||||
local repo_location = project_name
|
||||
if repo.location then
|
||||
repo_location = repo_location .. "/" .. repo.location
|
||||
end
|
||||
repo_location = repo_location:gsub("/", path_sep)
|
||||
compile_location = utils.join_path(cache_folder, repo_location)
|
||||
end
|
||||
local parser_lib_name = utils.join_path(install_folder, lang) .. ".so"
|
||||
|
||||
generate_from_grammar = repo.requires_generate_from_grammar or generate_from_grammar
|
||||
|
||||
if generate_from_grammar and vim.fn.executable "tree-sitter" ~= 1 then
|
||||
api.nvim_err_writeln "tree-sitter CLI not found: `tree-sitter` is not executable!"
|
||||
if repo.requires_generate_from_grammar then
|
||||
api.nvim_err_writeln(
|
||||
"tree-sitter CLI is needed because `"
|
||||
.. lang
|
||||
.. "` is marked that it needs "
|
||||
.. "to be generated from the grammar definitions to be compatible with nvim!"
|
||||
)
|
||||
end
|
||||
return
|
||||
else
|
||||
if not M.ts_generate_args then
|
||||
local ts_cli_version = utils.ts_cli_version()
|
||||
if ts_cli_version and vim.split(ts_cli_version, " ")[1] > "0.20.2" then
|
||||
M.ts_generate_args = { "generate", "--no-bindings", "--abi", vim.treesitter.language_version }
|
||||
else
|
||||
M.ts_generate_args = { "generate", "--no-bindings" }
|
||||
end
|
||||
end
|
||||
end
|
||||
if generate_from_grammar and vim.fn.executable "node" ~= 1 then
|
||||
api.nvim_err_writeln "Node JS not found: `node` is not executable!"
|
||||
return
|
||||
end
|
||||
local cc = shell.select_executable(M.compilers)
|
||||
if not cc then
|
||||
api.nvim_err_writeln('No C compiler found! "' .. table.concat(
|
||||
vim.tbl_filter(function(c) ---@param c string
|
||||
return type(c) == "string"
|
||||
end, M.compilers),
|
||||
'", "'
|
||||
) .. '" are not executable.')
|
||||
return
|
||||
end
|
||||
|
||||
local revision = repo.revision
|
||||
if not revision then
|
||||
revision = get_revision(lang)
|
||||
end
|
||||
|
||||
---@class Command
|
||||
---@field cmd string
|
||||
---@field info string
|
||||
---@field err string
|
||||
---@field opts CmdOpts
|
||||
|
||||
---@class CmdOpts
|
||||
---@field args string[]
|
||||
---@field cwd string
|
||||
|
||||
---@type Command[]
|
||||
local command_list = {}
|
||||
if not from_local_path then
|
||||
vim.list_extend(command_list, { shell.select_install_rm_cmd(cache_folder, project_name) })
|
||||
vim.list_extend(
|
||||
command_list,
|
||||
shell.select_download_commands(repo, project_name, cache_folder, revision, M.prefer_git)
|
||||
)
|
||||
end
|
||||
if generate_from_grammar then
|
||||
if repo.generate_requires_npm then
|
||||
if vim.fn.executable "npm" ~= 1 then
|
||||
api.nvim_err_writeln("`" .. lang .. "` requires NPM to be installed from grammar.js")
|
||||
return
|
||||
end
|
||||
vim.list_extend(command_list, {
|
||||
{
|
||||
cmd = "npm",
|
||||
info = "Installing NPM dependencies of " .. lang .. " parser",
|
||||
err = "Error during `npm install` (required for parser generation of " .. lang .. " with npm dependencies)",
|
||||
opts = {
|
||||
args = { "install" },
|
||||
cwd = compile_location,
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
vim.list_extend(command_list, {
|
||||
{
|
||||
cmd = vim.fn.exepath "tree-sitter",
|
||||
info = "Generating source files from grammar.js...",
|
||||
err = 'Error during "tree-sitter generate"',
|
||||
opts = {
|
||||
args = M.ts_generate_args,
|
||||
cwd = compile_location,
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
vim.list_extend(command_list, {
|
||||
shell.select_compile_command(repo, cc, compile_location),
|
||||
shell.select_mv_cmd("parser.so", parser_lib_name, compile_location),
|
||||
{
|
||||
cmd = function()
|
||||
vim.fn.writefile({ revision or "" }, utils.join_path(configs.get_parser_info_dir() or "", lang .. ".revision"))
|
||||
end,
|
||||
},
|
||||
{ -- auto-attach modules after installation
|
||||
cmd = reattach_if_possible_fn(lang, true),
|
||||
},
|
||||
})
|
||||
if not from_local_path then
|
||||
vim.list_extend(command_list, { shell.select_install_rm_cmd(cache_folder, project_name) })
|
||||
end
|
||||
|
||||
if with_sync then
|
||||
if iter_cmd_sync(command_list) == true then
|
||||
print("Treesitter parser for " .. lang .. " has been installed")
|
||||
end
|
||||
else
|
||||
M.iter_cmd(command_list, 1, lang, "Treesitter parser for " .. lang .. " has been installed")
|
||||
end
|
||||
end
|
||||
|
||||
---@param lang string
|
||||
---@param ask_reinstall boolean|string
|
||||
---@param cache_folder string
|
||||
---@param install_folder string
|
||||
---@param with_sync boolean
|
||||
---@param generate_from_grammar boolean
|
||||
local function install_lang(lang, ask_reinstall, cache_folder, install_folder, with_sync, generate_from_grammar)
|
||||
if is_installed(lang) and ask_reinstall ~= "force" then
|
||||
if not ask_reinstall then
|
||||
return
|
||||
end
|
||||
|
||||
local yesno = fn.input(lang .. " parser already available: would you like to reinstall ? y/n: ")
|
||||
print "\n "
|
||||
if not string.match(yesno, "^y.*") then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local ok, install_info = pcall(get_parser_install_info, lang, true)
|
||||
if not ok then
|
||||
vim.notify("Installation not possible: " .. install_info, vim.log.levels.ERROR)
|
||||
if not parsers.get_parser_configs()[lang] then
|
||||
vim.notify(
|
||||
"See https://github.com/nvim-treesitter/nvim-treesitter/#adding-parsers on how to add a new parser!",
|
||||
vim.log.levels.INFO
|
||||
)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
run_install(cache_folder, install_folder, lang, install_info, with_sync, generate_from_grammar)
|
||||
end
|
||||
|
||||
---@class InstallOptions
|
||||
---@field with_sync boolean
|
||||
---@field ask_reinstall boolean|string
|
||||
---@field generate_from_grammar boolean
|
||||
---@field exclude_configured_parsers boolean
|
||||
|
||||
-- Install a parser
|
||||
---@param options? InstallOptions
|
||||
---@return function
|
||||
local function install(options)
|
||||
options = options or {}
|
||||
local with_sync = options.with_sync
|
||||
local ask_reinstall = options.ask_reinstall
|
||||
local generate_from_grammar = options.generate_from_grammar
|
||||
local exclude_configured_parsers = options.exclude_configured_parsers
|
||||
|
||||
return function(...)
|
||||
if fn.executable "git" == 0 then
|
||||
return api.nvim_err_writeln "Git is required on your system to run this command"
|
||||
end
|
||||
|
||||
local cache_folder, err = utils.get_cache_dir()
|
||||
if err then
|
||||
return api.nvim_err_writeln(err)
|
||||
end
|
||||
assert(cache_folder)
|
||||
|
||||
local install_folder
|
||||
install_folder, err = configs.get_parser_install_dir()
|
||||
if err then
|
||||
return api.nvim_err_writeln(err)
|
||||
end
|
||||
install_folder = install_folder and clean_path(install_folder)
|
||||
assert(install_folder)
|
||||
|
||||
local languages ---@type string[]
|
||||
local ask ---@type boolean|string
|
||||
if ... == "all" then
|
||||
languages = parsers.available_parsers()
|
||||
ask = false
|
||||
else
|
||||
languages = compat.flatten { ... }
|
||||
ask = ask_reinstall
|
||||
end
|
||||
|
||||
if exclude_configured_parsers then
|
||||
languages = utils.difference(languages, configs.get_ignored_parser_installs())
|
||||
end
|
||||
|
||||
if #languages > 1 then
|
||||
reset_progress_counter()
|
||||
end
|
||||
|
||||
for _, lang in ipairs(languages) do
|
||||
install_lang(lang, ask, cache_folder, install_folder, with_sync, generate_from_grammar)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.setup_auto_install()
|
||||
vim.api.nvim_create_autocmd("FileType", {
|
||||
pattern = { "*" },
|
||||
group = vim.api.nvim_create_augroup("NvimTreesitter-auto_install", { clear = true }),
|
||||
callback = function()
|
||||
local lang = parsers.get_buf_lang()
|
||||
if parsers.get_parser_configs()[lang] and not is_installed(lang) and not is_ignored_parser(lang) then
|
||||
install() { lang }
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
function M.update(options)
|
||||
options = options or {}
|
||||
return function(...)
|
||||
M.lockfile = {}
|
||||
reset_progress_counter()
|
||||
if ... and ... ~= "all" then
|
||||
---@type string[]
|
||||
local languages = compat.flatten { ... }
|
||||
local installed = 0
|
||||
for _, lang in ipairs(languages) do
|
||||
if (not is_installed(lang)) or (needs_update(lang)) then
|
||||
installed = installed + 1
|
||||
install {
|
||||
ask_reinstall = "force",
|
||||
with_sync = options.with_sync,
|
||||
}(lang)
|
||||
end
|
||||
end
|
||||
if installed == 0 then
|
||||
utils.notify "Parsers are up-to-date!"
|
||||
end
|
||||
else
|
||||
local parsers_to_update = outdated_parsers() or info.installed_parsers()
|
||||
if #parsers_to_update == 0 then
|
||||
utils.notify "All parsers are up-to-date!"
|
||||
end
|
||||
for _, lang in pairs(parsers_to_update) do
|
||||
install {
|
||||
ask_reinstall = "force",
|
||||
exclude_configured_parsers = true,
|
||||
with_sync = options.with_sync,
|
||||
}(lang)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.uninstall(...)
|
||||
if vim.tbl_contains({ "all" }, ...) then
|
||||
reset_progress_counter()
|
||||
local installed = info.installed_parsers()
|
||||
M.uninstall(installed)
|
||||
elseif ... then
|
||||
local ensure_installed_parsers = configs.get_ensure_installed_parsers()
|
||||
if ensure_installed_parsers == "all" then
|
||||
ensure_installed_parsers = parsers.available_parsers()
|
||||
end
|
||||
ensure_installed_parsers = utils.difference(ensure_installed_parsers, configs.get_ignored_parser_installs())
|
||||
|
||||
---@type string[]
|
||||
local languages = compat.flatten { ... }
|
||||
for _, lang in ipairs(languages) do
|
||||
local install_dir, err = configs.get_parser_install_dir()
|
||||
if err then
|
||||
return api.nvim_err_writeln(err)
|
||||
end
|
||||
install_dir = install_dir and clean_path(install_dir)
|
||||
|
||||
if vim.tbl_contains(ensure_installed_parsers, lang) then
|
||||
vim.notify(
|
||||
"Uninstalling "
|
||||
.. lang
|
||||
.. '. But the parser is still configured in "ensure_installed" setting of nvim-treesitter.'
|
||||
.. " Please consider updating your config!",
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
end
|
||||
|
||||
local parser_lib = utils.join_path(install_dir, lang) .. ".so"
|
||||
local all_parsers = vim.api.nvim_get_runtime_file("parser/" .. lang .. ".so", true)
|
||||
if vim.fn.filereadable(parser_lib) == 1 then
|
||||
local command_list = {
|
||||
shell.select_rm_file_cmd(parser_lib, "Uninstalling parser for " .. lang),
|
||||
{
|
||||
cmd = function()
|
||||
local all_parsers_after_deletion = vim.api.nvim_get_runtime_file("parser/" .. lang .. ".so", true)
|
||||
if #all_parsers_after_deletion > 0 then
|
||||
vim.notify(
|
||||
"Tried to uninstall parser for "
|
||||
.. lang
|
||||
.. "! But the parser is still installed (not by nvim-treesitter):"
|
||||
.. table.concat(all_parsers_after_deletion, ", "),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
end
|
||||
end,
|
||||
},
|
||||
{ -- auto-reattach or detach modules after uninstallation
|
||||
cmd = reattach_if_possible_fn(lang, false),
|
||||
},
|
||||
}
|
||||
M.iter_cmd(command_list, 1, lang, "Treesitter parser for " .. lang .. " has been uninstalled")
|
||||
elseif #all_parsers > 0 then
|
||||
vim.notify(
|
||||
"Parser for "
|
||||
.. lang
|
||||
.. " is installed! But not by nvim-treesitter! Please manually remove the following files: "
|
||||
.. table.concat(all_parsers, ", "),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.write_lockfile(verbose, skip_langs)
|
||||
local sorted_parsers = {} ---@type Parser[]
|
||||
-- Load previous lockfile
|
||||
load_lockfile()
|
||||
skip_langs = skip_langs or {}
|
||||
|
||||
for k, v in pairs(parsers.get_parser_configs()) do
|
||||
table.insert(sorted_parsers, { name = k, parser = v })
|
||||
end
|
||||
|
||||
---@param a Parser
|
||||
---@param b Parser
|
||||
table.sort(sorted_parsers, function(a, b)
|
||||
return a.name < b.name
|
||||
end)
|
||||
|
||||
for _, v in ipairs(sorted_parsers) do
|
||||
if not vim.tbl_contains(skip_langs, v.name) then
|
||||
-- I'm sure this can be done in aync way with iter_cmd
|
||||
local sha ---@type string
|
||||
if v.parser.install_info.branch then
|
||||
sha = vim.split(
|
||||
vim.fn.systemlist(
|
||||
"git ls-remote " .. v.parser.install_info.url .. " | grep refs/heads/" .. v.parser.install_info.branch
|
||||
)[1],
|
||||
"\t"
|
||||
)[1]
|
||||
else
|
||||
sha = vim.split(vim.fn.systemlist("git ls-remote " .. v.parser.install_info.url)[1], "\t")[1]
|
||||
end
|
||||
lockfile[v.name] = { revision = sha }
|
||||
if verbose then
|
||||
print(v.name .. ": " .. sha)
|
||||
end
|
||||
else
|
||||
print("Skipping " .. v.name)
|
||||
end
|
||||
end
|
||||
|
||||
if verbose then
|
||||
print(vim.inspect(lockfile))
|
||||
end
|
||||
vim.fn.writefile(
|
||||
vim.fn.split(vim.fn.json_encode(lockfile), "\n"),
|
||||
utils.join_path(utils.get_package_path(), "lockfile.json")
|
||||
)
|
||||
end
|
||||
|
||||
M.ensure_installed = install { exclude_configured_parsers = true }
|
||||
M.ensure_installed_sync = install { with_sync = true, exclude_configured_parsers = true }
|
||||
|
||||
M.commands = {
|
||||
TSInstall = {
|
||||
run = install { ask_reinstall = true },
|
||||
["run!"] = install { ask_reinstall = "force" },
|
||||
args = {
|
||||
"-nargs=+",
|
||||
"-bang",
|
||||
"-complete=custom,nvim_treesitter#installable_parsers",
|
||||
},
|
||||
},
|
||||
TSInstallFromGrammar = {
|
||||
run = install { generate_from_grammar = true, ask_reinstall = true },
|
||||
["run!"] = install { generate_from_grammar = true, ask_reinstall = "force" },
|
||||
args = {
|
||||
"-nargs=+",
|
||||
"-bang",
|
||||
"-complete=custom,nvim_treesitter#installable_parsers",
|
||||
},
|
||||
},
|
||||
TSInstallSync = {
|
||||
run = install { with_sync = true, ask_reinstall = true },
|
||||
["run!"] = install { with_sync = true, ask_reinstall = "force" },
|
||||
args = {
|
||||
"-nargs=+",
|
||||
"-bang",
|
||||
"-complete=custom,nvim_treesitter#installable_parsers",
|
||||
},
|
||||
},
|
||||
TSUpdate = {
|
||||
run = M.update {},
|
||||
args = {
|
||||
"-nargs=*",
|
||||
"-complete=custom,nvim_treesitter#installed_parsers",
|
||||
},
|
||||
},
|
||||
TSUpdateSync = {
|
||||
run = M.update { with_sync = true },
|
||||
args = {
|
||||
"-nargs=*",
|
||||
"-complete=custom,nvim_treesitter#installed_parsers",
|
||||
},
|
||||
},
|
||||
TSUninstall = {
|
||||
run = M.uninstall,
|
||||
args = {
|
||||
"-nargs=+",
|
||||
"-complete=custom,nvim_treesitter#installed_parsers",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return M
|
||||
@ -0,0 +1,365 @@
|
||||
-- Functions to handle locals
|
||||
-- Locals are a generalization of definition and scopes
|
||||
-- its the way nvim-treesitter uses to "understand" the code
|
||||
|
||||
local queries = require "nvim-treesitter.query"
|
||||
local ts_utils = require "nvim-treesitter.ts_utils"
|
||||
local ts = vim.treesitter
|
||||
local api = vim.api
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.collect_locals(bufnr)
|
||||
return queries.collect_group_results(bufnr, "locals")
|
||||
end
|
||||
|
||||
-- Iterates matches from a locals query file.
|
||||
-- @param bufnr the buffer
|
||||
-- @param root the root node
|
||||
function M.iter_locals(bufnr, root)
|
||||
return queries.iter_group_results(bufnr, "locals", root)
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@return any
|
||||
function M.get_locals(bufnr)
|
||||
return queries.get_matches(bufnr, "locals")
|
||||
end
|
||||
|
||||
-- Creates unique id for a node based on text and range
|
||||
---@param scope TSNode: the scope node of the definition
|
||||
---@param node_text string: the node text to use
|
||||
---@return string: a string id
|
||||
function M.get_definition_id(scope, node_text)
|
||||
-- Add a valid starting character in case node text doesn't start with a valid one.
|
||||
return table.concat({ "k", node_text or "", scope:range() }, "_")
|
||||
end
|
||||
|
||||
function M.get_definitions(bufnr)
|
||||
local locals = M.get_locals(bufnr)
|
||||
|
||||
local defs = {}
|
||||
|
||||
for _, loc in ipairs(locals) do
|
||||
if loc["local"]["definition"] then
|
||||
table.insert(defs, loc["local"]["definition"])
|
||||
end
|
||||
end
|
||||
|
||||
return defs
|
||||
end
|
||||
|
||||
function M.get_scopes(bufnr)
|
||||
local locals = M.get_locals(bufnr)
|
||||
|
||||
local scopes = {}
|
||||
|
||||
for _, loc in ipairs(locals) do
|
||||
if loc["local"]["scope"] and loc["local"]["scope"].node then
|
||||
table.insert(scopes, loc["local"]["scope"].node)
|
||||
end
|
||||
end
|
||||
|
||||
return scopes
|
||||
end
|
||||
|
||||
function M.get_references(bufnr)
|
||||
local locals = M.get_locals(bufnr)
|
||||
|
||||
local refs = {}
|
||||
|
||||
for _, loc in ipairs(locals) do
|
||||
if loc["local"]["reference"] and loc["local"]["reference"].node then
|
||||
table.insert(refs, loc["local"]["reference"].node)
|
||||
end
|
||||
end
|
||||
|
||||
return refs
|
||||
end
|
||||
|
||||
-- Gets a table with all the scopes containing a node
|
||||
-- The order is from most specific to least (bottom up)
|
||||
---@param node TSNode
|
||||
---@param bufnr integer
|
||||
---@return TSNode[]
|
||||
function M.get_scope_tree(node, bufnr)
|
||||
local scopes = {} ---@type TSNode[]
|
||||
|
||||
for scope in M.iter_scope_tree(node, bufnr) do
|
||||
table.insert(scopes, scope)
|
||||
end
|
||||
|
||||
return scopes
|
||||
end
|
||||
|
||||
-- Iterates over a nodes scopes moving from the bottom up
|
||||
---@param node TSNode
|
||||
---@param bufnr integer
|
||||
---@return fun(): TSNode|nil
|
||||
function M.iter_scope_tree(node, bufnr)
|
||||
local last_node = node
|
||||
return function()
|
||||
if not last_node then
|
||||
return
|
||||
end
|
||||
|
||||
local scope = M.containing_scope(last_node, bufnr, false) or ts_utils.get_root_for_node(node)
|
||||
|
||||
last_node = scope:parent()
|
||||
|
||||
return scope
|
||||
end
|
||||
end
|
||||
|
||||
-- Gets a table of all nodes and their 'kinds' from a locals list
|
||||
---@param local_def any: the local list result
|
||||
---@return table: a list of node entries
|
||||
function M.get_local_nodes(local_def)
|
||||
local result = {}
|
||||
|
||||
M.recurse_local_nodes(local_def, function(def, _node, kind)
|
||||
table.insert(result, vim.tbl_extend("keep", { kind = kind }, def))
|
||||
end)
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- Recurse locals results until a node is found.
|
||||
-- The accumulator function is given
|
||||
-- * The table of the node
|
||||
-- * The node
|
||||
-- * The full definition match `@definition.var.something` -> 'var.something'
|
||||
-- * The last definition match `@definition.var.something` -> 'something'
|
||||
---@param local_def any The locals result
|
||||
---@param accumulator function The accumulator function
|
||||
---@param full_match? string The full match path to append to
|
||||
---@param last_match? string The last match
|
||||
function M.recurse_local_nodes(local_def, accumulator, full_match, last_match)
|
||||
if type(local_def) ~= "table" then
|
||||
return
|
||||
end
|
||||
|
||||
if local_def.node then
|
||||
accumulator(local_def, local_def.node, full_match, last_match)
|
||||
else
|
||||
for match_key, def in pairs(local_def) do
|
||||
M.recurse_local_nodes(def, accumulator, full_match and (full_match .. "." .. match_key) or match_key, match_key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Get a single dimension table to look definition nodes.
|
||||
-- Keys are generated by using the range of the containing scope and the text of the definition node.
|
||||
-- This makes looking up a definition for a given scope a simple key lookup.
|
||||
--
|
||||
-- This is memoized by buffer tick. If the function is called in succession
|
||||
-- without the buffer tick changing, then the previous result will be used
|
||||
-- since the syntax tree hasn't changed.
|
||||
--
|
||||
-- Usage lookups require finding the definition of the node, so `find_definition`
|
||||
-- is called very frequently, which is why this lookup must be fast as possible.
|
||||
--
|
||||
---@param bufnr integer: the buffer
|
||||
---@return table result: a table for looking up definitions
|
||||
M.get_definitions_lookup_table = ts_utils.memoize_by_buf_tick(function(bufnr)
|
||||
local definitions = M.get_definitions(bufnr)
|
||||
local result = {}
|
||||
|
||||
for _, definition in ipairs(definitions) do
|
||||
for _, node_entry in ipairs(M.get_local_nodes(definition)) do
|
||||
local scopes = M.get_definition_scopes(node_entry.node, bufnr, node_entry.scope)
|
||||
-- Always use the highest valid scope
|
||||
local scope = scopes[#scopes]
|
||||
local node_text = ts.get_node_text(node_entry.node, bufnr)
|
||||
local id = M.get_definition_id(scope, node_text)
|
||||
|
||||
result[id] = node_entry
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end)
|
||||
|
||||
-- Gets all the scopes of a definition based on the scope type
|
||||
-- Scope types can be
|
||||
--
|
||||
-- "parent": Uses the parent of the containing scope, basically, skipping a scope
|
||||
-- "global": Uses the top most scope
|
||||
-- "local": Uses the containing scope of the definition. This is the default
|
||||
--
|
||||
---@param node TSNode: the definition node
|
||||
---@param bufnr integer: the buffer
|
||||
---@param scope_type string: the scope type
|
||||
function M.get_definition_scopes(node, bufnr, scope_type)
|
||||
local scopes = {}
|
||||
local scope_count = 1 ---@type integer|nil
|
||||
|
||||
-- Definition is valid for the containing scope
|
||||
-- and the containing scope of that scope
|
||||
if scope_type == "parent" then
|
||||
scope_count = 2
|
||||
-- Definition is valid in all parent scopes
|
||||
elseif scope_type == "global" then
|
||||
scope_count = nil
|
||||
end
|
||||
|
||||
local i = 0
|
||||
for scope in M.iter_scope_tree(node, bufnr) do
|
||||
table.insert(scopes, scope)
|
||||
i = i + 1
|
||||
|
||||
if scope_count and i >= scope_count then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return scopes
|
||||
end
|
||||
|
||||
---@param node TSNode
|
||||
---@param bufnr integer
|
||||
---@return TSNode node
|
||||
---@return TSNode scope
|
||||
---@return string|nil kind
|
||||
function M.find_definition(node, bufnr)
|
||||
local def_lookup = M.get_definitions_lookup_table(bufnr)
|
||||
local node_text = ts.get_node_text(node, bufnr)
|
||||
|
||||
for scope in M.iter_scope_tree(node, bufnr) do
|
||||
local id = M.get_definition_id(scope, node_text)
|
||||
|
||||
if def_lookup[id] then
|
||||
local entry = def_lookup[id]
|
||||
|
||||
return entry.node, scope, entry.kind
|
||||
end
|
||||
end
|
||||
|
||||
return node, ts_utils.get_root_for_node(node), nil
|
||||
end
|
||||
|
||||
-- Finds usages of a node in a given scope.
|
||||
---@param node TSNode the node to find usages for
|
||||
---@param scope_node TSNode the node to look within
|
||||
---@return TSNode[]: a list of nodes
|
||||
function M.find_usages(node, scope_node, bufnr)
|
||||
bufnr = bufnr or api.nvim_get_current_buf()
|
||||
local node_text = ts.get_node_text(node, bufnr)
|
||||
|
||||
if not node_text or #node_text < 1 then
|
||||
return {}
|
||||
end
|
||||
|
||||
local scope_node = scope_node or ts_utils.get_root_for_node(node)
|
||||
local usages = {}
|
||||
|
||||
for match in M.iter_locals(bufnr, scope_node) do
|
||||
match = match["local"]
|
||||
if match.reference and match.reference.node and ts.get_node_text(match.reference.node, bufnr) == node_text then
|
||||
local def_node, _, kind = M.find_definition(match.reference.node, bufnr)
|
||||
|
||||
if kind == nil or def_node == node then
|
||||
table.insert(usages, match.reference.node)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return usages
|
||||
end
|
||||
|
||||
---@param node TSNode
|
||||
---@param bufnr? integer
|
||||
---@param allow_scope? boolean
|
||||
---@return TSNode|nil
|
||||
function M.containing_scope(node, bufnr, allow_scope)
|
||||
local bufnr = bufnr or api.nvim_get_current_buf()
|
||||
local allow_scope = allow_scope == nil or allow_scope == true
|
||||
|
||||
local scopes = M.get_scopes(bufnr)
|
||||
if not node or not scopes then
|
||||
return
|
||||
end
|
||||
|
||||
local iter_node = node
|
||||
|
||||
while iter_node ~= nil and not vim.tbl_contains(scopes, iter_node) do
|
||||
iter_node = iter_node:parent()
|
||||
end
|
||||
|
||||
return iter_node or (allow_scope and node or nil)
|
||||
end
|
||||
|
||||
function M.nested_scope(node, cursor_pos)
|
||||
local bufnr = api.nvim_get_current_buf()
|
||||
|
||||
local scopes = M.get_scopes(bufnr)
|
||||
if not node or not scopes then
|
||||
return
|
||||
end
|
||||
|
||||
local row = cursor_pos.row ---@type integer
|
||||
local col = cursor_pos.col ---@type integer
|
||||
local scope = M.containing_scope(node)
|
||||
|
||||
for _, child in ipairs(ts_utils.get_named_children(scope)) do
|
||||
local row_, col_ = child:start()
|
||||
if vim.tbl_contains(scopes, child) and ((row_ + 1 == row and col_ > col) or row_ + 1 > row) then
|
||||
return child
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.next_scope(node)
|
||||
local bufnr = api.nvim_get_current_buf()
|
||||
|
||||
local scopes = M.get_scopes(bufnr)
|
||||
if not node or not scopes then
|
||||
return
|
||||
end
|
||||
|
||||
local scope = M.containing_scope(node)
|
||||
|
||||
local parent = scope:parent()
|
||||
if not parent then
|
||||
return
|
||||
end
|
||||
|
||||
local is_prev = true
|
||||
for _, child in ipairs(ts_utils.get_named_children(parent)) do
|
||||
if child == scope then
|
||||
is_prev = false
|
||||
elseif not is_prev and vim.tbl_contains(scopes, child) then
|
||||
return child
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param node TSNode
|
||||
---@return TSNode|nil
|
||||
function M.previous_scope(node)
|
||||
local bufnr = api.nvim_get_current_buf()
|
||||
|
||||
local scopes = M.get_scopes(bufnr)
|
||||
if not node or not scopes then
|
||||
return
|
||||
end
|
||||
|
||||
local scope = M.containing_scope(node)
|
||||
|
||||
local parent = scope:parent()
|
||||
if not parent then
|
||||
return
|
||||
end
|
||||
|
||||
local is_prev = true
|
||||
local children = ts_utils.get_named_children(parent)
|
||||
for i = #children, 1, -1 do
|
||||
if children[i] == scope then
|
||||
is_prev = false
|
||||
elseif not is_prev and vim.tbl_contains(scopes, children[i]) then
|
||||
return children[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
2518
config/neovim/store/nvim-treesitter/lua/nvim-treesitter/parsers.lua
Normal file
2518
config/neovim/store/nvim-treesitter/lua/nvim-treesitter/parsers.lua
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,453 @@
|
||||
local api = vim.api
|
||||
local ts = require "nvim-treesitter.compat"
|
||||
local tsrange = require "nvim-treesitter.tsrange"
|
||||
local utils = require "nvim-treesitter.utils"
|
||||
local parsers = require "nvim-treesitter.parsers"
|
||||
local caching = require "nvim-treesitter.caching"
|
||||
|
||||
local M = {}
|
||||
|
||||
local EMPTY_ITER = function() end
|
||||
|
||||
M.built_in_query_groups = { "highlights", "locals", "folds", "indents", "injections" }
|
||||
|
||||
-- Creates a function that checks whether a given query exists
|
||||
-- for a specific language.
|
||||
---@param query string
|
||||
---@return fun(string): boolean
|
||||
local function get_query_guard(query)
|
||||
return function(lang)
|
||||
return M.has_query_files(lang, query)
|
||||
end
|
||||
end
|
||||
|
||||
for _, query in ipairs(M.built_in_query_groups) do
|
||||
M["has_" .. query] = get_query_guard(query)
|
||||
end
|
||||
|
||||
---@return string[]
|
||||
function M.available_query_groups()
|
||||
local query_files = api.nvim_get_runtime_file("queries/*/*.scm", true)
|
||||
local groups = {}
|
||||
for _, f in ipairs(query_files) do
|
||||
groups[vim.fn.fnamemodify(f, ":t:r")] = true
|
||||
end
|
||||
local list = {}
|
||||
for k, _ in pairs(groups) do
|
||||
table.insert(list, k)
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
do
|
||||
local query_cache = caching.create_buffer_cache()
|
||||
|
||||
local function update_cached_matches(bufnr, changed_tick, query_group)
|
||||
query_cache.set(query_group, bufnr, {
|
||||
tick = changed_tick,
|
||||
cache = M.collect_group_results(bufnr, query_group) or {},
|
||||
})
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param query_group string
|
||||
---@return any
|
||||
function M.get_matches(bufnr, query_group)
|
||||
bufnr = bufnr or api.nvim_get_current_buf()
|
||||
local cached_local = query_cache.get(query_group, bufnr)
|
||||
if not cached_local or api.nvim_buf_get_changedtick(bufnr) > cached_local.tick then
|
||||
update_cached_matches(bufnr, api.nvim_buf_get_changedtick(bufnr), query_group)
|
||||
end
|
||||
|
||||
return query_cache.get(query_group, bufnr).cache
|
||||
end
|
||||
end
|
||||
|
||||
---@param lang string
|
||||
---@param query_name string
|
||||
---@return string[]
|
||||
local function runtime_queries(lang, query_name)
|
||||
return api.nvim_get_runtime_file(string.format("queries/%s/%s.scm", lang, query_name), true) or {}
|
||||
end
|
||||
|
||||
---@type table<string, table<string, boolean>>
|
||||
local query_files_cache = {}
|
||||
|
||||
---@param lang string
|
||||
---@param query_name string
|
||||
---@return boolean
|
||||
function M.has_query_files(lang, query_name)
|
||||
if not query_files_cache[lang] then
|
||||
query_files_cache[lang] = {}
|
||||
end
|
||||
if query_files_cache[lang][query_name] == nil then
|
||||
local files = runtime_queries(lang, query_name)
|
||||
query_files_cache[lang][query_name] = files and #files > 0
|
||||
end
|
||||
return query_files_cache[lang][query_name]
|
||||
end
|
||||
|
||||
do
|
||||
local mt = {}
|
||||
mt.__index = function(tbl, key)
|
||||
if rawget(tbl, key) == nil then
|
||||
rawset(tbl, key, {})
|
||||
end
|
||||
return rawget(tbl, key)
|
||||
end
|
||||
|
||||
-- cache will auto set the table for each lang if it is nil
|
||||
---@type table<string, table<string, Query>>
|
||||
local cache = setmetatable({}, mt)
|
||||
|
||||
-- Same as `vim.treesitter.query` except will return cached values
|
||||
---@param lang string
|
||||
---@param query_name string
|
||||
function M.get_query(lang, query_name)
|
||||
if cache[lang][query_name] == nil then
|
||||
cache[lang][query_name] = ts.get_query(lang, query_name)
|
||||
end
|
||||
|
||||
return cache[lang][query_name]
|
||||
end
|
||||
|
||||
-- Invalidates the query file cache.
|
||||
--
|
||||
-- If lang and query_name is both present, will reload for only the lang and query_name.
|
||||
-- If only lang is present, will reload all query_names for that lang
|
||||
-- If none are present, will reload everything
|
||||
---@param lang? string
|
||||
---@param query_name? string
|
||||
function M.invalidate_query_cache(lang, query_name)
|
||||
if lang and query_name then
|
||||
cache[lang][query_name] = nil
|
||||
if query_files_cache[lang] then
|
||||
query_files_cache[lang][query_name] = nil
|
||||
end
|
||||
elseif lang and not query_name then
|
||||
query_files_cache[lang] = nil
|
||||
for query_name0, _ in pairs(cache[lang]) do
|
||||
M.invalidate_query_cache(lang, query_name0)
|
||||
end
|
||||
elseif not lang and not query_name then
|
||||
query_files_cache = {}
|
||||
for lang0, _ in pairs(cache) do
|
||||
for query_name0, _ in pairs(cache[lang0]) do
|
||||
M.invalidate_query_cache(lang0, query_name0)
|
||||
end
|
||||
end
|
||||
else
|
||||
error "Cannot have query_name by itself!"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- This function is meant for an autocommand and not to be used. Only use if file is a query file.
|
||||
---@param fname string
|
||||
function M.invalidate_query_file(fname)
|
||||
local fnamemodify = vim.fn.fnamemodify
|
||||
M.invalidate_query_cache(fnamemodify(fname, ":p:h:t"), fnamemodify(fname, ":t:r"))
|
||||
end
|
||||
|
||||
---@class QueryInfo
|
||||
---@field root TSNode
|
||||
---@field source integer
|
||||
---@field start integer
|
||||
---@field stop integer
|
||||
|
||||
---@param bufnr integer
|
||||
---@param query_name string
|
||||
---@param root TSNode
|
||||
---@param root_lang string|nil
|
||||
---@return Query|nil, QueryInfo|nil
|
||||
local function prepare_query(bufnr, query_name, root, root_lang)
|
||||
local buf_lang = parsers.get_buf_lang(bufnr)
|
||||
|
||||
if not buf_lang then
|
||||
return
|
||||
end
|
||||
|
||||
local parser = parsers.get_parser(bufnr, buf_lang)
|
||||
if not parser then
|
||||
return
|
||||
end
|
||||
|
||||
if not root then
|
||||
local first_tree = parser:trees()[1]
|
||||
|
||||
if first_tree then
|
||||
root = first_tree:root()
|
||||
end
|
||||
end
|
||||
|
||||
if not root then
|
||||
return
|
||||
end
|
||||
|
||||
local range = { root:range() }
|
||||
|
||||
if not root_lang then
|
||||
local lang_tree = parser:language_for_range(range)
|
||||
|
||||
if lang_tree then
|
||||
root_lang = lang_tree:lang()
|
||||
end
|
||||
end
|
||||
|
||||
if not root_lang then
|
||||
return
|
||||
end
|
||||
|
||||
local query = M.get_query(root_lang, query_name)
|
||||
if not query then
|
||||
return
|
||||
end
|
||||
|
||||
return query,
|
||||
{
|
||||
root = root,
|
||||
source = bufnr,
|
||||
start = range[1],
|
||||
-- The end row is exclusive so we need to add 1 to it.
|
||||
stop = range[3] + 1,
|
||||
}
|
||||
end
|
||||
|
||||
-- Given a path (i.e. a List(String)) this functions inserts value at path
|
||||
---@param object any
|
||||
---@param path string[]
|
||||
---@param value any
|
||||
function M.insert_to_path(object, path, value)
|
||||
local curr_obj = object
|
||||
|
||||
for index = 1, (#path - 1) do
|
||||
if curr_obj[path[index]] == nil then
|
||||
curr_obj[path[index]] = {}
|
||||
end
|
||||
|
||||
curr_obj = curr_obj[path[index]]
|
||||
end
|
||||
|
||||
curr_obj[path[#path]] = value
|
||||
end
|
||||
|
||||
---@param query Query
|
||||
---@param bufnr integer
|
||||
---@param start_row integer
|
||||
---@param end_row integer
|
||||
function M.iter_prepared_matches(query, qnode, bufnr, start_row, end_row)
|
||||
-- A function that splits a string on '.'
|
||||
---@param to_split string
|
||||
---@return string[]
|
||||
local function split(to_split)
|
||||
local t = {}
|
||||
for str in string.gmatch(to_split, "([^.]+)") do
|
||||
table.insert(t, str)
|
||||
end
|
||||
|
||||
return t
|
||||
end
|
||||
|
||||
local matches = query:iter_matches(qnode, bufnr, start_row, end_row)
|
||||
|
||||
local function iterator()
|
||||
local pattern, match, metadata = matches()
|
||||
if pattern ~= nil then
|
||||
local prepared_match = {}
|
||||
|
||||
-- Extract capture names from each match
|
||||
for id, node in pairs(match) do
|
||||
local name = query.captures[id] -- name of the capture in the query
|
||||
if name ~= nil then
|
||||
local path = split(name .. ".node")
|
||||
M.insert_to_path(prepared_match, path, node)
|
||||
local metadata_path = split(name .. ".metadata")
|
||||
M.insert_to_path(prepared_match, metadata_path, metadata[id])
|
||||
end
|
||||
end
|
||||
|
||||
-- Add some predicates for testing
|
||||
---@type string[][] ( TODO: make pred type so this can be pred[])
|
||||
local preds = query.info.patterns[pattern]
|
||||
if preds then
|
||||
for _, pred in pairs(preds) do
|
||||
-- functions
|
||||
if pred[1] == "set!" and type(pred[2]) == "string" then
|
||||
M.insert_to_path(prepared_match, split(pred[2]), pred[3])
|
||||
end
|
||||
if pred[1] == "make-range!" and type(pred[2]) == "string" and #pred == 4 then
|
||||
M.insert_to_path(
|
||||
prepared_match,
|
||||
split(pred[2] .. ".node"),
|
||||
tsrange.TSRange.from_nodes(bufnr, match[pred[3]], match[pred[4]])
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return prepared_match
|
||||
end
|
||||
end
|
||||
return iterator
|
||||
end
|
||||
|
||||
-- Return all nodes corresponding to a specific capture path (like @definition.var, @reference.type)
|
||||
-- Works like M.get_references or M.get_scopes except you can choose the capture
|
||||
-- Can also be a nested capture like @definition.function to get all nodes defining a function.
|
||||
--
|
||||
---@param bufnr integer the buffer
|
||||
---@param captures string|string[]
|
||||
---@param query_group string the name of query group (highlights or injections for example)
|
||||
---@param root TSNode|nil node from where to start the search
|
||||
---@param lang string|nil the language from where to get the captures.
|
||||
--- Root nodes can have several languages.
|
||||
---@return table|nil
|
||||
function M.get_capture_matches(bufnr, captures, query_group, root, lang)
|
||||
if type(captures) == "string" then
|
||||
captures = { captures }
|
||||
end
|
||||
local strip_captures = {} ---@type string[]
|
||||
for i, capture in ipairs(captures) do
|
||||
if capture:sub(1, 1) ~= "@" then
|
||||
error 'Captures must start with "@"'
|
||||
return
|
||||
end
|
||||
-- Remove leading "@".
|
||||
strip_captures[i] = capture:sub(2)
|
||||
end
|
||||
|
||||
local matches = {}
|
||||
for match in M.iter_group_results(bufnr, query_group, root, lang) do
|
||||
for _, capture in ipairs(strip_captures) do
|
||||
local insert = utils.get_at_path(match, capture)
|
||||
if insert then
|
||||
table.insert(matches, insert)
|
||||
end
|
||||
end
|
||||
end
|
||||
return matches
|
||||
end
|
||||
|
||||
function M.iter_captures(bufnr, query_name, root, lang)
|
||||
local query, params = prepare_query(bufnr, query_name, root, lang)
|
||||
if not query then
|
||||
return EMPTY_ITER
|
||||
end
|
||||
assert(params)
|
||||
|
||||
local iter = query:iter_captures(params.root, params.source, params.start, params.stop)
|
||||
|
||||
local function wrapped_iter()
|
||||
local id, node, metadata = iter()
|
||||
if not id then
|
||||
return
|
||||
end
|
||||
|
||||
local name = query.captures[id]
|
||||
if string.sub(name, 1, 1) == "_" then
|
||||
return wrapped_iter()
|
||||
end
|
||||
|
||||
return name, node, metadata
|
||||
end
|
||||
|
||||
return wrapped_iter
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param capture_string string
|
||||
---@param query_group string
|
||||
---@param filter_predicate fun(match: table): boolean
|
||||
---@param scoring_function fun(match: table): number
|
||||
---@param root TSNode
|
||||
---@return table|unknown
|
||||
function M.find_best_match(bufnr, capture_string, query_group, filter_predicate, scoring_function, root)
|
||||
if string.sub(capture_string, 1, 1) == "@" then
|
||||
--remove leading "@"
|
||||
capture_string = string.sub(capture_string, 2)
|
||||
end
|
||||
|
||||
local best ---@type table|nil
|
||||
local best_score ---@type number
|
||||
|
||||
for maybe_match in M.iter_group_results(bufnr, query_group, root) do
|
||||
local match = utils.get_at_path(maybe_match, capture_string)
|
||||
|
||||
if match and filter_predicate(match) then
|
||||
local current_score = scoring_function(match)
|
||||
if not best then
|
||||
best = match
|
||||
best_score = current_score
|
||||
end
|
||||
if current_score > best_score then
|
||||
best = match
|
||||
best_score = current_score
|
||||
end
|
||||
end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
---Iterates matches from a query file.
|
||||
---@param bufnr integer the buffer
|
||||
---@param query_group string the query file to use
|
||||
---@param root TSNode the root node
|
||||
---@param root_lang? string the root node lang, if known
|
||||
function M.iter_group_results(bufnr, query_group, root, root_lang)
|
||||
local query, params = prepare_query(bufnr, query_group, root, root_lang)
|
||||
if not query then
|
||||
return EMPTY_ITER
|
||||
end
|
||||
assert(params)
|
||||
|
||||
return M.iter_prepared_matches(query, params.root, params.source, params.start, params.stop)
|
||||
end
|
||||
|
||||
function M.collect_group_results(bufnr, query_group, root, lang)
|
||||
local matches = {}
|
||||
|
||||
for prepared_match in M.iter_group_results(bufnr, query_group, root, lang) do
|
||||
table.insert(matches, prepared_match)
|
||||
end
|
||||
|
||||
return matches
|
||||
end
|
||||
|
||||
---@alias CaptureResFn function(string, LanguageTree, LanguageTree): string, string
|
||||
|
||||
-- Same as get_capture_matches except this will recursively get matches for every language in the tree.
|
||||
---@param bufnr integer The buffer
|
||||
---@param capture_or_fn string|CaptureResFn The capture to get. If a function is provided then that
|
||||
--- function will be used to resolve both the capture and query argument.
|
||||
--- The function can return `nil` to ignore that tree.
|
||||
---@param query_type string? The query to get the capture from. This is ignored if a function is provided
|
||||
--- for the capture argument.
|
||||
---@return table[]
|
||||
function M.get_capture_matches_recursively(bufnr, capture_or_fn, query_type)
|
||||
---@type CaptureResFn
|
||||
local type_fn
|
||||
if type(capture_or_fn) == "function" then
|
||||
type_fn = capture_or_fn
|
||||
else
|
||||
type_fn = function(_, _, _)
|
||||
return capture_or_fn, query_type
|
||||
end
|
||||
end
|
||||
local parser = parsers.get_parser(bufnr)
|
||||
local matches = {}
|
||||
|
||||
if parser then
|
||||
parser:for_each_tree(function(tree, lang_tree)
|
||||
local lang = lang_tree:lang()
|
||||
local capture, type_ = type_fn(lang, tree, lang_tree)
|
||||
|
||||
if capture then
|
||||
vim.list_extend(matches, M.get_capture_matches(bufnr, capture, type_, tree:root(), lang) or {})
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return matches
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,238 @@
|
||||
local query = require "vim.treesitter.query"
|
||||
|
||||
local html_script_type_languages = {
|
||||
["importmap"] = "json",
|
||||
["module"] = "javascript",
|
||||
["application/ecmascript"] = "javascript",
|
||||
["text/ecmascript"] = "javascript",
|
||||
}
|
||||
|
||||
local non_filetype_match_injection_language_aliases = {
|
||||
ex = "elixir",
|
||||
pl = "perl",
|
||||
sh = "bash",
|
||||
uxn = "uxntal",
|
||||
ts = "typescript",
|
||||
}
|
||||
|
||||
local function get_parser_from_markdown_info_string(injection_alias)
|
||||
local match = vim.filetype.match { filename = "a." .. injection_alias }
|
||||
return match or non_filetype_match_injection_language_aliases[injection_alias] or injection_alias
|
||||
end
|
||||
|
||||
local function error(str)
|
||||
vim.api.nvim_err_writeln(str)
|
||||
end
|
||||
|
||||
local function valid_args(name, pred, count, strict_count)
|
||||
local arg_count = #pred - 1
|
||||
|
||||
if strict_count then
|
||||
if arg_count ~= count then
|
||||
error(string.format("%s must have exactly %d arguments", name, count))
|
||||
return false
|
||||
end
|
||||
elseif arg_count < count then
|
||||
error(string.format("%s must have at least %d arguments", name, count))
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
---@param match (TSNode|nil)[]
|
||||
---@param _pattern string
|
||||
---@param _bufnr integer
|
||||
---@param pred string[]
|
||||
---@return boolean|nil
|
||||
query.add_predicate("nth?", function(match, _pattern, _bufnr, pred)
|
||||
if not valid_args("nth?", pred, 2, true) then
|
||||
return
|
||||
end
|
||||
|
||||
local node = match[pred[2]] ---@type TSNode
|
||||
local n = tonumber(pred[3])
|
||||
if node and node:parent() and node:parent():named_child_count() > n then
|
||||
return node:parent():named_child(n) == node
|
||||
end
|
||||
|
||||
return false
|
||||
end, true)
|
||||
|
||||
---@param match (TSNode|nil)[]
|
||||
---@param _pattern string
|
||||
---@param _bufnr integer
|
||||
---@param pred string[]
|
||||
---@return boolean|nil
|
||||
local function has_ancestor(match, _pattern, _bufnr, pred)
|
||||
if not valid_args(pred[1], pred, 2) then
|
||||
return
|
||||
end
|
||||
|
||||
local node = match[pred[2]]
|
||||
local ancestor_types = { unpack(pred, 3) }
|
||||
if not node then
|
||||
return true
|
||||
end
|
||||
|
||||
local just_direct_parent = pred[1]:find("has-parent", 1, true)
|
||||
|
||||
node = node:parent()
|
||||
while node do
|
||||
if vim.tbl_contains(ancestor_types, node:type()) then
|
||||
return true
|
||||
end
|
||||
if just_direct_parent then
|
||||
node = nil
|
||||
else
|
||||
node = node:parent()
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
query.add_predicate("has-ancestor?", has_ancestor, true)
|
||||
|
||||
query.add_predicate("has-parent?", has_ancestor, true)
|
||||
|
||||
---@param match (TSNode|nil)[]
|
||||
---@param _pattern string
|
||||
---@param bufnr integer
|
||||
---@param pred string[]
|
||||
---@return boolean|nil
|
||||
query.add_predicate("is?", function(match, _pattern, bufnr, pred)
|
||||
if not valid_args("is?", pred, 2) then
|
||||
return
|
||||
end
|
||||
|
||||
-- Avoid circular dependencies
|
||||
local locals = require "nvim-treesitter.locals"
|
||||
local node = match[pred[2]]
|
||||
local types = { unpack(pred, 3) }
|
||||
|
||||
if not node then
|
||||
return true
|
||||
end
|
||||
|
||||
local _, _, kind = locals.find_definition(node, bufnr)
|
||||
|
||||
return vim.tbl_contains(types, kind)
|
||||
end, true)
|
||||
|
||||
---@param match (TSNode|nil)[]
|
||||
---@param _pattern string
|
||||
---@param _bufnr integer
|
||||
---@param pred string[]
|
||||
---@return boolean|nil
|
||||
query.add_predicate("has-type?", function(match, _pattern, _bufnr, pred)
|
||||
if not valid_args(pred[1], pred, 2) then
|
||||
return
|
||||
end
|
||||
|
||||
local node = match[pred[2]]
|
||||
local types = { unpack(pred, 3) }
|
||||
|
||||
if not node then
|
||||
return true
|
||||
end
|
||||
|
||||
return vim.tbl_contains(types, node:type())
|
||||
end, true)
|
||||
|
||||
---@param match (TSNode|nil)[]
|
||||
---@param _ string
|
||||
---@param bufnr integer
|
||||
---@param pred string[]
|
||||
---@return boolean|nil
|
||||
query.add_directive("set-lang-from-mimetype!", function(match, _, bufnr, pred, metadata)
|
||||
local capture_id = pred[2]
|
||||
local node = match[capture_id]
|
||||
if not node then
|
||||
return
|
||||
end
|
||||
local type_attr_value = vim.treesitter.get_node_text(node, bufnr)
|
||||
local configured = html_script_type_languages[type_attr_value]
|
||||
if configured then
|
||||
metadata["injection.language"] = configured
|
||||
else
|
||||
local parts = vim.split(type_attr_value, "/", {})
|
||||
metadata["injection.language"] = parts[#parts]
|
||||
end
|
||||
end, true)
|
||||
|
||||
---@param match (TSNode|nil)[]
|
||||
---@param _ string
|
||||
---@param bufnr integer
|
||||
---@param pred string[]
|
||||
---@return boolean|nil
|
||||
query.add_directive("set-lang-from-info-string!", function(match, _, bufnr, pred, metadata)
|
||||
local capture_id = pred[2]
|
||||
local node = match[capture_id]
|
||||
if not node then
|
||||
return
|
||||
end
|
||||
local injection_alias = vim.treesitter.get_node_text(node, bufnr):lower()
|
||||
metadata["injection.language"] = get_parser_from_markdown_info_string(injection_alias)
|
||||
end, true)
|
||||
|
||||
-- Just avoid some annoying warnings for this directive
|
||||
query.add_directive("make-range!", function() end, true)
|
||||
|
||||
--- transform node text to lower case (e.g., to make @injection.language case insensitive)
|
||||
---
|
||||
---@param match (TSNode|nil)[]
|
||||
---@param _ string
|
||||
---@param bufnr integer
|
||||
---@param pred string[]
|
||||
---@return boolean|nil
|
||||
query.add_directive("downcase!", function(match, _, bufnr, pred, metadata)
|
||||
local id = pred[2]
|
||||
local node = match[id]
|
||||
if not node then
|
||||
return
|
||||
end
|
||||
|
||||
local text = vim.treesitter.get_node_text(node, bufnr, { metadata = metadata[id] }) or ""
|
||||
if not metadata[id] then
|
||||
metadata[id] = {}
|
||||
end
|
||||
metadata[id].text = string.lower(text)
|
||||
end, true)
|
||||
|
||||
-- Trim blank lines from end of the region
|
||||
-- Arguments are the captures to trim.
|
||||
---@param match (TSNode|nil)[]
|
||||
---@param _ string
|
||||
---@param bufnr integer
|
||||
---@param pred string[]
|
||||
---@param metadata table
|
||||
query.add_directive("trim!", function(match, _, bufnr, pred, metadata)
|
||||
for _, id in ipairs { select(2, unpack(pred)) } do
|
||||
local node = match[id]
|
||||
local start_row, start_col, end_row, end_col = node:range()
|
||||
|
||||
-- Don't trim if region ends in middle of a line
|
||||
if end_col ~= 0 then
|
||||
return
|
||||
end
|
||||
|
||||
while true do
|
||||
-- As we only care when end_col == 0, always inspect one line above end_row.
|
||||
local end_line = vim.api.nvim_buf_get_lines(bufnr, end_row - 1, end_row, true)[1]
|
||||
|
||||
if end_line ~= "" then
|
||||
break
|
||||
end
|
||||
|
||||
end_row = end_row - 1
|
||||
end
|
||||
|
||||
-- If this produces an invalid range, we just skip it.
|
||||
if start_row < end_row or (start_row == end_row and start_col <= end_col) then
|
||||
if not metadata[id] then
|
||||
metadata[id] = {}
|
||||
end
|
||||
metadata[id].range = { start_row, start_col, end_row, end_col }
|
||||
end
|
||||
end
|
||||
end, true)
|
||||
@ -0,0 +1,337 @@
|
||||
local fn = vim.fn
|
||||
local utils = require "nvim-treesitter.utils"
|
||||
|
||||
-- Convert path for cmd.exe on Windows.
|
||||
-- This is needed when vim.opt.shellslash is in use.
|
||||
---@param p string
|
||||
---@return string
|
||||
local function cmdpath(p)
|
||||
if vim.opt.shellslash:get() then
|
||||
local r = p:gsub("/", "\\")
|
||||
return r
|
||||
else
|
||||
return p
|
||||
end
|
||||
end
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Returns the mkdir command based on the OS
|
||||
---@param directory string
|
||||
---@param cwd string
|
||||
---@param info_msg string
|
||||
---@return table
|
||||
function M.select_mkdir_cmd(directory, cwd, info_msg)
|
||||
if fn.has "win32" == 1 then
|
||||
return {
|
||||
cmd = "cmd",
|
||||
opts = {
|
||||
args = { "/C", "mkdir", cmdpath(directory) },
|
||||
cwd = cwd,
|
||||
},
|
||||
info = info_msg,
|
||||
err = "Could not create " .. directory,
|
||||
}
|
||||
else
|
||||
return {
|
||||
cmd = "mkdir",
|
||||
opts = {
|
||||
args = { directory },
|
||||
cwd = cwd,
|
||||
},
|
||||
info = info_msg,
|
||||
err = "Could not create " .. directory,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns the remove command based on the OS
|
||||
---@param file string
|
||||
---@param info_msg string
|
||||
---@return table
|
||||
function M.select_rm_file_cmd(file, info_msg)
|
||||
if fn.has "win32" == 1 then
|
||||
return {
|
||||
cmd = "cmd",
|
||||
opts = {
|
||||
args = { "/C", "if", "exist", cmdpath(file), "del", cmdpath(file) },
|
||||
},
|
||||
info = info_msg,
|
||||
err = "Could not delete " .. file,
|
||||
}
|
||||
else
|
||||
return {
|
||||
cmd = "rm",
|
||||
opts = {
|
||||
args = { file },
|
||||
},
|
||||
info = info_msg,
|
||||
err = "Could not delete " .. file,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
---@param executables string[]
|
||||
---@return string|nil
|
||||
function M.select_executable(executables)
|
||||
return vim.tbl_filter(function(c) ---@param c string
|
||||
return c ~= vim.NIL and fn.executable(c) == 1
|
||||
end, executables)[1]
|
||||
end
|
||||
|
||||
-- Returns the compiler arguments based on the compiler and OS
|
||||
---@param repo InstallInfo
|
||||
---@param compiler string
|
||||
---@return string[]
|
||||
function M.select_compiler_args(repo, compiler)
|
||||
if string.match(compiler, "cl$") or string.match(compiler, "cl.exe$") then
|
||||
return {
|
||||
"/Fe:",
|
||||
"parser.so",
|
||||
"/Isrc",
|
||||
repo.files,
|
||||
"-Os",
|
||||
"/utf-8",
|
||||
"/LD",
|
||||
}
|
||||
elseif string.match(compiler, "zig$") or string.match(compiler, "zig.exe$") then
|
||||
return {
|
||||
"c++",
|
||||
"-o",
|
||||
"parser.so",
|
||||
repo.files,
|
||||
"-lc",
|
||||
"-Isrc",
|
||||
"-shared",
|
||||
"-Os",
|
||||
}
|
||||
else
|
||||
local args = {
|
||||
"-o",
|
||||
"parser.so",
|
||||
"-I./src",
|
||||
repo.files,
|
||||
"-Os",
|
||||
}
|
||||
if fn.has "mac" == 1 then
|
||||
table.insert(args, "-bundle")
|
||||
else
|
||||
table.insert(args, "-shared")
|
||||
end
|
||||
if
|
||||
#vim.tbl_filter(function(file) ---@param file string
|
||||
local ext = vim.fn.fnamemodify(file, ":e")
|
||||
return ext == "cc" or ext == "cpp" or ext == "cxx"
|
||||
end, repo.files) > 0
|
||||
then
|
||||
table.insert(args, "-lstdc++")
|
||||
end
|
||||
if fn.has "win32" == 0 then
|
||||
table.insert(args, "-fPIC")
|
||||
end
|
||||
return args
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns the compile command based on the OS and user options
|
||||
---@param repo InstallInfo
|
||||
---@param cc string
|
||||
---@param compile_location string
|
||||
---@return Command
|
||||
function M.select_compile_command(repo, cc, compile_location)
|
||||
local make = M.select_executable { "gmake", "make" }
|
||||
if
|
||||
string.match(cc, "cl$")
|
||||
or string.match(cc, "cl.exe$")
|
||||
or not repo.use_makefile
|
||||
or fn.has "win32" == 1
|
||||
or not make
|
||||
then
|
||||
return {
|
||||
cmd = cc,
|
||||
info = "Compiling...",
|
||||
err = "Error during compilation",
|
||||
opts = {
|
||||
args = require("nvim-treesitter.compat").flatten(M.select_compiler_args(repo, cc)),
|
||||
cwd = compile_location,
|
||||
},
|
||||
}
|
||||
else
|
||||
return {
|
||||
cmd = make,
|
||||
info = "Compiling...",
|
||||
err = "Error during compilation",
|
||||
opts = {
|
||||
args = {
|
||||
"--makefile=" .. utils.join_path(utils.get_package_path(), "scripts", "compile_parsers.makefile"),
|
||||
"CC=" .. cc,
|
||||
"CXX_STANDARD=" .. (repo.cxx_standard or "c++14"),
|
||||
},
|
||||
cwd = compile_location,
|
||||
},
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns the remove command based on the OS
|
||||
---@param cache_folder string
|
||||
---@param project_name string
|
||||
---@return Command
|
||||
function M.select_install_rm_cmd(cache_folder, project_name)
|
||||
if fn.has "win32" == 1 then
|
||||
local dir = cache_folder .. "\\" .. project_name
|
||||
return {
|
||||
cmd = "cmd",
|
||||
opts = {
|
||||
args = { "/C", "if", "exist", cmdpath(dir), "rmdir", "/s", "/q", cmdpath(dir) },
|
||||
},
|
||||
}
|
||||
else
|
||||
return {
|
||||
cmd = "rm",
|
||||
opts = {
|
||||
args = { "-rf", cache_folder .. "/" .. project_name },
|
||||
},
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns the move command based on the OS
|
||||
---@param from string
|
||||
---@param to string
|
||||
---@param cwd string
|
||||
---@return Command
|
||||
function M.select_mv_cmd(from, to, cwd)
|
||||
if fn.has "win32" == 1 then
|
||||
return {
|
||||
cmd = "cmd",
|
||||
opts = {
|
||||
args = { "/C", "move", "/Y", cmdpath(from), cmdpath(to) },
|
||||
cwd = cwd,
|
||||
},
|
||||
}
|
||||
else
|
||||
return {
|
||||
cmd = "mv",
|
||||
opts = {
|
||||
args = { "-f", from, to },
|
||||
cwd = cwd,
|
||||
},
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
---@param repo InstallInfo
|
||||
---@param project_name string
|
||||
---@param cache_folder string
|
||||
---@param revision string|nil
|
||||
---@param prefer_git boolean
|
||||
---@return table
|
||||
function M.select_download_commands(repo, project_name, cache_folder, revision, prefer_git)
|
||||
local can_use_tar = vim.fn.executable "tar" == 1 and vim.fn.executable "curl" == 1
|
||||
local is_github = repo.url:find("github.com", 1, true)
|
||||
local is_gitlab = repo.url:find("gitlab.com", 1, true)
|
||||
|
||||
revision = revision or repo.branch or "master"
|
||||
|
||||
if can_use_tar and (is_github or is_gitlab) and not prefer_git then
|
||||
local path_sep = utils.get_path_sep()
|
||||
local url = repo.url:gsub(".git$", "")
|
||||
|
||||
local folder_rev = revision
|
||||
if is_github and revision:match "^v%d" then
|
||||
folder_rev = revision:sub(2)
|
||||
end
|
||||
|
||||
return {
|
||||
M.select_install_rm_cmd(cache_folder, project_name .. "-tmp"),
|
||||
{
|
||||
cmd = "curl",
|
||||
info = "Downloading " .. project_name .. "...",
|
||||
err = "Error during download, please verify your internet connection",
|
||||
opts = {
|
||||
args = {
|
||||
"--silent",
|
||||
"-L", -- follow redirects
|
||||
is_github and url .. "/archive/" .. revision .. ".tar.gz"
|
||||
or url .. "/-/archive/" .. revision .. "/" .. project_name .. "-" .. revision .. ".tar.gz",
|
||||
"--output",
|
||||
project_name .. ".tar.gz",
|
||||
},
|
||||
cwd = cache_folder,
|
||||
},
|
||||
},
|
||||
M.select_mkdir_cmd(project_name .. "-tmp", cache_folder, "Creating temporary directory"),
|
||||
{
|
||||
cmd = "tar",
|
||||
info = "Extracting " .. project_name .. "...",
|
||||
err = "Error during tarball extraction.",
|
||||
opts = {
|
||||
args = {
|
||||
"-xvzf",
|
||||
project_name .. ".tar.gz",
|
||||
"-C",
|
||||
project_name .. "-tmp",
|
||||
},
|
||||
cwd = cache_folder,
|
||||
},
|
||||
},
|
||||
M.select_rm_file_cmd(cache_folder .. path_sep .. project_name .. ".tar.gz"),
|
||||
M.select_mv_cmd(
|
||||
utils.join_path(project_name .. "-tmp", url:match "[^/]-$" .. "-" .. folder_rev),
|
||||
project_name,
|
||||
cache_folder
|
||||
),
|
||||
M.select_install_rm_cmd(cache_folder, project_name .. "-tmp"),
|
||||
}
|
||||
else
|
||||
local git_folder = utils.join_path(cache_folder, project_name)
|
||||
local clone_error = "Error during download, please verify your internet connection"
|
||||
|
||||
return {
|
||||
{
|
||||
cmd = "git",
|
||||
info = "Downloading " .. project_name .. "...",
|
||||
err = clone_error,
|
||||
opts = {
|
||||
args = {
|
||||
"clone",
|
||||
repo.url,
|
||||
project_name,
|
||||
"--filter=blob:none",
|
||||
},
|
||||
cwd = cache_folder,
|
||||
},
|
||||
},
|
||||
{
|
||||
cmd = "git",
|
||||
info = "Checking out locked revision",
|
||||
err = "Error while checking out revision",
|
||||
opts = {
|
||||
args = {
|
||||
"checkout",
|
||||
revision,
|
||||
},
|
||||
cwd = git_folder,
|
||||
},
|
||||
},
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
---@param dir string
|
||||
---@param command string
|
||||
---@return string command
|
||||
function M.make_directory_change_for_command(dir, command)
|
||||
if fn.has "win32" == 1 then
|
||||
if string.find(vim.o.shell, "cmd") ~= nil then
|
||||
return string.format("pushd %s & %s & popd", cmdpath(dir), command)
|
||||
else
|
||||
return string.format("pushd %s ; %s ; popd", cmdpath(dir), command)
|
||||
end
|
||||
else
|
||||
return string.format("cd %s;\n%s", dir, command)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,53 @@
|
||||
local parsers = require "nvim-treesitter.parsers"
|
||||
local ts_utils = require "nvim-treesitter.ts_utils"
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Trim spaces and opening brackets from end
|
||||
local transform_line = function(line)
|
||||
return line:gsub("%s*[%[%(%{]*%s*$", "")
|
||||
end
|
||||
|
||||
function M.statusline(opts)
|
||||
if not parsers.has_parser() then
|
||||
return
|
||||
end
|
||||
local options = opts or {}
|
||||
if type(opts) == "number" then
|
||||
options = { indicator_size = opts }
|
||||
end
|
||||
local bufnr = options.bufnr or 0
|
||||
local indicator_size = options.indicator_size or 100
|
||||
local type_patterns = options.type_patterns or { "class", "function", "method" }
|
||||
local transform_fn = options.transform_fn or transform_line
|
||||
local separator = options.separator or " -> "
|
||||
local allow_duplicates = options.allow_duplicates or false
|
||||
|
||||
local current_node = ts_utils.get_node_at_cursor()
|
||||
if not current_node then
|
||||
return ""
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
local expr = current_node
|
||||
|
||||
while expr do
|
||||
local line = ts_utils._get_line_for_node(expr, type_patterns, transform_fn, bufnr)
|
||||
if line ~= "" then
|
||||
if allow_duplicates or not vim.tbl_contains(lines, line) then
|
||||
table.insert(lines, 1, line)
|
||||
end
|
||||
end
|
||||
expr = expr:parent()
|
||||
end
|
||||
|
||||
local text = table.concat(lines, separator)
|
||||
local text_len = #text
|
||||
if text_len > indicator_size then
|
||||
return "..." .. text:sub(text_len - indicator_size, text_len)
|
||||
end
|
||||
|
||||
return text
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,467 @@
|
||||
local api = vim.api
|
||||
|
||||
local parsers = require "nvim-treesitter.parsers"
|
||||
local utils = require "nvim-treesitter.utils"
|
||||
local ts = vim.treesitter
|
||||
|
||||
local M = {}
|
||||
|
||||
local function get_node_text(node, bufnr)
|
||||
bufnr = bufnr or api.nvim_get_current_buf()
|
||||
if not node then
|
||||
return {}
|
||||
end
|
||||
|
||||
-- We have to remember that end_col is end-exclusive
|
||||
local start_row, start_col, end_row, end_col = ts.get_node_range(node)
|
||||
|
||||
if start_row ~= end_row then
|
||||
local lines = api.nvim_buf_get_lines(bufnr, start_row, end_row + 1, false)
|
||||
if next(lines) == nil then
|
||||
return {}
|
||||
end
|
||||
lines[1] = string.sub(lines[1], start_col + 1)
|
||||
-- end_row might be just after the last line. In this case the last line is not truncated.
|
||||
if #lines == end_row - start_row + 1 then
|
||||
lines[#lines] = string.sub(lines[#lines], 1, end_col)
|
||||
end
|
||||
return lines
|
||||
else
|
||||
local line = api.nvim_buf_get_lines(bufnr, start_row, start_row + 1, false)[1]
|
||||
-- If line is nil then the line is empty
|
||||
return line and { string.sub(line, start_col + 1, end_col) } or {}
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param node TSNode
|
||||
---@param type_patterns string[]
|
||||
---@param transform_fn fun(line: string): string
|
||||
---@param bufnr integer
|
||||
---@return string
|
||||
function M._get_line_for_node(node, type_patterns, transform_fn, bufnr)
|
||||
local node_type = node:type()
|
||||
local is_valid = false
|
||||
for _, rgx in ipairs(type_patterns) do
|
||||
if node_type:find(rgx) then
|
||||
is_valid = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not is_valid then
|
||||
return ""
|
||||
end
|
||||
local line = transform_fn(vim.trim(get_node_text(node, bufnr)[1] or ""), node)
|
||||
-- Escape % to avoid statusline to evaluate content as expression
|
||||
return line:gsub("%%", "%%%%")
|
||||
end
|
||||
|
||||
-- Gets the actual text content of a node
|
||||
-- @deprecated Use vim.treesitter.query.get_node_text
|
||||
-- @param node the node to get the text from
|
||||
-- @param bufnr the buffer containing the node
|
||||
-- @return list of lines of text of the node
|
||||
function M.get_node_text(node, bufnr)
|
||||
vim.notify_once(
|
||||
"nvim-treesitter.ts_utils.get_node_text is deprecated: use vim.treesitter.query.get_node_text",
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return get_node_text(node, bufnr)
|
||||
end
|
||||
|
||||
-- Determines whether a node is the parent of another
|
||||
-- @param dest the possible parent
|
||||
-- @param source the possible child node
|
||||
function M.is_parent(dest, source)
|
||||
if not (dest and source) then
|
||||
return false
|
||||
end
|
||||
|
||||
local current = source
|
||||
while current ~= nil do
|
||||
if current == dest then
|
||||
return true
|
||||
end
|
||||
|
||||
current = current:parent()
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- Get next node with same parent
|
||||
---@param node TSNode
|
||||
---@param allow_switch_parents? boolean allow switching parents if last node
|
||||
---@param allow_next_parent? boolean allow next parent if last node and next parent without children
|
||||
function M.get_next_node(node, allow_switch_parents, allow_next_parent)
|
||||
local destination_node ---@type TSNode
|
||||
local parent = node:parent()
|
||||
|
||||
if not parent then
|
||||
return
|
||||
end
|
||||
local found_pos = 0
|
||||
for i = 0, parent:named_child_count() - 1, 1 do
|
||||
if parent:named_child(i) == node then
|
||||
found_pos = i
|
||||
break
|
||||
end
|
||||
end
|
||||
if parent:named_child_count() > found_pos + 1 then
|
||||
destination_node = parent:named_child(found_pos + 1)
|
||||
elseif allow_switch_parents then
|
||||
local next_node = M.get_next_node(node:parent())
|
||||
if next_node and next_node:named_child_count() > 0 then
|
||||
destination_node = next_node:named_child(0)
|
||||
elseif next_node and allow_next_parent then
|
||||
destination_node = next_node
|
||||
end
|
||||
end
|
||||
|
||||
return destination_node
|
||||
end
|
||||
|
||||
-- Get previous node with same parent
|
||||
---@param node TSNode
|
||||
---@param allow_switch_parents? boolean allow switching parents if first node
|
||||
---@param allow_previous_parent? boolean allow previous parent if first node and previous parent without children
|
||||
function M.get_previous_node(node, allow_switch_parents, allow_previous_parent)
|
||||
local destination_node ---@type TSNode
|
||||
local parent = node:parent()
|
||||
if not parent then
|
||||
return
|
||||
end
|
||||
|
||||
local found_pos = 0
|
||||
for i = 0, parent:named_child_count() - 1, 1 do
|
||||
if parent:named_child(i) == node then
|
||||
found_pos = i
|
||||
break
|
||||
end
|
||||
end
|
||||
if 0 < found_pos then
|
||||
destination_node = parent:named_child(found_pos - 1)
|
||||
elseif allow_switch_parents then
|
||||
local previous_node = M.get_previous_node(node:parent())
|
||||
if previous_node and previous_node:named_child_count() > 0 then
|
||||
destination_node = previous_node:named_child(previous_node:named_child_count() - 1)
|
||||
elseif previous_node and allow_previous_parent then
|
||||
destination_node = previous_node
|
||||
end
|
||||
end
|
||||
return destination_node
|
||||
end
|
||||
|
||||
function M.get_named_children(node)
|
||||
local nodes = {} ---@type TSNode[]
|
||||
for i = 0, node:named_child_count() - 1, 1 do
|
||||
nodes[i + 1] = node:named_child(i)
|
||||
end
|
||||
return nodes
|
||||
end
|
||||
|
||||
function M.get_node_at_cursor(winnr, ignore_injected_langs)
|
||||
winnr = winnr or 0
|
||||
local cursor = api.nvim_win_get_cursor(winnr)
|
||||
local cursor_range = { cursor[1] - 1, cursor[2] }
|
||||
|
||||
local buf = vim.api.nvim_win_get_buf(winnr)
|
||||
local root_lang_tree = parsers.get_parser(buf)
|
||||
if not root_lang_tree then
|
||||
return
|
||||
end
|
||||
|
||||
local root ---@type TSNode|nil
|
||||
if ignore_injected_langs then
|
||||
for _, tree in pairs(root_lang_tree:trees()) do
|
||||
local tree_root = tree:root()
|
||||
if tree_root and ts.is_in_node_range(tree_root, cursor_range[1], cursor_range[2]) then
|
||||
root = tree_root
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
root = M.get_root_for_position(cursor_range[1], cursor_range[2], root_lang_tree)
|
||||
end
|
||||
|
||||
if not root then
|
||||
return
|
||||
end
|
||||
|
||||
return root:named_descendant_for_range(cursor_range[1], cursor_range[2], cursor_range[1], cursor_range[2])
|
||||
end
|
||||
|
||||
function M.get_root_for_position(line, col, root_lang_tree)
|
||||
if not root_lang_tree then
|
||||
if not parsers.has_parser() then
|
||||
return
|
||||
end
|
||||
|
||||
root_lang_tree = parsers.get_parser()
|
||||
end
|
||||
|
||||
local lang_tree = root_lang_tree:language_for_range { line, col, line, col }
|
||||
|
||||
for _, tree in pairs(lang_tree:trees()) do
|
||||
local root = tree:root()
|
||||
|
||||
if root and ts.is_in_node_range(root, line, col) then
|
||||
return root, tree, lang_tree
|
||||
end
|
||||
end
|
||||
|
||||
-- This isn't a likely scenario, since the position must belong to a tree somewhere.
|
||||
return nil, nil, lang_tree
|
||||
end
|
||||
|
||||
---comment
|
||||
---@param node TSNode
|
||||
---@return TSNode result
|
||||
function M.get_root_for_node(node)
|
||||
local parent = node
|
||||
local result = node
|
||||
|
||||
while parent ~= nil do
|
||||
result = parent
|
||||
parent = result:parent()
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function M.highlight_node(node, buf, hl_namespace, hl_group)
|
||||
if not node then
|
||||
return
|
||||
end
|
||||
M.highlight_range({ node:range() }, buf, hl_namespace, hl_group)
|
||||
end
|
||||
|
||||
-- Get a compatible vim range (1 index based) from a TS node range.
|
||||
--
|
||||
-- TS nodes start with 0 and the end col is ending exclusive.
|
||||
-- They also treat a EOF/EOL char as a char ending in the first
|
||||
-- col of the next row.
|
||||
---comment
|
||||
---@param range integer[]
|
||||
---@param buf integer|nil
|
||||
---@return integer, integer, integer, integer
|
||||
function M.get_vim_range(range, buf)
|
||||
---@type integer, integer, integer, integer
|
||||
local srow, scol, erow, ecol = unpack(range)
|
||||
srow = srow + 1
|
||||
scol = scol + 1
|
||||
erow = erow + 1
|
||||
|
||||
if ecol == 0 then
|
||||
-- Use the value of the last col of the previous row instead.
|
||||
erow = erow - 1
|
||||
if not buf or buf == 0 then
|
||||
ecol = vim.fn.col { erow, "$" } - 1
|
||||
else
|
||||
ecol = #api.nvim_buf_get_lines(buf, erow - 1, erow, false)[1]
|
||||
end
|
||||
ecol = math.max(ecol, 1)
|
||||
end
|
||||
return srow, scol, erow, ecol
|
||||
end
|
||||
|
||||
function M.highlight_range(range, buf, hl_namespace, hl_group)
|
||||
---@type integer, integer, integer, integer
|
||||
local start_row, start_col, end_row, end_col = unpack(range)
|
||||
---@diagnostic disable-next-line: missing-parameter
|
||||
vim.highlight.range(buf, hl_namespace, hl_group, { start_row, start_col }, { end_row, end_col })
|
||||
end
|
||||
|
||||
-- Set visual selection to node
|
||||
-- @param selection_mode One of "charwise" (default) or "v", "linewise" or "V",
|
||||
-- "blockwise" or "<C-v>" (as a string with 5 characters or a single character)
|
||||
function M.update_selection(buf, node, selection_mode)
|
||||
local start_row, start_col, end_row, end_col = M.get_vim_range({ ts.get_node_range(node) }, buf)
|
||||
|
||||
local v_table = { charwise = "v", linewise = "V", blockwise = "<C-v>" }
|
||||
selection_mode = selection_mode or "charwise"
|
||||
|
||||
-- Normalise selection_mode
|
||||
if vim.tbl_contains(vim.tbl_keys(v_table), selection_mode) then
|
||||
selection_mode = v_table[selection_mode]
|
||||
end
|
||||
|
||||
-- enter visual mode if normal or operator-pending (no) mode
|
||||
-- Why? According to https://learnvimscriptthehardway.stevelosh.com/chapters/15.html
|
||||
-- If your operator-pending mapping ends with some text visually selected, Vim will operate on that text.
|
||||
-- Otherwise, Vim will operate on the text between the original cursor position and the new position.
|
||||
local mode = api.nvim_get_mode()
|
||||
if mode.mode ~= selection_mode then
|
||||
-- Call to `nvim_replace_termcodes()` is needed for sending appropriate command to enter blockwise mode
|
||||
selection_mode = vim.api.nvim_replace_termcodes(selection_mode, true, true, true)
|
||||
api.nvim_cmd({ cmd = "normal", bang = true, args = { selection_mode } }, {})
|
||||
end
|
||||
|
||||
api.nvim_win_set_cursor(0, { start_row, start_col - 1 })
|
||||
vim.cmd "normal! o"
|
||||
api.nvim_win_set_cursor(0, { end_row, end_col - 1 })
|
||||
end
|
||||
|
||||
-- Byte length of node range
|
||||
---@param node TSNode
|
||||
---@return number
|
||||
function M.node_length(node)
|
||||
local _, _, start_byte = node:start()
|
||||
local _, _, end_byte = node:end_()
|
||||
return end_byte - start_byte
|
||||
end
|
||||
|
||||
---@deprecated Use `vim.treesitter.is_in_node_range()` instead
|
||||
function M.is_in_node_range(node, line, col)
|
||||
vim.notify_once(
|
||||
"nvim-treesitter.ts_utils.is_in_node_range is deprecated: use vim.treesitter.is_in_node_range",
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return ts.is_in_node_range(node, line, col)
|
||||
end
|
||||
|
||||
---@deprecated Use `vim.treesitter.get_node_range()` instead
|
||||
function M.get_node_range(node_or_range)
|
||||
vim.notify_once(
|
||||
"nvim-treesitter.ts_utils.get_node_range is deprecated: use vim.treesitter.get_node_range",
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return ts.get_node_range(node_or_range)
|
||||
end
|
||||
|
||||
---@param node TSNode
|
||||
---@return table
|
||||
function M.node_to_lsp_range(node)
|
||||
local start_line, start_col, end_line, end_col = ts.get_node_range(node)
|
||||
local rtn = {}
|
||||
rtn.start = { line = start_line, character = start_col }
|
||||
rtn["end"] = { line = end_line, character = end_col }
|
||||
return rtn
|
||||
end
|
||||
|
||||
-- Memoizes a function based on the buffer tick of the provided bufnr.
|
||||
-- The cache entry is cleared when the buffer is detached to avoid memory leaks.
|
||||
-- The options argument is a table with two optional values:
|
||||
-- - bufnr: extracts a bufnr from the given arguments.
|
||||
-- - key: extracts the cache key from the given arguments.
|
||||
---@param fn function the fn to memoize, taking the buffer as first argument
|
||||
---@param options? {bufnr: integer?, key: string|fun(...): string?} the memoization options
|
||||
---@return function: a memoized function
|
||||
function M.memoize_by_buf_tick(fn, options)
|
||||
options = options or {}
|
||||
|
||||
---@type table<string, {result: any, last_tick: integer}>
|
||||
local cache = setmetatable({}, { __mode = "kv" })
|
||||
local bufnr_fn = utils.to_func(options.bufnr or utils.identity)
|
||||
local key_fn = utils.to_func(options.key or utils.identity)
|
||||
|
||||
return function(...)
|
||||
local bufnr = bufnr_fn(...)
|
||||
local key = key_fn(...)
|
||||
local tick = api.nvim_buf_get_changedtick(bufnr)
|
||||
|
||||
if cache[key] then
|
||||
if cache[key].last_tick == tick then
|
||||
return cache[key].result
|
||||
end
|
||||
else
|
||||
local function detach_handler()
|
||||
cache[key] = nil
|
||||
end
|
||||
|
||||
-- Clean up logic only!
|
||||
api.nvim_buf_attach(bufnr, false, {
|
||||
on_detach = detach_handler,
|
||||
on_reload = detach_handler,
|
||||
})
|
||||
end
|
||||
|
||||
cache[key] = {
|
||||
result = fn(...),
|
||||
last_tick = tick,
|
||||
}
|
||||
|
||||
return cache[key].result
|
||||
end
|
||||
end
|
||||
|
||||
function M.swap_nodes(node_or_range1, node_or_range2, bufnr, cursor_to_second)
|
||||
if not node_or_range1 or not node_or_range2 then
|
||||
return
|
||||
end
|
||||
local range1 = M.node_to_lsp_range(node_or_range1)
|
||||
local range2 = M.node_to_lsp_range(node_or_range2)
|
||||
|
||||
local text1 = get_node_text(node_or_range1, bufnr)
|
||||
local text2 = get_node_text(node_or_range2, bufnr)
|
||||
|
||||
local edit1 = { range = range1, newText = table.concat(text2, "\n") }
|
||||
local edit2 = { range = range2, newText = table.concat(text1, "\n") }
|
||||
vim.lsp.util.apply_text_edits({ edit1, edit2 }, bufnr, "utf-8")
|
||||
|
||||
if cursor_to_second then
|
||||
utils.set_jump()
|
||||
|
||||
local char_delta = 0
|
||||
local line_delta = 0
|
||||
if
|
||||
range1["end"].line < range2.start.line
|
||||
or (range1["end"].line == range2.start.line and range1["end"].character <= range2.start.character)
|
||||
then
|
||||
line_delta = #text2 - #text1
|
||||
end
|
||||
|
||||
if range1["end"].line == range2.start.line and range1["end"].character <= range2.start.character then
|
||||
if line_delta ~= 0 then
|
||||
--- why?
|
||||
--correction_after_line_change = -range2.start.character
|
||||
--text_now_before_range2 = #(text2[#text2])
|
||||
--space_between_ranges = range2.start.character - range1["end"].character
|
||||
--char_delta = correction_after_line_change + text_now_before_range2 + space_between_ranges
|
||||
--- Equivalent to:
|
||||
char_delta = #text2[#text2] - range1["end"].character
|
||||
|
||||
-- add range1.start.character if last line of range1 (now text2) does not start at 0
|
||||
if range1.start.line == range2.start.line + line_delta then
|
||||
char_delta = char_delta + range1.start.character
|
||||
end
|
||||
else
|
||||
char_delta = #text2[#text2] - #text1[#text1]
|
||||
end
|
||||
end
|
||||
|
||||
api.nvim_win_set_cursor(
|
||||
api.nvim_get_current_win(),
|
||||
{ range2.start.line + 1 + line_delta, range2.start.character + char_delta }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
function M.goto_node(node, goto_end, avoid_set_jump)
|
||||
if not node then
|
||||
return
|
||||
end
|
||||
if not avoid_set_jump then
|
||||
utils.set_jump()
|
||||
end
|
||||
local range = { M.get_vim_range { node:range() } }
|
||||
---@type table<number>
|
||||
local position
|
||||
if not goto_end then
|
||||
position = { range[1], range[2] }
|
||||
else
|
||||
position = { range[3], range[4] }
|
||||
end
|
||||
|
||||
-- Enter visual mode if we are in operator pending mode
|
||||
-- If we don't do this, it will miss the last character.
|
||||
local mode = vim.api.nvim_get_mode()
|
||||
if mode.mode == "no" then
|
||||
vim.cmd "normal! v"
|
||||
end
|
||||
|
||||
-- Position is 1, 0 indexed.
|
||||
api.nvim_win_set_cursor(0, { position[1], position[2] - 1 })
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,154 @@
|
||||
local M = {}
|
||||
local TSRange = {}
|
||||
TSRange.__index = TSRange
|
||||
|
||||
local api = vim.api
|
||||
local ts_utils = require "nvim-treesitter.ts_utils"
|
||||
local parsers = require "nvim-treesitter.parsers"
|
||||
|
||||
local function get_byte_offset(buf, row, col)
|
||||
return api.nvim_buf_get_offset(buf, row) + vim.fn.byteidx(api.nvim_buf_get_lines(buf, row, row + 1, false)[1], col)
|
||||
end
|
||||
|
||||
function TSRange.new(buf, start_row, start_col, end_row, end_col)
|
||||
return setmetatable({
|
||||
start_pos = { start_row, start_col, get_byte_offset(buf, start_row, start_col) },
|
||||
end_pos = { end_row, end_col, get_byte_offset(buf, end_row, end_col) },
|
||||
buf = buf,
|
||||
[1] = start_row,
|
||||
[2] = start_col,
|
||||
[3] = end_row,
|
||||
[4] = end_col,
|
||||
}, TSRange)
|
||||
end
|
||||
|
||||
function TSRange.from_nodes(buf, start_node, end_node)
|
||||
TSRange.__index = TSRange
|
||||
local start_pos = start_node and { start_node:start() } or { end_node:start() }
|
||||
local end_pos = end_node and { end_node:end_() } or { start_node:end_() }
|
||||
return setmetatable({
|
||||
start_pos = { start_pos[1], start_pos[2], start_pos[3] },
|
||||
end_pos = { end_pos[1], end_pos[2], end_pos[3] },
|
||||
buf = buf,
|
||||
[1] = start_pos[1],
|
||||
[2] = start_pos[2],
|
||||
[3] = end_pos[1],
|
||||
[4] = end_pos[2],
|
||||
}, TSRange)
|
||||
end
|
||||
|
||||
function TSRange.from_table(buf, range)
|
||||
return setmetatable({
|
||||
start_pos = { range[1], range[2], get_byte_offset(buf, range[1], range[2]) },
|
||||
end_pos = { range[3], range[4], get_byte_offset(buf, range[3], range[4]) },
|
||||
buf = buf,
|
||||
[1] = range[1],
|
||||
[2] = range[2],
|
||||
[3] = range[3],
|
||||
[4] = range[4],
|
||||
}, TSRange)
|
||||
end
|
||||
|
||||
function TSRange:parent()
|
||||
local root_lang_tree = parsers.get_parser(self.buf)
|
||||
local root = ts_utils.get_root_for_position(self[1], self[2], root_lang_tree)
|
||||
|
||||
return root
|
||||
and root:named_descendant_for_range(self.start_pos[1], self.start_pos[2], self.end_pos[1], self.end_pos[2])
|
||||
or nil
|
||||
end
|
||||
|
||||
function TSRange:field() end
|
||||
|
||||
function TSRange:child_count()
|
||||
return #self:collect_children()
|
||||
end
|
||||
|
||||
function TSRange:named_child_count()
|
||||
return #self:collect_children(function(c)
|
||||
return c:named()
|
||||
end)
|
||||
end
|
||||
|
||||
function TSRange:iter_children()
|
||||
local raw_iterator = self:parent().iter_children()
|
||||
return function()
|
||||
while true do
|
||||
local node = raw_iterator()
|
||||
if not node then
|
||||
return
|
||||
end
|
||||
local _, _, start_byte = node:start()
|
||||
local _, _, end_byte = node:end_()
|
||||
if start_byte >= self.start_pos[3] and end_byte <= self.end_pos[3] then
|
||||
return node
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function TSRange:collect_children(filter_fun)
|
||||
local children = {}
|
||||
for _, c in self:iter_children() do
|
||||
if not filter_fun or filter_fun(c) then
|
||||
table.insert(children, c)
|
||||
end
|
||||
end
|
||||
return children
|
||||
end
|
||||
|
||||
function TSRange:child(index)
|
||||
return self:collect_children()[index + 1]
|
||||
end
|
||||
|
||||
function TSRange:named_child(index)
|
||||
return self:collect_children(function(c)
|
||||
return c.named()
|
||||
end)[index + 1]
|
||||
end
|
||||
|
||||
function TSRange:start()
|
||||
return unpack(self.start_pos)
|
||||
end
|
||||
|
||||
function TSRange:end_()
|
||||
return unpack(self.end_pos)
|
||||
end
|
||||
|
||||
function TSRange:range()
|
||||
return self.start_pos[1], self.start_pos[2], self.end_pos[1], self.end_pos[2]
|
||||
end
|
||||
|
||||
function TSRange:type()
|
||||
return "nvim-treesitter-range"
|
||||
end
|
||||
|
||||
function TSRange:symbol()
|
||||
return -1
|
||||
end
|
||||
|
||||
function TSRange:named()
|
||||
return false
|
||||
end
|
||||
|
||||
function TSRange:missing()
|
||||
return false
|
||||
end
|
||||
|
||||
function TSRange:has_error()
|
||||
return #self:collect_children(function(c)
|
||||
return c:has_error()
|
||||
end) > 0 and true or false
|
||||
end
|
||||
|
||||
function TSRange:sexpr()
|
||||
return table.concat(
|
||||
vim.tbl_map(function(c)
|
||||
return c:sexpr()
|
||||
end, self:collect_children()),
|
||||
" "
|
||||
)
|
||||
end
|
||||
|
||||
M.TSRange = TSRange
|
||||
return M
|
||||
@ -0,0 +1,237 @@
|
||||
local api = vim.api
|
||||
local fn = vim.fn
|
||||
local luv = vim.loop
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Wrapper around vim.notify with common options set.
|
||||
---@param msg string
|
||||
---@param log_level number|nil
|
||||
---@param opts table|nil
|
||||
function M.notify(msg, log_level, opts)
|
||||
local default_opts = { title = "nvim-treesitter" }
|
||||
vim.notify(msg, log_level, vim.tbl_extend("force", default_opts, opts or {}))
|
||||
end
|
||||
|
||||
-- Returns the system-specific path separator.
|
||||
---@return string
|
||||
function M.get_path_sep()
|
||||
return (fn.has "win32" == 1 and not vim.opt.shellslash:get()) and "\\" or "/"
|
||||
end
|
||||
|
||||
-- Returns a function that joins the given arguments with separator. Arguments
|
||||
-- can't be nil. Example:
|
||||
--
|
||||
--[[
|
||||
print(M.generate_join(" ")("foo", "bar"))
|
||||
--]]
|
||||
--prints "foo bar"
|
||||
---@param separator string
|
||||
---@return fun(...: string): string
|
||||
function M.generate_join(separator)
|
||||
return function(...)
|
||||
return table.concat({ ... }, separator)
|
||||
end
|
||||
end
|
||||
|
||||
M.join_path = M.generate_join(M.get_path_sep())
|
||||
|
||||
M.join_space = M.generate_join " "
|
||||
|
||||
---@class Command
|
||||
---@field run function
|
||||
---@field f_args string
|
||||
---@field args string
|
||||
|
||||
-- Define user defined vim command which calls nvim-treesitter module function
|
||||
-- - If module name is 'mod', it should be defined in hierarchy 'nvim-treesitter.mod'
|
||||
-- - A table with name 'commands' should be defined in 'mod' which needs to be passed as
|
||||
-- the commands param of this function
|
||||
--
|
||||
---@param mod string Name of the module that resides in the hierarchy - nvim-treesitter.module
|
||||
---@param commands table<string, Command> Command list for the module
|
||||
--- - {command_name} Name of the vim user defined command, Keys:
|
||||
--- - {run}: (function) callback function that needs to be executed
|
||||
--- - {f_args}: (string, default <f-args>)
|
||||
--- - type of arguments that needs to be passed to the vim command
|
||||
--- - {args}: (string, optional)
|
||||
--- - vim command attributes
|
||||
---
|
||||
---* @example
|
||||
--- If module is nvim-treesitter.custom_mod
|
||||
--- <pre>
|
||||
--- M.commands = {
|
||||
--- custom_command = {
|
||||
--- run = M.module_function,
|
||||
--- f_args = "<f-args>",
|
||||
--- args = {
|
||||
--- "-range"
|
||||
--- }
|
||||
--- }
|
||||
--- }
|
||||
---
|
||||
--- utils.setup_commands("custom_mod", require("nvim-treesitter.custom_mod").commands)
|
||||
--- </pre>
|
||||
---
|
||||
--- Will generate command :
|
||||
--- <pre>
|
||||
--- command! -range custom_command \
|
||||
--- lua require'nvim-treesitter.custom_mod'.commands.custom_command['run<bang>'](<f-args>)
|
||||
--- </pre>
|
||||
function M.setup_commands(mod, commands)
|
||||
for command_name, def in pairs(commands) do
|
||||
local f_args = def.f_args or "<f-args>"
|
||||
local call_fn =
|
||||
string.format("lua require'nvim-treesitter.%s'.commands.%s['run<bang>'](%s)", mod, command_name, f_args)
|
||||
local parts = require("nvim-treesitter.compat").flatten {
|
||||
"command!",
|
||||
"-bar",
|
||||
def.args,
|
||||
command_name,
|
||||
call_fn,
|
||||
}
|
||||
api.nvim_command(table.concat(parts, " "))
|
||||
end
|
||||
end
|
||||
|
||||
---@param dir string
|
||||
---@param create_err string
|
||||
---@param writeable_err string
|
||||
---@return string|nil, string|nil
|
||||
function M.create_or_reuse_writable_dir(dir, create_err, writeable_err)
|
||||
create_err = create_err or M.join_space("Could not create dir '", dir, "': ")
|
||||
writeable_err = writeable_err or M.join_space("Invalid rights, '", dir, "' should be read/write")
|
||||
-- Try creating and using parser_dir if it doesn't exist
|
||||
if not luv.fs_stat(dir) then
|
||||
local ok, error = pcall(vim.fn.mkdir, dir, "p", "0755")
|
||||
if not ok then
|
||||
return nil, M.join_space(create_err, error)
|
||||
end
|
||||
|
||||
return dir
|
||||
end
|
||||
|
||||
-- parser_dir exists, use it if it's read/write
|
||||
if luv.fs_access(dir, "RW") then
|
||||
return dir
|
||||
end
|
||||
|
||||
-- parser_dir exists but isn't read/write, give up
|
||||
return nil, M.join_space(writeable_err, dir, "'")
|
||||
end
|
||||
|
||||
function M.get_package_path()
|
||||
-- Path to this source file, removing the leading '@'
|
||||
local source = string.sub(debug.getinfo(1, "S").source, 2)
|
||||
|
||||
-- Path to the package root
|
||||
return fn.fnamemodify(source, ":p:h:h:h")
|
||||
end
|
||||
|
||||
function M.get_cache_dir()
|
||||
local cache_dir = fn.stdpath "data"
|
||||
|
||||
if luv.fs_access(cache_dir, "RW") then
|
||||
return cache_dir
|
||||
elseif luv.fs_access("/tmp", "RW") then
|
||||
return "/tmp"
|
||||
end
|
||||
|
||||
return nil, M.join_space("Invalid cache rights,", fn.stdpath "data", "or /tmp should be read/write")
|
||||
end
|
||||
|
||||
-- Returns $XDG_DATA_HOME/nvim/site, but could use any directory that is in
|
||||
-- runtimepath
|
||||
function M.get_site_dir()
|
||||
return M.join_path(fn.stdpath "data", "site")
|
||||
end
|
||||
|
||||
-- Gets a property at path
|
||||
---@param tbl table the table to access
|
||||
---@param path string the '.' separated path
|
||||
---@return table|nil result the value at path or nil
|
||||
function M.get_at_path(tbl, path)
|
||||
if path == "" then
|
||||
return tbl
|
||||
end
|
||||
|
||||
local segments = vim.split(path, ".", true)
|
||||
---@type table[]|table
|
||||
local result = tbl
|
||||
|
||||
for _, segment in ipairs(segments) do
|
||||
if type(result) == "table" then
|
||||
---@type table
|
||||
-- TODO: figure out the actual type of tbl
|
||||
result = result[segment]
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function M.set_jump()
|
||||
vim.cmd "normal! m'"
|
||||
end
|
||||
|
||||
-- Filters a list based on the given predicate
|
||||
---@param tbl any[] The list to filter
|
||||
---@param predicate fun(v:any, i:number):boolean The predicate to filter with
|
||||
function M.filter(tbl, predicate)
|
||||
local result = {}
|
||||
|
||||
for i, v in ipairs(tbl) do
|
||||
if predicate(v, i) then
|
||||
table.insert(result, v)
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- Returns a list of all values from the first list
|
||||
-- that are not present in the second list.
|
||||
---@param tbl1 any[] The first table
|
||||
---@param tbl2 any[] The second table
|
||||
---@return table
|
||||
function M.difference(tbl1, tbl2)
|
||||
return M.filter(tbl1, function(v)
|
||||
return not vim.tbl_contains(tbl2, v)
|
||||
end)
|
||||
end
|
||||
|
||||
function M.identity(a)
|
||||
return a
|
||||
end
|
||||
|
||||
-- Returns a function returning the given value
|
||||
---@param a any
|
||||
---@return fun():any
|
||||
function M.constant(a)
|
||||
return function()
|
||||
return a
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns a function that returns the given value if it is a function,
|
||||
-- otherwise returns a function that returns the given value.
|
||||
---@param a any
|
||||
---@return fun(...):any
|
||||
function M.to_func(a)
|
||||
return type(a) == "function" and a or M.constant(a)
|
||||
end
|
||||
|
||||
---@return string|nil
|
||||
function M.ts_cli_version()
|
||||
if fn.executable "tree-sitter" == 1 then
|
||||
local handle = io.popen "tree-sitter -V"
|
||||
if not handle then
|
||||
return
|
||||
end
|
||||
local result = handle:read "*a"
|
||||
handle:close()
|
||||
return vim.split(result, "\n")[1]:match "[^tree%psitter ].*"
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user