diff options
Diffstat (limited to 'runtime/lua')
-rw-r--r-- | runtime/lua/provider/health.lua | 730 | ||||
-rw-r--r-- | runtime/lua/vim/F.lua | 31 | ||||
-rw-r--r-- | runtime/lua/vim/_editor.lua | 1 | ||||
-rw-r--r-- | runtime/lua/vim/_init_packages.lua | 1 | ||||
-rw-r--r-- | runtime/lua/vim/_inspector.lua | 22 | ||||
-rw-r--r-- | runtime/lua/vim/_meta.lua | 20 | ||||
-rw-r--r-- | runtime/lua/vim/filetype/detect.lua | 10 | ||||
-rw-r--r-- | runtime/lua/vim/fs.lua | 2 | ||||
-rw-r--r-- | runtime/lua/vim/loader.lua | 4 | ||||
-rw-r--r-- | runtime/lua/vim/lsp.lua | 17 | ||||
-rw-r--r-- | runtime/lua/vim/shared.lua | 2 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter.lua | 234 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/_fold.lua | 2 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/highlighter.lua | 21 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/language.lua | 4 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/languagetree.lua | 114 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/playground.lua | 225 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/query.lua | 8 |
18 files changed, 1099 insertions, 349 deletions
diff --git a/runtime/lua/provider/health.lua b/runtime/lua/provider/health.lua new file mode 100644 index 0000000000..c1e4b8c23a --- /dev/null +++ b/runtime/lua/provider/health.lua @@ -0,0 +1,730 @@ +local M = {} + +local start = vim.health.report_start +local ok = vim.health.report_ok +local info = vim.health.report_info +local warn = vim.health.report_warn +local error = vim.health.report_error +local iswin = vim.loop.os_uname().sysname == 'Windows_NT' + +local shell_error_code = 0 +local function shell_error() + return shell_error_code ~= 0 +end + +-- Returns true if `cmd` exits with success, else false. +local function cmd_ok(cmd) + vim.fn.system(cmd) + return vim.v.shell_error == 0 +end + +local function executable(exe) + return vim.fn.executable(exe) == 1 +end + +local function is_blank(s) + return s:find('^%s*$') ~= nil +end + +local function isdir(path) + if not path then + return false + end + local stat = vim.loop.fs_stat(path) + if not stat then + return false + end + return stat.type == 'directory' +end + +local function isfile(path) + if not path then + return false + end + local stat = vim.loop.fs_stat(path) + if not stat then + return false + end + return stat.type == 'file' +end + +-- Handler for s:system() function. +local function system_handler(self, _, data, event) + if event == 'stderr' then + if self.add_stderr_to_output then + self.output = self.output .. vim.fn.join(data, '') + else + self.stderr = self.stderr .. vim.fn.join(data, '') + end + elseif event == 'stdout' then + self.output = self.output .. vim.fn.join(data, '') + elseif event == 'exit' then + shell_error_code = data + end +end + +-- Attempts to construct a shell command from an args list. +-- Only for display, to help users debug a failed command. +local function shellify(cmd) + if type(cmd) ~= 'table' then + return cmd + end + return vim.fn.join( + vim.fn.map(vim.fn.copy(cmd), [[v:val =~# ''\m[^\-.a-zA-Z_/]'' ? shellescape(v:val) : v:val]]), + ' ' + ) +end + +-- Run a system command and timeout after 30 seconds. +local function system(cmd, ...) + local args = { ... } + local args_count = vim.tbl_count(args) + + local stdin = (args_count > 0 and args[1] or '') + local stderr = (args_count > 1 and args[2] or false) + local ignore_error = (args_count > 2 and args[3] or false) + + local opts = { + add_stderr_to_output = stderr, + output = '', + stderr = '', + on_stdout = system_handler, + on_stderr = system_handler, + on_exit = system_handler, + } + local jobid = vim.fn.jobstart(cmd, opts) + + if jobid < 1 then + local message = 'Command error (job=' + .. jobid + .. '): `' + .. shellify(cmd) + .. '` (in ' + .. vim.fn.string(vim.fn.getcwd()) + .. ')' + + error(message) + shell_error_code = 1 + return opts.output + end + + if not is_blank(stdin) then + vim.cmd([[call jobsend(jobid, stdin)]]) + end + + local res = vim.fn.jobwait({ jobid }, 30000) + if res[1] == -1 then + error('Command timed out: ' .. shellify(cmd)) + vim.cmd([[call jobstop(jobid)]]) + elseif shell_error() and not ignore_error then + local emsg = 'Command error (job=' + .. jobid + .. ', exit code ' + .. shell_error_code + .. '): `' + .. shellify(cmd) + .. '` (in ' + .. vim.fn.string(vim.fn.getcwd()) + .. ')' + if not is_blank(opts.output) then + emsg = emsg .. '\noutput: ' .. opts.output + end + if not is_blank(opts.stderr) then + emsg = emsg .. '\nstderr: ' .. opts.stderr + end + error(emsg) + end + + -- return opts.output + local _ = ... + return vim.trim(vim.fn.system(cmd)) +end + +local function clipboard() + start('Clipboard (optional)') + + if + os.getenv('TMUX') + and executable('tmux') + and executable('pbpaste') + and not cmd_ok('pbpaste') + then + local tmux_version = string.match(vim.fn.system('tmux -V'), '%d+%.%d+') + local advice = { + 'Install tmux 2.6+. https://superuser.com/q/231130', + 'or use tmux with reattach-to-user-namespace. https://superuser.com/a/413233', + } + error('pbcopy does not work with tmux version: ' .. tmux_version, advice) + end + + local clipboard_tool = vim.fn['provider#clipboard#Executable']() + if vim.g.clipboard and is_blank(clipboard_tool) then + local error_message = vim.fn['provider#clipboard#Error']() + error( + error_message, + "Use the example in :help g:clipboard as a template, or don't set g:clipboard at all." + ) + elseif is_blank(clipboard_tool) then + warn( + 'No clipboard tool found. Clipboard registers (`"+` and `"*`) will not work.', + ':help clipboard' + ) + else + ok('Clipboard tool found: ' .. clipboard_tool) + end +end + +local function disabled_via_loaded_var(provider) + local loaded_var = 'loaded_' .. provider .. '_provider' + local v = vim.g[loaded_var] + if v == 0 then + info('Disabled (' .. loaded_var .. '=' .. v .. ').') + return true + end + return false +end + +-- Check if pyenv is available and a valid pyenv root can be found, then return +-- their respective paths. If either of those is invalid, return two empty +-- strings, effectively ignoring pyenv. +local function check_for_pyenv() + local pyenv_path = vim.fn.resolve(vim.fn.exepath('pyenv')) + + if is_blank(pyenv_path) then + return { '', '' } + end + + info('pyenv: Path: ' .. pyenv_path) + + local pyenv_root = os.getenv('PYENV_ROOT') and vim.fn.resolve('$PYENV_ROOT') or '' + + if is_blank(pyenv_root) then + pyenv_root = vim.trim(system({ pyenv_path, 'root' })) + info('pyenv: $PYENV_ROOT is not set. Infer from `pyenv root`.') + end + + if not isdir(pyenv_root) then + local message = 'pyenv: Root does not exist: ' + .. pyenv_root + .. '. Ignoring pyenv for all following checks.' + warn(message) + return { '', '' } + end + + info('pyenv: Root: ' .. pyenv_root) + + return { pyenv_path, pyenv_root } +end + +-- Check the Python interpreter's usability. +local function check_bin(bin) + if not isfile(bin) and (not iswin or not isfile(bin .. '.exe')) then + error('"' .. bin .. '" was not found.') + return false + elseif not executable(bin) then + error('"' .. bin .. '" is not executable.') + return false + end + return true +end + +-- Fetch the contents of a URL. +local function download(url) + local has_curl = executable('curl') + if has_curl and vim.fn.system({ 'curl', '-V' }):find('Protocols:.*https') then + local rv = system({ 'curl', '-sL', url }, '', 1, 1) + if shell_error() then + return 'curl error with ' .. url .. ': ' .. shell_error_code + else + return rv + end + elseif executable('python') then + local script = "try:\n\ + from urllib.request import urlopen\n\ + except ImportError:\n\ + from urllib2 import urlopen\n\ + response = urlopen('" .. url .. "')\n\ + print(response.read().decode('utf8'))\n" + local rv = system({ 'python', '-c', script }) + if is_blank(rv) and shell_error() then + return 'python urllib.request error: ' .. shell_error_code + else + return rv + end + end + + local message = 'missing `curl` ' + + if has_curl then + message = message .. '(with HTTPS support) ' + end + message = message .. 'and `python`, cannot make web request' + + return message +end + +-- Get the latest Nvim Python client (pynvim) version from PyPI. +local function latest_pypi_version() + local pypi_version = 'unable to get pypi response' + local pypi_response = download('https://pypi.python.org/pypi/pynvim/json') + if not is_blank(pypi_response) then + local pcall_ok, output = pcall(vim.fn.json_decode, pypi_response) + local pypi_data + if pcall_ok then + pypi_data = output + else + return 'error: ' .. pypi_response + end + + local pypi_element = pypi_data['info'] or {} + pypi_version = pypi_element['version'] or 'unable to parse' + end + return pypi_version +end + +local function is_bad_response(s) + local lower = s:lower() + return vim.startswith(lower, 'unable') + or vim.startswith(lower, 'error') + or vim.startswith(lower, 'outdated') +end + +-- Get version information using the specified interpreter. The interpreter is +-- used directly in case breaking changes were introduced since the last time +-- Nvim's Python client was updated. +-- +-- Returns: { +-- {python executable version}, +-- {current nvim version}, +-- {current pypi nvim status}, +-- {installed version status} +-- } +local function version_info(python) + local pypi_version = latest_pypi_version() + + local python_version = vim.trim(system({ + python, + '-c', + 'import sys; print(".".join(str(x) for x in sys.version_info[:3]))', + })) + + if is_blank(python_version) then + python_version = 'unable to parse ' .. python .. ' response' + end + + local nvim_path = vim.trim(system({ + python, + '-c', + 'import sys; sys.path = [p for p in sys.path if p != ""]; import neovim; print(neovim.__file__)', + })) + if shell_error() or is_blank(nvim_path) then + return { python_version, 'unable to load neovim Python module', pypi_version, nvim_path } + end + + -- Assuming that multiple versions of a package are installed, sort them + -- numerically in descending order. + local function compare(metapath1, metapath2) + local a = vim.fn.matchstr(vim.fn.fnamemodify(metapath1, ':p:h:t'), [[[0-9.]\+]]) + local b = vim.fn.matchstr(vim.fn.fnamemodify(metapath2, ':p:h:t'), [[[0-9.]\+]]) + if a == b then + return 0 + elseif a > b then + return 1 + else + return -1 + end + end + + -- Try to get neovim.VERSION (added in 0.1.11dev). + local nvim_version = system({ + python, + '-c', + 'from neovim import VERSION as v; print("{}.{}.{}{}".format(v.major, v.minor, v.patch, v.prerelease))', + }, '', 1, 1) + if is_blank(nvim_version) then + nvim_version = 'unable to find pynvim module version' + local base = vim.fs.basename(nvim_path, ':h') + local metas = vim.fn.glob(base .. '-*/METADATA', 1, 1) + vim.list_extend(metas, vim.fn.glob(base .. '-*/PKG-INFO', 1, 1)) + vim.list_extend(metas, vim.fn.glob(base .. '.egg-info/PKG-INFO', 1, 1)) + metas = table.sort(metas, compare) + + if metas and next(metas) ~= nil then + for _, meta_line in ipairs(vim.fn.readfile(metas[1])) do + if vim.startswith(meta_line, 'Version:') then + nvim_version = vim.fn.matchstr(meta_line, [[^Version: \zs\S\+]]) + break + end + end + end + end + + local nvim_path_base = vim.fn.fnamemodify(nvim_path, [[:~:h]]) + local version_status = 'unknown; ' .. nvim_path_base + if is_bad_response(nvim_version) and is_bad_response(pypi_version) then + if vim.version.lt(nvim_version, pypi_version) then + version_status = 'outdated; from ' .. nvim_path_base + else + version_status = 'up to date' + end + end + + return { python_version, nvim_version, pypi_version, version_status } +end + +local function python() + start('Python 3 provider (optional)') + + local pyname = 'python3' + local python_exe = '' + local virtual_env = os.getenv('VIRTUAL_ENV') + local venv = virtual_env and vim.fn.resolve(virtual_env) or '' + local host_prog_var = pyname .. '_host_prog' + local python_multiple = {} + + if disabled_via_loaded_var(pyname) then + return + end + + local pyenv_table = check_for_pyenv() + local pyenv = pyenv_table[1] + local pyenv_root = pyenv_table[2] + + if vim.g['host_prog_var'] then + local message = 'Using: g:' .. host_prog_var .. ' = "' .. vim.g['host_prog_var'] .. '"' + info(message) + end + + local python_table = vim.fn['provider#pythonx#Detect'](3) + pyname = python_table[1] + local pythonx_warnings = python_table[2] + + if is_blank(pyname) then + warn( + 'No Python executable found that can `import neovim`. ' + .. 'Using the first available executable for diagnostics.' + ) + elseif vim.g['host_prog_var'] then + python_exe = pyname + end + + -- No Python executable could `import neovim`, or host_prog_var was used. + if not is_blank(pythonx_warnings) then + warn(pythonx_warnings, { + 'See :help provider-python for more information.', + 'You may disable this provider (and warning) by adding `let g:loaded_python3_provider = 0` to your init.vim', + }) + elseif not is_blank(pyname) and is_blank(python_exe) then + if not vim.g['host_prog_var'] then + local message = '`g:' + .. host_prog_var + .. '` is not set. Searching for ' + .. pyname + .. ' in the environment.' + info(message) + end + + if not is_blank(pyenv) then + python_exe = vim.trim(system({ pyenv, 'which', pyname }, '', 1)) + if is_blank(python_exe) then + warn('pyenv could not find ' .. pyname .. '.') + end + end + + if is_blank(python_exe) then + python_exe = vim.fn.exepath(pyname) + + if os.getenv('PATH') then + local path_sep = iswin and ';' or ':' + local paths = vim.split(os.getenv('PATH') or '', path_sep) + + for _, path in ipairs(paths) do + local path_bin = vim.fs.normalize(path .. '/' .. pyname) + if + path_bin ~= vim.fs.normalize(python_exe) + and vim.tbl_contains(python_multiple, path_bin) + and executable(path_bin) + then + python_multiple[#python_multiple + 1] = path_bin + end + end + + if vim.tbl_count(python_multiple) > 0 then + -- This is worth noting since the user may install something + -- that changes $PATH, like homebrew. + local message = 'Multiple ' + .. pyname + .. ' executables found. ' + .. 'Set `g:' + .. host_prog_var + .. '` to avoid surprises.' + info(message) + end + + if python_exe:find('shims') then + local message = '`' .. python_exe .. '` appears to be a pyenv shim.' + local advice = '`pyenv` is not in $PATH, your pyenv installation is broken. Set `g:' + .. host_prog_var + .. '` to avoid surprises.' + + warn(message, advice) + end + end + end + end + + if not is_blank(python_exe) and not vim.g[host_prog_var] then + if + is_blank(venv) + and not is_blank(pyenv) + and not is_blank(pyenv_root) + and vim.startswith(vim.fn.resolve(python_exe), pyenv_root .. '/') + then + local advice = 'Create a virtualenv specifically for Nvim using pyenv, and set `g:' + .. host_prog_var + .. '`. This will avoid the need to install the pynvim module in each version/virtualenv.' + warn('pyenv is not set up optimally.', advice) + elseif not is_blank(venv) then + local venv_root + if not is_blank(pyenv_root) then + venv_root = pyenv_root + else + venv_root = vim.fs.dirname(venv) + end + + if vim.startswith(vim.fn.resolve(python_exe), venv_root .. '/') then + local advice = 'Create a virtualenv specifically for Nvim and use `g:' + .. host_prog_var + .. '`. This will avoid the need to install the pynvim module in each virtualenv.' + warn('Your virtualenv is not set up optimally.', advice) + end + end + end + + if is_blank(python_exe) and not is_blank(pyname) then + -- An error message should have already printed. + error('`' .. pyname .. '` was not found.') + elseif not is_blank(python_exe) and not check_bin(python_exe) then + python_exe = '' + end + + -- Diagnostic output + info('Executable: ' .. (is_blank(python_exe) and 'Not found' or python_exe)) + if vim.tbl_count(python_multiple) > 0 then + for _, path_bin in ipairs(python_multiple) do + info('Other python executable: ' .. path_bin) + end + end + + if is_blank(python_exe) then + -- No Python executable can import 'neovim'. Check if any Python executable + -- can import 'pynvim'. If so, that Python failed to import 'neovim' as + -- well, which is most probably due to a failed pip upgrade: + -- https://github.com/neovim/neovim/wiki/Following-HEAD#20181118 + local pynvim_table = vim.fn['provider#pythonx#DetectByModule']('pynvim', 3) + local pynvim_exe = pynvim_table[1] + if not is_blank(pynvim_exe) then + local message = 'Detected pip upgrade failure: Python executable can import "pynvim" but not "neovim": ' + .. pynvim_exe + local advice = { + 'Use that Python version to reinstall "pynvim" and optionally "neovim".', + pynvim_exe .. ' -m pip uninstall pynvim neovim', + pynvim_exe .. ' -m pip install pynvim', + pynvim_exe .. ' -m pip install neovim # only if needed by third-party software', + } + error(message, advice) + end + else + local version_info_table = version_info(python_exe) + local majorpyversion = version_info_table[1] + local current = version_info_table[2] + local latest = version_info_table[3] + local status = version_info_table[4] + + if vim.fn.str2nr(majorpyversion) ~= 3 then + warn('Unexpected Python version. This could lead to confusing error messages.') + end + + info('Python version: ' .. majorpyversion) + + if is_bad_response(status) then + info('pynvim version: ' .. current .. ' (' .. status .. ')') + else + info('pynvim version: ' .. current) + end + + if is_bad_response(current) then + error( + 'pynvim is not installed.\nError: ' .. current, + 'Run in shell: ' .. python_exe .. ' -m pip install pynvim' + ) + end + + if is_bad_response(latest) then + warn('Could not contact PyPI to get latest version.') + error('HTTP request failed: ' .. latest) + elseif is_bad_response(status) then + warn('Latest pynvim is NOT installed: ' .. latest) + elseif not is_bad_response(current) then + ok('Latest pynvim is installed.') + end + end +end + +-- Resolves Python executable path by invoking and checking `sys.executable`. +local function python_exepath(invocation) + return vim.fs.normalize( + system(vim.fn.fnameescape(invocation) .. ' -c "import sys; sys.stdout.write(sys.executable)"') + ) +end + +-- Checks that $VIRTUAL_ENV Python executables are found at front of $PATH in +-- Nvim and subshells. +local function virtualenv() + start('Python virtualenv') + if not os.getenv('VIRTUAL_ENV') then + ok('no $VIRTUAL_ENV') + return + end + local errors = {} + -- Keep hints as dict keys in order to discard duplicates. + local hints = {} + -- The virtualenv should contain some Python executables, and those + -- executables should be first both on Nvim's $PATH and the $PATH of + -- subshells launched from Nvim. + local bin_dir = (iswin and '/Scripts' or '/bin') + local venv_bins = vim.fn.glob(os.getenv('VIRTUAL_ENV') .. bin_dir .. '/python*', true, true) + -- XXX: Remove irrelevant executables found in bin/. + venv_bins = vim.fn.filter(venv_bins, 'v:val !~# "python-config"') + if vim.tbl_coun(venv_bins) > 0 then + for _, venv_bin in pairs(venv_bins) do + venv_bin = vim.fs.normalize(venv_bin) + local py_bin_basename = vim.fs.basename(venv_bin) + local nvim_py_bin = python_exepath(vim.fn.exepath(py_bin_basename)) + local subshell_py_bin = python_exepath(py_bin_basename) + if venv_bin ~= nvim_py_bin then + errors[#errors + 1] = '$PATH yields this ' + .. py_bin_basename + .. ' executable: ' + .. nvim_py_bin + local hint = '$PATH ambiguities arise if the virtualenv is not ' + .. 'properly activated prior to launching Nvim. Close Nvim, activate the virtualenv, ' + .. 'check that invoking Python from the command line launches the correct one, ' + .. 'then relaunch Nvim.' + hints[hint] = true + end + if venv_bin ~= subshell_py_bin then + errors[#errors + 1] = '$PATH in subshells yields this ' + .. py_bin_basename + .. ' executable: ' + .. subshell_py_bin + local hint = '$PATH ambiguities in subshells typically are ' + .. 'caused by your shell config overriding the $PATH previously set by the ' + .. 'virtualenv. Either prevent them from doing so, or use this workaround: ' + .. 'https://vi.stackexchange.com/a/34996' + hints[hint] = true + end + end + else + errors[#errors + 1] = 'no Python executables found in the virtualenv ' + .. bin_dir + .. ' directory.' + end + + local msg = '$VIRTUAL_ENV is set to: ' .. os.getenv('VIRTUAL_ENV') + if vim.tbl_count(errors) > 0 then + if vim.tbl_count(venv_bins) > 0 then + msg = msg + .. '\nAnd its ' + .. bin_dir + .. ' directory contains: ' + .. vim.fn.join(vim.fn.map(venv_bins, [[fnamemodify(v:val, ':t')]]), ', ') + end + local conj = '\nBut ' + for _, err in ipairs(errors) do + msg = msg .. conj .. err + conj = '\nAnd ' + end + msg = msg .. '\nSo invoking Python may lead to unexpected results.' + warn(msg, vim.fn.keys(hints)) + else + info(msg) + info( + 'Python version: ' + .. system('python -c "import platform, sys; sys.stdout.write(platform.python_version())"') + ) + ok('$VIRTUAL_ENV provides :!python.') + end +end + +local function ruby() + start('Ruby provider (optional)') + + if disabled_via_loaded_var('ruby') then + return + end + + if not executable('ruby') or not executable('gem') then + warn( + '`ruby` and `gem` must be in $PATH.', + 'Install Ruby and verify that `ruby` and `gem` commands work.' + ) + return + end + info('Ruby: ' .. system({ 'ruby', '-v' })) + + local ruby_detect_table = vim.fn['provider#ruby#Detect']() + local host = ruby_detect_table[1] + if is_blank(host) then + warn('`neovim-ruby-host` not found.', { + 'Run `gem install neovim` to ensure the neovim RubyGem is installed.', + 'Run `gem environment` to ensure the gem bin directory is in $PATH.', + 'If you are using rvm/rbenv/chruby, try "rehashing".', + 'See :help g:ruby_host_prog for non-standard gem installations.', + 'You may disable this provider (and warning) by adding `let g:loaded_ruby_provider = 0` to your init.vim', + }) + return + end + info('Host: ' .. host) + + local latest_gem_cmd = (iswin and 'cmd /c gem list -ra "^^neovim$"' or 'gem list -ra ^neovim$') + local latest_gem = system(vim.fn.split(latest_gem_cmd)) + if shell_error() or is_blank(latest_gem) then + error( + 'Failed to run: ' .. latest_gem_cmd, + { "Make sure you're connected to the internet.", 'Are you behind a firewall or proxy?' } + ) + return + end + local gem_split = vim.split(latest_gem, [[neovim (\|, \|)$]]) + latest_gem = gem_split[1] or 'not found' + + local current_gem_cmd = { host, '--version' } + local current_gem = system(current_gem_cmd) + if shell_error() then + error( + 'Failed to run: ' .. table.concat(current_gem_cmd, ' '), + { 'Report this issue with the output of: ', table.concat(current_gem_cmd, ' ') } + ) + return + end + + if vim.version.lt(current_gem, latest_gem) then + local message = 'Gem "neovim" is out-of-date. Installed: ' + .. current_gem + .. ', latest: ' + .. latest_gem + warn(message, 'Run in shell: gem update neovim') + else + ok('Latest "neovim" gem is installed: ' .. current_gem) + end +end + +function M.check() + clipboard() + python() + virtualenv() + ruby() +end + +return M diff --git a/runtime/lua/vim/F.lua b/runtime/lua/vim/F.lua index 3e370c0a84..16c834b371 100644 --- a/runtime/lua/vim/F.lua +++ b/runtime/lua/vim/F.lua @@ -1,18 +1,29 @@ local F = {} ---- Returns {a} if it is not nil, otherwise returns {b}. +--- Returns the first argument which is not nil. --- ----@generic A ----@generic B +--- If all arguments are nil, returns nil. --- ----@param a A ----@param b B ----@return A | B -function F.if_nil(a, b) - if a == nil then - return b +--- Examples: +--- <pre> +--- local a = nil +--- local b = nil +--- local c = 42 +--- local d = true +--- assert(vim.F.if_nil(a, b, c, d) == 42) +--- </pre> +--- +---@param ... any +---@return any +function F.if_nil(...) + local nargs = select('#', ...) + for i = 1, nargs do + local v = select(i, ...) + if v ~= nil then + return v + end end - return a + return nil end -- Use in combination with pcall diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua index 0c4b634f6f..fa0980563a 100644 --- a/runtime/lua/vim/_editor.lua +++ b/runtime/lua/vim/_editor.lua @@ -36,7 +36,6 @@ for k, v in pairs({ keymap = true, ui = true, health = true, - fs = true, secure = true, _watch = true, }) do diff --git a/runtime/lua/vim/_init_packages.lua b/runtime/lua/vim/_init_packages.lua index 57c0fc9122..2cf2e91a8c 100644 --- a/runtime/lua/vim/_init_packages.lua +++ b/runtime/lua/vim/_init_packages.lua @@ -54,6 +54,7 @@ require('vim.shared') vim._submodules = { inspect = true, version = true, + fs = true, } -- These are for loading runtime modules in the vim namespace lazily. diff --git a/runtime/lua/vim/_inspector.lua b/runtime/lua/vim/_inspector.lua index 92d380b08c..05983d3f0d 100644 --- a/runtime/lua/vim/_inspector.lua +++ b/runtime/lua/vim/_inspector.lua @@ -81,6 +81,12 @@ function vim.inspect_pos(bufnr, row, col, filter) end end + -- namespace id -> name map + local nsmap = {} + for name, id in pairs(vim.api.nvim_get_namespaces()) do + nsmap[id] = name + end + --- Convert an extmark tuple into a map-like table --- @private local function to_map(extmark) @@ -90,6 +96,8 @@ function vim.inspect_pos(bufnr, row, col, filter) col = extmark[3], opts = resolve_hl(extmark[4]), } + extmark.ns_id = extmark.opts.ns_id + extmark.ns = nsmap[extmark.ns_id] or '' extmark.end_row = extmark.opts.end_row or extmark.row -- inclusive extmark.end_col = extmark.opts.end_col or (extmark.col + 1) -- exclusive return extmark @@ -104,17 +112,9 @@ function vim.inspect_pos(bufnr, row, col, filter) end -- all extmarks at this position - local extmarks = {} - for ns, nsid in pairs(vim.api.nvim_get_namespaces()) do - local ns_marks = vim.api.nvim_buf_get_extmarks(bufnr, nsid, 0, -1, { details = true }) - ns_marks = vim.tbl_map(to_map, ns_marks) - ns_marks = vim.tbl_filter(is_here, ns_marks) - for _, mark in ipairs(ns_marks) do - mark.ns_id = nsid - mark.ns = ns - end - vim.list_extend(extmarks, ns_marks) - end + local extmarks = vim.api.nvim_buf_get_extmarks(bufnr, -1, 0, -1, { details = true }) + extmarks = vim.tbl_map(to_map, extmarks) + extmarks = vim.tbl_filter(is_here, extmarks) if filter.semantic_tokens then results.semantic_tokens = vim.tbl_filter(function(extmark) diff --git a/runtime/lua/vim/_meta.lua b/runtime/lua/vim/_meta.lua index 104f29c4c0..e3ad4d76c9 100644 --- a/runtime/lua/vim/_meta.lua +++ b/runtime/lua/vim/_meta.lua @@ -1,4 +1,4 @@ -local a = vim.api +local api = vim.api -- TODO(tjdevries): Improve option metadata so that this doesn't have to be hardcoded. -- Can be done in a separate PR. @@ -30,7 +30,7 @@ end local options_info = setmetatable({}, { __index = function(t, k) - local info = a.nvim_get_option_info(k) + local info = api.nvim_get_option_info(k) info.metatype = get_option_metatype(k, info) rawset(t, k, info) return rawget(t, k) @@ -74,12 +74,12 @@ local function new_opt_accessor(handle, scope) return new_opt_accessor(k, scope) end opt_validate(k, scope) - return a.nvim_get_option_value(k, { [scope] = handle or 0 }) + return api.nvim_get_option_value(k, { [scope] = handle or 0 }) end, __newindex = function(_, k, v) opt_validate(k, scope) - return a.nvim_set_option_value(k, v, { [scope] = handle or 0 }) + return api.nvim_set_option_value(k, v, { [scope] = handle or 0 }) end, }) end @@ -91,10 +91,10 @@ vim.wo = new_opt_accessor(nil, 'win') -- this ONLY sets the global option. like `setglobal` vim.go = setmetatable({}, { __index = function(_, k) - return a.nvim_get_option_value(k, { scope = 'global' }) + return api.nvim_get_option_value(k, { scope = 'global' }) end, __newindex = function(_, k, v) - return a.nvim_set_option_value(k, v, { scope = 'global' }) + return api.nvim_set_option_value(k, v, { scope = 'global' }) end, }) @@ -102,10 +102,10 @@ vim.go = setmetatable({}, { -- it has no additional metamethod magic. vim.o = setmetatable({}, { __index = function(_, k) - return a.nvim_get_option_value(k, {}) + return api.nvim_get_option_value(k, {}) end, __newindex = function(_, k, v) - return a.nvim_set_option_value(k, v, {}) + return api.nvim_set_option_value(k, v, {}) end, }) @@ -488,7 +488,7 @@ local function create_option_accessor(scope) -- opt[my_option] = value _set = function(self) local value = convert_value_to_vim(self._name, self._info, self._value) - a.nvim_set_option_value(self._name, value, { scope = scope }) + api.nvim_set_option_value(self._name, value, { scope = scope }) end, get = function(self) @@ -526,7 +526,7 @@ local function create_option_accessor(scope) return setmetatable({}, { __index = function(_, k) - return make_option(k, a.nvim_get_option_value(k, {})) + return make_option(k, api.nvim_get_option_value(k, {})) end, __newindex = function(_, k, v) diff --git a/runtime/lua/vim/filetype/detect.lua b/runtime/lua/vim/filetype/detect.lua index b3d9fedeae..d1eabadc4a 100644 --- a/runtime/lua/vim/filetype/detect.lua +++ b/runtime/lua/vim/filetype/detect.lua @@ -473,12 +473,12 @@ function M.fs(bufnr) if vim.g.filetype_fs then return vim.g.filetype_fs end - local line = nextnonblank(bufnr, 1) - if findany(line, { '^%s*%.?%( ', '^%s*\\G? ', '^\\$', '^%s*: %S' }) then - return 'forth' - else - return 'fsharp' + for _, line in ipairs(getlines(bufnr, 1, 100)) do + if line:find('^[:(\\] ') then + return 'forth' + end end + return 'fsharp' end function M.git(bufnr) diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua index 407b334f20..2a51bde263 100644 --- a/runtime/lua/vim/fs.lua +++ b/runtime/lua/vim/fs.lua @@ -216,7 +216,7 @@ function M.find(names, opts) ---@private local function add(match) - matches[#matches + 1] = match + matches[#matches + 1] = M.normalize(match) if #matches == limit then return true end diff --git a/runtime/lua/vim/loader.lua b/runtime/lua/vim/loader.lua index 201de18497..38b1e9fc0f 100644 --- a/runtime/lua/vim/loader.lua +++ b/runtime/lua/vim/loader.lua @@ -11,7 +11,7 @@ local M = {} ---@class ModuleFindOpts ---@field all? boolean Search for all matches (defaults to `false`) ---@field rtp? boolean Search for modname in the runtime path (defaults to `true`) ----@field patterns? string[] Paterns to use (defaults to `{"/init.lua", ".lua"}`) +---@field patterns? string[] Patterns to use (defaults to `{"/init.lua", ".lua"}`) ---@field paths? string[] Extra paths to search for modname ---@class ModuleInfo @@ -469,7 +469,7 @@ end ---@class ProfileOpts ---@field loaders? boolean Add profiling to the loaders ---- Debug function that wrapps all loaders and tracks stats +--- Debug function that wraps all loaders and tracks stats ---@private ---@param opts ProfileOpts? function M._profile(opts) diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 7e8c73ddb6..2d39f2d45d 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1101,21 +1101,16 @@ function lsp.start_client(config) return true end - local last_set_from = vim.fn.gettext('\n\tLast set from ') - local line = vim.fn.gettext(' line ') - local scriptname - - vim.api.nvim_buf_call(bufnr, function() - scriptname = vim.fn - .execute('verbose set ' .. option .. '?') - :match(last_set_from .. '(.*)' .. line .. '%d+') - end) + local info = vim.api.nvim_get_option_info2(option, { buf = bufnr }) + local scriptinfo = vim.tbl_filter(function(e) + return e.sid == info.last_set_sid + end, vim.fn.getscriptinfo()) - if not scriptname then + if #scriptinfo ~= 1 then return false end - return vim.startswith(vim.fn.expand(scriptname), vim.fn.expand('$VIMRUNTIME')) + return vim.startswith(scriptinfo[1].name, vim.fn.expand('$VIMRUNTIME')) end ---@private diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 9e337e93e8..eb734fb512 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -655,7 +655,7 @@ end --- vim.validate{arg1={{'foo'}, {'table', 'string'}}, arg2={'foo', {'table', 'string'}}} --- --> NOP (success) --- ---- vim.validate{arg1={1, {'string', table'}}} +--- vim.validate{arg1={1, {'string', 'table'}}} --- --> error('arg1: expected string|table, got number') --- --- </pre> diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index 2594c1672d..d1f5996768 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -1,4 +1,4 @@ -local a = vim.api +local api = vim.api local LanguageTree = require('vim.treesitter.languagetree') local Range = require('vim.treesitter._range') @@ -80,7 +80,7 @@ function M._create_parser(bufnr, lang, opts) local source = self:source() --[[@as integer]] - a.nvim_buf_attach( + api.nvim_buf_attach( source, false, { on_bytes = bytes_cb, on_detach = detach_cb, on_reload = reload_cb, preview = true } @@ -109,7 +109,7 @@ function M.get_parser(bufnr, lang, opts) opts = opts or {} if bufnr == nil or bufnr == 0 then - bufnr = a.nvim_get_current_buf() + bufnr = api.nvim_get_current_buf() end if not valid_lang(lang) then @@ -141,7 +141,7 @@ end ---@return boolean function M._has_parser(bufnr) if bufnr == nil or bufnr == 0 then - bufnr = a.nvim_get_current_buf() + bufnr = api.nvim_get_current_buf() end return parsers[bufnr] ~= nil end @@ -229,7 +229,7 @@ local function buf_range_get_text(buf, range) end_col = -1 end_row = end_row - 1 end - local lines = a.nvim_buf_get_text(buf, start_row, start_col, end_row, end_col, {}) + local lines = api.nvim_buf_get_text(buf, start_row, start_col, end_row, end_col, {}) return table.concat(lines, '\n') end @@ -294,7 +294,7 @@ end ---@return table[] List of captures `{ capture = "name", metadata = { ... } }` function M.get_captures_at_pos(bufnr, row, col) if bufnr == 0 then - bufnr = a.nvim_get_current_buf() + bufnr = api.nvim_get_current_buf() end local buf_highlighter = M.highlighter.active[bufnr] @@ -345,8 +345,8 @@ end ---@return string[] List of capture names function M.get_captures_at_cursor(winnr) winnr = winnr or 0 - local bufnr = a.nvim_win_get_buf(winnr) - local cursor = a.nvim_win_get_cursor(winnr) + local bufnr = api.nvim_win_get_buf(winnr) + local cursor = api.nvim_win_get_cursor(winnr) local data = M.get_captures_at_pos(bufnr, cursor[1] - 1, cursor[2]) @@ -374,7 +374,7 @@ function M.get_node(opts) local bufnr = opts.bufnr if not bufnr or bufnr == 0 then - bufnr = a.nvim_get_current_buf() + bufnr = api.nvim_get_current_buf() end local row, col @@ -383,10 +383,10 @@ function M.get_node(opts) row, col = opts.pos[1], opts.pos[2] else assert( - bufnr == a.nvim_get_current_buf(), + bufnr == api.nvim_get_current_buf(), 'Position must be explicitly provided when not using the current buffer' ) - local pos = a.nvim_win_get_cursor(0) + local pos = api.nvim_win_get_cursor(0) -- Subtract one to account for 1-based row indexing in nvim_win_get_cursor row, col = pos[1] - 1, pos[2] end @@ -417,7 +417,7 @@ end function M.get_node_at_pos(bufnr, row, col, opts) vim.deprecate('vim.treesitter.get_node_at_pos()', 'vim.treesitter.get_node()', '0.10') if bufnr == 0 then - bufnr = a.nvim_get_current_buf() + bufnr = api.nvim_get_current_buf() end local ts_range = { row, col, row, col } @@ -440,7 +440,7 @@ end function M.get_node_at_cursor(winnr) vim.deprecate('vim.treesitter.get_node_at_cursor()', 'vim.treesitter.get_node():type()', '0.10') winnr = winnr or 0 - local bufnr = a.nvim_win_get_buf(winnr) + local bufnr = api.nvim_win_get_buf(winnr) return M.get_node({ bufnr = bufnr, ignore_injections = false }):type() end @@ -465,7 +465,7 @@ end ---@param bufnr (integer|nil) Buffer to be highlighted (default: current buffer) ---@param lang (string|nil) Language of the parser (default: buffer filetype) function M.start(bufnr, lang) - bufnr = bufnr or a.nvim_get_current_buf() + bufnr = bufnr or api.nvim_get_current_buf() local parser = M.get_parser(bufnr, lang) M.highlighter.new(parser) end @@ -474,7 +474,7 @@ end --- ---@param bufnr (integer|nil) Buffer to stop highlighting (default: current buffer) function M.stop(bufnr) - bufnr = bufnr or a.nvim_get_current_buf() + bufnr = bufnr or api.nvim_get_current_buf() if M.highlighter.active[bufnr] then M.highlighter.active[bufnr]:destroy() @@ -502,208 +502,8 @@ end --- function, it accepts the buffer number of the source buffer as its only --- argument and should return a string. function M.inspect_tree(opts) - vim.validate({ - opts = { opts, 't', true }, - }) - - opts = opts or {} - - local Playground = require('vim.treesitter.playground') - local buf = a.nvim_get_current_buf() - local win = a.nvim_get_current_win() - local pg = assert(Playground:new(buf, opts.lang)) - - -- Close any existing playground window - if vim.b[buf].playground then - local w = vim.b[buf].playground - if a.nvim_win_is_valid(w) then - a.nvim_win_close(w, true) - end - end - - local w = opts.winid - if not w then - vim.cmd(opts.command or '60vnew') - w = a.nvim_get_current_win() - end - - local b = opts.bufnr - if b then - a.nvim_win_set_buf(w, b) - else - b = a.nvim_win_get_buf(w) - end - - vim.b[buf].playground = w - - vim.wo[w].scrolloff = 5 - vim.wo[w].wrap = false - vim.bo[b].buflisted = false - vim.bo[b].buftype = 'nofile' - vim.bo[b].bufhidden = 'wipe' - vim.bo[b].filetype = 'query' - - local title = opts.title - if not title then - local bufname = a.nvim_buf_get_name(buf) - title = string.format('Syntax tree for %s', vim.fn.fnamemodify(bufname, ':.')) - elseif type(title) == 'function' then - title = title(buf) - end - - assert(type(title) == 'string', 'Window title must be a string') - a.nvim_buf_set_name(b, title) - - pg:draw(b) - - a.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) - a.nvim_buf_set_keymap(b, 'n', '<CR>', '', { - desc = 'Jump to the node under the cursor in the source buffer', - callback = function() - local row = a.nvim_win_get_cursor(w)[1] - local pos = pg:get(row) - a.nvim_set_current_win(win) - a.nvim_win_set_cursor(win, { pos.lnum + 1, pos.col }) - end, - }) - a.nvim_buf_set_keymap(b, 'n', 'a', '', { - desc = 'Toggle anonymous nodes', - callback = function() - local row, col = unpack(a.nvim_win_get_cursor(w)) - local curnode = pg:get(row) - while curnode and not curnode.named do - row = row - 1 - curnode = pg:get(row) - end - - pg.opts.anon = not pg.opts.anon - pg:draw(b) - - if not curnode then - return - end - - local id = curnode.id - for i, node in pg:iter() do - if node.id == id then - a.nvim_win_set_cursor(w, { i, col }) - break - end - end - end, - }) - a.nvim_buf_set_keymap(b, 'n', 'I', '', { - desc = 'Toggle language display', - callback = function() - pg.opts.lang = not pg.opts.lang - pg:draw(b) - end, - }) - - local group = a.nvim_create_augroup('treesitter/playground', {}) - - a.nvim_create_autocmd('CursorMoved', { - group = group, - buffer = b, - callback = function() - a.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) - local row = a.nvim_win_get_cursor(w)[1] - local pos = pg:get(row) - a.nvim_buf_set_extmark(buf, pg.ns, pos.lnum, pos.col, { - end_row = pos.end_lnum, - end_col = math.max(0, pos.end_col), - hl_group = 'Visual', - }) - - local topline, botline = vim.fn.line('w0', win), vim.fn.line('w$', win) - - -- Move the cursor if highlighted range is completely out of view - if pos.lnum < topline and pos.end_lnum < topline then - a.nvim_win_set_cursor(win, { pos.end_lnum + 1, 0 }) - elseif pos.lnum > botline and pos.end_lnum > botline then - a.nvim_win_set_cursor(win, { pos.lnum + 1, 0 }) - end - end, - }) - - a.nvim_create_autocmd('CursorMoved', { - group = group, - buffer = buf, - callback = function() - if not a.nvim_buf_is_loaded(b) then - return true - end - - a.nvim_buf_clear_namespace(b, pg.ns, 0, -1) - - local cursor_node = M.get_node({ - bufnr = buf, - lang = opts.lang, - ignore_injections = false, - }) - if not cursor_node then - return - end - - local cursor_node_id = cursor_node:id() - for i, v in pg:iter() do - if v.id == cursor_node_id then - local start = v.depth - local end_col = start + #v.text - a.nvim_buf_set_extmark(b, pg.ns, i - 1, start, { - end_col = end_col, - hl_group = 'Visual', - }) - a.nvim_win_set_cursor(w, { i, 0 }) - break - end - end - end, - }) - - a.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, { - group = group, - buffer = buf, - callback = function() - if not a.nvim_buf_is_loaded(b) then - return true - end - - pg = assert(Playground:new(buf, opts.lang)) - pg:draw(b) - end, - }) - - a.nvim_create_autocmd('BufLeave', { - group = group, - buffer = b, - callback = function() - a.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) - end, - }) - - a.nvim_create_autocmd('BufLeave', { - group = group, - buffer = buf, - callback = function() - if not a.nvim_buf_is_loaded(b) then - return true - end - - a.nvim_buf_clear_namespace(b, pg.ns, 0, -1) - end, - }) - - a.nvim_create_autocmd('BufHidden', { - group = group, - buffer = buf, - once = true, - callback = function() - if a.nvim_win_is_valid(w) then - a.nvim_win_close(w, true) - end - end, - }) + ---@cast opts InspectTreeOpts + require('vim.treesitter.playground').inspect_tree(opts) end --- Returns the fold level for {lnum} in the current buffer. Can be set directly to 'foldexpr': diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index 6547ab936e..7df93d1b2e 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -68,7 +68,7 @@ function FoldInfo:add_stop(lnum) self.stop_counts[lnum] = (self.stop_counts[lnum] or 0) + 1 end ----@packag +---@package ---@param lnum integer ---@return integer function FoldInfo:get_start(lnum) diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index 729cd34090..ac2a929487 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -1,4 +1,4 @@ -local a = vim.api +local api = vim.api local query = vim.treesitter.query ---@alias TSHlIter fun(): integer, TSNode, TSMetadata @@ -25,7 +25,7 @@ TSHighlighter.active = TSHighlighter.active or {} local TSHighlighterQuery = {} TSHighlighterQuery.__index = TSHighlighterQuery -local ns = a.nvim_create_namespace('treesitter/highlighter') +local ns = api.nvim_create_namespace('treesitter/highlighter') ---@private function TSHighlighterQuery.new(lang, query_string) @@ -36,7 +36,7 @@ function TSHighlighterQuery.new(lang, query_string) local name = self._query.captures[capture] local id = 0 if not vim.startswith(name, '_') then - id = a.nvim_get_hl_id_by_name('@' .. name .. '.' .. lang) + id = api.nvim_get_hl_id_by_name('@' .. name .. '.' .. lang) end rawset(table, capture, id) @@ -121,7 +121,7 @@ function TSHighlighter.new(tree, opts) vim.cmd.runtime({ 'syntax/synload.vim', bang = true }) end - a.nvim_buf_call(self.bufnr, function() + api.nvim_buf_call(self.bufnr, function() vim.opt_local.spelloptions:append('noplainbuffer') end) @@ -140,7 +140,7 @@ function TSHighlighter:destroy() vim.bo[self.bufnr].spelloptions = self.orig_spelloptions vim.b[self.bufnr].ts_highlight = nil if vim.g.syntax_on == 1 then - a.nvim_exec_autocmds('FileType', { group = 'syntaxset', buffer = self.bufnr }) + api.nvim_exec_autocmds('FileType', { group = 'syntaxset', buffer = self.bufnr }) end end end @@ -168,7 +168,7 @@ end ---@param start_row integer ---@param new_end integer function TSHighlighter:on_bytes(_, _, start_row, _, _, _, _, _, new_end) - a.nvim__buf_redraw_range(self.bufnr, start_row, start_row + new_end + 1) + api.nvim__buf_redraw_range(self.bufnr, start_row, start_row + new_end + 1) end ---@package @@ -180,7 +180,7 @@ end ---@param changes integer[][]? function TSHighlighter:on_changedtree(changes) for _, ch in ipairs(changes or {}) do - a.nvim__buf_redraw_range(self.bufnr, ch[1], ch[3] + 1) + api.nvim__buf_redraw_range(self.bufnr, ch[1], ch[3] + 1) end end @@ -236,7 +236,8 @@ local function on_line_impl(self, buf, line, is_spell_nav) break end - local start_row, start_col, end_row, end_col = node:range() + local range = vim.treesitter.get_range(node, buf, metadata[capture]) + local start_row, start_col, _, end_row, end_col, _ = unpack(range) local hl = highlighter_query.hl_cache[capture] local capture_name = highlighter_query:query().captures[capture] @@ -251,7 +252,7 @@ local function on_line_impl(self, buf, line, is_spell_nav) local spell_pri_offset = capture_name == 'nospell' and 1 or 0 if hl and end_row >= line and (not is_spell_nav or spell ~= nil) then - a.nvim_buf_set_extmark(buf, ns, start_row, start_col, { + api.nvim_buf_set_extmark(buf, ns, start_row, start_col, { end_line = end_row, end_col = end_col, hl_group = hl, @@ -322,7 +323,7 @@ function TSHighlighter._on_win(_, _win, buf, _topline) return true end -a.nvim_set_decoration_provider(ns, { +api.nvim_set_decoration_provider(ns, { on_buf = TSHighlighter._on_buf, on_win = TSHighlighter._on_win, on_line = TSHighlighter._on_line, diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index 5b74bb6200..b616d4d70b 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -1,4 +1,4 @@ -local a = vim.api +local api = vim.api ---@class TSLanguageModule local M = {} @@ -89,7 +89,7 @@ function M.add(lang, opts) end local fname = 'parser/' .. lang .. '.*' - local paths = a.nvim_get_runtime_file(fname, false) + local paths = api.nvim_get_runtime_file(fname, false) if #paths == 0 then error("no parser for '" .. lang .. "' language, see :help treesitter-parsers") end diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 922e4881ca..4aa07d1b96 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -32,7 +32,7 @@ --- a plugin that does any kind of analysis on a tree should use a timer to throttle too frequent --- updates. -local a = vim.api +local api = vim.api local query = require('vim.treesitter.query') local language = require('vim.treesitter.language') local Range = require('vim.treesitter._range') @@ -57,7 +57,10 @@ local Range = require('vim.treesitter._range') ---@field private _injection_query Query Queries defining injected languages ---@field private _opts table Options ---@field private _parser TSParser Parser for language ----@field private _regions Range6[][] List of regions this tree should manage and parse +---@field private _has_regions boolean +---@field private _regions Range6[][]? +---List of regions this tree should manage and parse. If nil then regions are +---taken from _trees. This is mostly a short-lived cache for included_regions() ---@field private _lang string Language name ---@field private _source (integer|string) Buffer or string to parse ---@field private _trees TSTree[] Reference to parsed tree (one for each language) @@ -91,7 +94,6 @@ function LanguageTree.new(source, lang, opts) _source = source, _lang = lang, _children = {}, - _regions = {}, _trees = {}, _opts = opts, _injection_query = injections[lang] and query.parse(lang, injections[lang]) @@ -140,16 +142,16 @@ function LanguageTree:_log(...) local prefix = string.format('%s:%d: [%s:%d] ', info.name, info.currentline, self:lang(), nregions) - a.nvim_out_write(prefix) + api.nvim_out_write(prefix) for _, x in ipairs(args) do if type(x) == 'string' then - a.nvim_out_write(x) + api.nvim_out_write(x) else - a.nvim_out_write(vim.inspect(x, { newline = ' ', indent = '' })) + api.nvim_out_write(vim.inspect(x, { newline = ' ', indent = '' })) end - a.nvim_out_write(' ') + api.nvim_out_write(' ') end - a.nvim_out_write('\n') + api.nvim_out_write('\n') end --- Invalidates this parser and all its children @@ -237,27 +239,21 @@ function LanguageTree:parse() --- At least 1 region is invalid if not self:is_valid(true) then - local function _parsetree(index) - local parse_time, tree, tree_changes = - tcall(self._parser.parse, self._parser, self._trees[index], self._source) - - self:_do_callback('changedtree', tree_changes, tree) - self._trees[index] = tree - vim.list_extend(changes, tree_changes) - - total_parse_time = total_parse_time + parse_time - regions_parsed = regions_parsed + 1 - end - - if #self._regions > 0 then - for i, ranges in ipairs(self._regions) do - if not self._valid or not self._valid[i] then - self._parser:set_included_ranges(ranges) - _parsetree(i) - end + -- If there are no ranges, set to an empty list + -- so the included ranges in the parser are cleared. + for i, ranges in ipairs(self:included_regions()) do + if not self._valid or not self._valid[i] then + self._parser:set_included_ranges(ranges) + local parse_time, tree, tree_changes = + tcall(self._parser.parse, self._parser, self._trees[i], self._source) + + self:_do_callback('changedtree', tree_changes, tree) + self._trees[i] = tree + vim.list_extend(changes, tree_changes) + + total_parse_time = total_parse_time + parse_time + regions_parsed = regions_parsed + 1 end - else - _parsetree(1) end end @@ -403,7 +399,7 @@ function LanguageTree:_iter_regions(fn) local all_valid = true - for i, region in ipairs(self._regions) do + for i, region in ipairs(self:included_regions()) do if self._valid[i] == nil then self._valid[i] = true end @@ -445,6 +441,8 @@ end ---@private ---@param new_regions Range6[][] List of regions this tree should manage and parse. function LanguageTree:set_included_regions(new_regions) + self._has_regions = true + -- Transform the tables from 4 element long to 6 element long (with byte offset) for _, region in ipairs(new_regions) do for i, range in ipairs(region) do @@ -454,7 +452,7 @@ function LanguageTree:set_included_regions(new_regions) end end - if #self._regions ~= #new_regions then + if #self:included_regions() ~= #new_regions then self._trees = {} self:invalidate() else @@ -462,13 +460,29 @@ function LanguageTree:set_included_regions(new_regions) return vim.deep_equal(new_regions[i], region) end) end + self._regions = new_regions end ---Gets the set of included regions ---@return integer[][] function LanguageTree:included_regions() - return self._regions + if self._regions then + return self._regions + end + + if not self._has_regions or #self._trees == 0 then + -- treesitter.c will default empty ranges to { -1, -1, -1, -1, -1, -1} + return { {} } + end + + local regions = {} ---@type Range6[][] + for i, _ in ipairs(self._trees) do + regions[i] = self._trees[i]:included_ranges(true) + end + + self._regions = regions + return regions end ---@private @@ -721,6 +735,8 @@ function LanguageTree:_edit( ) end + self._regions = nil + local changed_range = { start_row, start_col, @@ -730,42 +746,16 @@ function LanguageTree:_edit( end_byte_old, } - local new_range = { - start_row, - start_col, - start_byte, - end_row_new, - end_col_new, - end_byte_new, - } - - if #self._regions == 0 then - self._valid = false - end - -- Validate regions after editing the tree self:_iter_regions(function(_, region) - for i, r in ipairs(region) do + if #region == 0 then + -- empty region, use the full source + return false + end + for _, r in ipairs(region) do if Range.intercepts(r, changed_range) then return false end - - -- Range after change. Adjust - if Range.cmp_pos.gt(r[1], r[2], changed_range[4], changed_range[5]) then - local byte_offset = new_range[6] - changed_range[6] - local row_offset = new_range[4] - changed_range[4] - - -- Update the range to avoid invalidation in set_included_regions() - -- which will compare the regions against the parsed injection regions - region[i] = { - r[1] + row_offset, - r[2], - r[3] + byte_offset, - r[4] + row_offset, - r[5], - r[6] + byte_offset, - } - end end return true end) diff --git a/runtime/lua/vim/treesitter/playground.lua b/runtime/lua/vim/treesitter/playground.lua index 35f06f5caf..2c0a0d1aa6 100644 --- a/runtime/lua/vim/treesitter/playground.lua +++ b/runtime/lua/vim/treesitter/playground.lua @@ -1,5 +1,8 @@ local api = vim.api +---@class TSPlaygroundModule +local M = {} + ---@class TSPlayground ---@field ns integer API namespace ---@field opts table Options table with the following keys: @@ -212,4 +215,224 @@ function TSPlayground:iter() return ipairs(self.opts.anon and self.nodes or self.named) end -return TSPlayground +--- @class InspectTreeOpts +--- @field lang string? The language of the source buffer. If omitted, the +--- filetype of the source buffer is used. +--- @field bufnr integer? Buffer to draw the tree into. If omitted, a new +--- buffer is created. +--- @field winid integer? Window id to display the tree buffer in. If omitted, +--- a new window is created with {command}. +--- @field command string? Vimscript command to create the window. Default +--- value is "60vnew". Only used when {winid} is nil. +--- @field title (string|fun(bufnr:integer):string|nil) Title of the window. If a +--- function, it accepts the buffer number of the source +--- buffer as its only argument and should return a string. + +--- @param opts InspectTreeOpts +function M.inspect_tree(opts) + vim.validate({ + opts = { opts, 't', true }, + }) + + opts = opts or {} + + local buf = api.nvim_get_current_buf() + local win = api.nvim_get_current_win() + local pg = assert(TSPlayground:new(buf, opts.lang)) + + -- Close any existing playground window + if vim.b[buf].playground then + local w = vim.b[buf].playground + if api.nvim_win_is_valid(w) then + api.nvim_win_close(w, true) + end + end + + local w = opts.winid + if not w then + vim.cmd(opts.command or '60vnew') + w = api.nvim_get_current_win() + end + + local b = opts.bufnr + if b then + api.nvim_win_set_buf(w, b) + else + b = api.nvim_win_get_buf(w) + end + + vim.b[buf].playground = w + + vim.wo[w].scrolloff = 5 + vim.wo[w].wrap = false + vim.wo[w].foldmethod = 'manual' -- disable folding + vim.bo[b].buflisted = false + vim.bo[b].buftype = 'nofile' + vim.bo[b].bufhidden = 'wipe' + vim.bo[b].filetype = 'query' + + local title --- @type string? + local opts_title = opts.title + if not opts_title then + local bufname = api.nvim_buf_get_name(buf) + title = string.format('Syntax tree for %s', vim.fn.fnamemodify(bufname, ':.')) + elseif type(opts_title) == 'function' then + title = opts_title(buf) + end + + assert(type(title) == 'string', 'Window title must be a string') + api.nvim_buf_set_name(b, title) + + pg:draw(b) + + api.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) + api.nvim_buf_set_keymap(b, 'n', '<CR>', '', { + desc = 'Jump to the node under the cursor in the source buffer', + callback = function() + local row = api.nvim_win_get_cursor(w)[1] + local pos = pg:get(row) + api.nvim_set_current_win(win) + api.nvim_win_set_cursor(win, { pos.lnum + 1, pos.col }) + end, + }) + api.nvim_buf_set_keymap(b, 'n', 'a', '', { + desc = 'Toggle anonymous nodes', + callback = function() + local row, col = unpack(api.nvim_win_get_cursor(w)) + local curnode = pg:get(row) + while curnode and not curnode.named do + row = row - 1 + curnode = pg:get(row) + end + + pg.opts.anon = not pg.opts.anon + pg:draw(b) + + if not curnode then + return + end + + local id = curnode.id + for i, node in pg:iter() do + if node.id == id then + api.nvim_win_set_cursor(w, { i, col }) + break + end + end + end, + }) + api.nvim_buf_set_keymap(b, 'n', 'I', '', { + desc = 'Toggle language display', + callback = function() + pg.opts.lang = not pg.opts.lang + pg:draw(b) + end, + }) + + local group = api.nvim_create_augroup('treesitter/playground', {}) + + api.nvim_create_autocmd('CursorMoved', { + group = group, + buffer = b, + callback = function() + api.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) + local row = api.nvim_win_get_cursor(w)[1] + local pos = pg:get(row) + api.nvim_buf_set_extmark(buf, pg.ns, pos.lnum, pos.col, { + end_row = pos.end_lnum, + end_col = math.max(0, pos.end_col), + hl_group = 'Visual', + }) + + local topline, botline = vim.fn.line('w0', win), vim.fn.line('w$', win) + + -- Move the cursor if highlighted range is completely out of view + if pos.lnum < topline and pos.end_lnum < topline then + api.nvim_win_set_cursor(win, { pos.end_lnum + 1, 0 }) + elseif pos.lnum > botline and pos.end_lnum > botline then + api.nvim_win_set_cursor(win, { pos.lnum + 1, 0 }) + end + end, + }) + + api.nvim_create_autocmd('CursorMoved', { + group = group, + buffer = buf, + callback = function() + if not api.nvim_buf_is_loaded(b) then + return true + end + + api.nvim_buf_clear_namespace(b, pg.ns, 0, -1) + + local cursor_node = vim.treesitter.get_node({ + bufnr = buf, + lang = opts.lang, + ignore_injections = false, + }) + if not cursor_node then + return + end + + local cursor_node_id = cursor_node:id() + for i, v in pg:iter() do + if v.id == cursor_node_id then + local start = v.depth + local end_col = start + #v.text + api.nvim_buf_set_extmark(b, pg.ns, i - 1, start, { + end_col = end_col, + hl_group = 'Visual', + }) + api.nvim_win_set_cursor(w, { i, 0 }) + break + end + end + end, + }) + + api.nvim_create_autocmd({ 'TextChanged', 'InsertLeave' }, { + group = group, + buffer = buf, + callback = function() + if not api.nvim_buf_is_loaded(b) then + return true + end + + pg = assert(TSPlayground:new(buf, opts.lang)) + pg:draw(b) + end, + }) + + api.nvim_create_autocmd('BufLeave', { + group = group, + buffer = b, + callback = function() + api.nvim_buf_clear_namespace(buf, pg.ns, 0, -1) + end, + }) + + api.nvim_create_autocmd('BufLeave', { + group = group, + buffer = buf, + callback = function() + if not api.nvim_buf_is_loaded(b) then + return true + end + + api.nvim_buf_clear_namespace(b, pg.ns, 0, -1) + end, + }) + + api.nvim_create_autocmd('BufHidden', { + group = group, + buffer = buf, + once = true, + callback = function() + if api.nvim_win_is_valid(w) then + api.nvim_win_close(w, true) + end + end, + }) +end + +return M diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 25623c1498..5b87e6ac31 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -1,4 +1,4 @@ -local a = vim.api +local api = vim.api local language = require('vim.treesitter.language') ---@class Query @@ -74,7 +74,7 @@ end ---@return string[] query_files List of files to load for given query and language function M.get_files(lang, query_name, is_included) local query_path = string.format('queries/%s/%s.scm', lang, query_name) - local lang_files = dedupe_files(a.nvim_get_runtime_file(query_path, true)) + local lang_files = dedupe_files(api.nvim_get_runtime_file(query_path, true)) if #lang_files == 0 then return {} @@ -635,7 +635,7 @@ end ---@return (fun(): integer, TSNode, TSMetadata): capture id, capture node, metadata function Query:iter_captures(node, source, start, stop) if type(source) == 'number' and source == 0 then - source = vim.api.nvim_get_current_buf() + source = api.nvim_get_current_buf() end start, stop = value_or_node_range(start, stop, node) @@ -690,7 +690,7 @@ end ---@return (fun(): integer, table<integer,TSNode>, table): pattern id, match, metadata function Query:iter_matches(node, source, start, stop) if type(source) == 'number' and source == 0 then - source = vim.api.nvim_get_current_buf() + source = api.nvim_get_current_buf() end start, stop = value_or_node_range(start, stop, node) |