aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua')
-rw-r--r--runtime/lua/provider/health.lua730
-rw-r--r--runtime/lua/vim/F.lua31
-rw-r--r--runtime/lua/vim/_editor.lua1
-rw-r--r--runtime/lua/vim/_init_packages.lua1
-rw-r--r--runtime/lua/vim/_inspector.lua22
-rw-r--r--runtime/lua/vim/_meta.lua20
-rw-r--r--runtime/lua/vim/filetype/detect.lua10
-rw-r--r--runtime/lua/vim/fs.lua2
-rw-r--r--runtime/lua/vim/loader.lua4
-rw-r--r--runtime/lua/vim/lsp.lua17
-rw-r--r--runtime/lua/vim/shared.lua2
-rw-r--r--runtime/lua/vim/treesitter.lua234
-rw-r--r--runtime/lua/vim/treesitter/_fold.lua2
-rw-r--r--runtime/lua/vim/treesitter/highlighter.lua21
-rw-r--r--runtime/lua/vim/treesitter/language.lua4
-rw-r--r--runtime/lua/vim/treesitter/languagetree.lua114
-rw-r--r--runtime/lua/vim/treesitter/playground.lua225
-rw-r--r--runtime/lua/vim/treesitter/query.lua8
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)