Compare commits

...

18 Commits

Author SHA1 Message Date
Artem
c0c283968c
Merge fab5cbad00 into 7121983c45 2024-12-18 14:40:43 +00:00
Lewis Russell
7121983c45 refactor(man.lua): various changes
- Replace all uses of vim.regex with simpler Lua patterns.
- Replace all uses of vim.fn.substitute with string.gsub.
- Rework error handling so expected errors are passed back via a return.
  - These get routed up an passed to `vim.notify()`
  - Any other errors will cause a stack trace.
- Reworked the module initialization of `localfile_arg`
- Updated all type annotations.
- Refactored CLI completion by introduction a parse_cmdline()
  function.
- Simplified `show_toc()`
- Refactor highlighting
- Inline some functions
- Fix completion on MacOS 13 and earlier.
  - Prefer `manpath -q` over `man -w`
- Make completion more efficient by avoiding vim.fn.sort and vim.fn.uniq
  - Reimplement using a single loop
2024-12-18 14:40:36 +00:00
phanium
888a803755
fix(lsp): vim.lsp.start fails if existing client has no workspace_folders #31608
Problem:
regression since https://github.com/neovim/neovim/pull/31340

`nvim -l repro.lua`:
```lua
vim.lsp.start { cmd = { 'lua-language-server' }, name = 'lua_ls' }
vim.lsp.start { cmd = { 'lua-language-server' }, name = 'lua_ls', root_dir = 'foo' }

-- swapped case will be ok:
-- vim.lsp.start { cmd = { 'lua-language-server' }, name = 'lua_ls', root_dir = 'foo' }
-- vim.lsp.start { cmd = { 'lua-language-server' }, name = 'lua_ls' }
```

Failure:
```
E5113: Error while calling lua chunk: /…/lua/vim/lsp.lua:214: bad argument #1 to
'ipairs' (table expected, got nil)
stack traceback:
        [C]: in function 'ipairs'
        /…/lua/vim/lsp.lua:214: in function 'reuse_client'
        /…/lua/vim/lsp.lua:629: in function 'start'
        repro.lua:34: in main chunk
```
2024-12-18 06:37:12 -08:00
Peter Lithammer
07d5dc8938
feat(lsp): show server version in :checkhealth #31611
Problem:
Language server version information missing from `:checkhealth vim.lsp`.

Solution:
Store `InitializeResult.serverInfo.version` from the `initialize`
response and display for each client in `:checkhealth vim.lsp`.
2024-12-18 06:31:25 -08:00
Justin M. Keyes
f9eb68f340
fix(coverity): error handling CHECKED_RETURN #31618
CID 516406:  Error handling issues  (CHECKED_RETURN)
    /src/nvim/api/vimscript.c: 284 in nvim_call_dict_function()
    278       Object rv = OBJECT_INIT;
    279
    280       typval_T rettv;
    281       bool mustfree = false;
    282       switch (dict.type) {
    283       case kObjectTypeString:
    >>>     CID 516406:  Error handling issues  (CHECKED_RETURN)
    >>>     Calling "eval0" without checking return value (as is done elsewhere 10 out of 12 times).
    284         TRY_WRAP(err, {
    285           eval0(dict.data.string.data, &rettv, NULL, &EVALARG_EVALUATE);
    286           clear_evalarg(&EVALARG_EVALUATE, NULL);
    287         });
    288         if (ERROR_SET(err)) {
    289           return rv;
2024-12-18 06:05:37 -08:00
vanaigr
fab5cbad00 test: add on_range test 2024-12-14 23:01:16 -06:00
vanaigr
a61c1eee21 docs: add docs for on_range() 2024-12-14 23:01:16 -06:00
vanaigr
be6b7cdc5c feat: on_range bounds are always on the first byte 2024-12-14 23:01:16 -06:00
vanaigr
e007e977d2 docs: update news 2024-12-14 23:01:16 -06:00
vanaigr
7d2d0a5549 test: test iter_captures columns 2024-12-14 23:01:16 -06:00
vanaigr
1e42a0e748 docs: generate docs 2024-12-14 23:01:16 -06:00
vanaigr
fb58bc8da5 refactor: improve API 2024-12-14 23:01:16 -06:00
vanaigr
b730a4c625 fix: make range estimation more optimal 2024-12-14 23:01:16 -06:00
vanaigr
f94e98556e fix: invalidate line after calling decor providers 2024-12-14 23:01:16 -06:00
vanaigr
112a5cf679 feat: approximate decor provider range 2024-12-14 23:01:16 -06:00
vanaigr
d43690d82a fix: check columns in root bounds 2024-12-14 23:01:16 -06:00
vanaigr
260b4bb3a5 perf: add on_range in treesitter highlighting 2024-12-14 23:01:16 -06:00
vanaigr
d2ce9e21c8 test: add treesitter long lines benchmark 2024-12-14 23:01:16 -06:00
27 changed files with 1051 additions and 520 deletions

View File

@ -2823,11 +2823,12 @@ nvim_set_decoration_provider({ns_id}, {opts})
Note: this function should not be called often. Rather, the callbacks Note: this function should not be called often. Rather, the callbacks
themselves can be used to throttle unneeded callbacks. the `on_start` themselves can be used to throttle unneeded callbacks. the `on_start`
callback can return `false` to disable the provider until the next redraw. callback can return `false` to disable the provider until the next redraw.
Similarly, return `false` in `on_win` will skip the `on_line` calls for Similarly, return `false` in `on_win` will skip the `on_line` and
that window (but any extmarks set in `on_win` will still be used). A `on_range` calls for that window (but any extmarks set in `on_win` will
plugin managing multiple sources of decoration should ideally only set one still be used). A plugin managing multiple sources of decoration should
provider, and merge the sources internally. You can use multiple `ns_id` ideally only set one provider, and merge the sources internally. You can
for the extmarks set/modified inside the callback anyway. use multiple `ns_id` for the extmarks set/modified inside the callback
anyway.
Note: doing anything other than setting extmarks is considered Note: doing anything other than setting extmarks is considered
experimental. Doing things like changing options are not explicitly experimental. Doing things like changing options are not explicitly
@ -2835,8 +2836,8 @@ nvim_set_decoration_provider({ns_id}, {opts})
consumption). Doing `vim.rpcnotify` should be OK, but `vim.rpcrequest` is consumption). Doing `vim.rpcnotify` should be OK, but `vim.rpcrequest` is
quite dubious for the moment. quite dubious for the moment.
Note: It is not allowed to remove or update extmarks in `on_line` Note: It is not allowed to remove or update extmarks in `on_line` and
callbacks. `on_range` callbacks.
Attributes: ~ Attributes: ~
Lua |vim.api| only Lua |vim.api| only
@ -2857,6 +2858,14 @@ nvim_set_decoration_provider({ns_id}, {opts})
• on_line: called for each buffer line being redrawn. (The • on_line: called for each buffer line being redrawn. (The
interaction with fold lines is subject to change) > interaction with fold lines is subject to change) >
["line", winid, bufnr, row] ["line", winid, bufnr, row]
<
• on_range: called for each buffer range being redrawn. Range
is end-exclusive and may span multiple lines. Range bounds
point to the first byte of a character. An end position of
the form (lnum, 0), including (number of lines, 0), is
valid and indicates that EOL of the preceding line is
included. >
["range", winid, bufnr, begin_row, begin_col, end_row, end_col]
< <
• on_end: called at the end of a redraw cycle > • on_end: called at the end of a redraw cycle >
["end", tick] ["end", tick]

View File

@ -1126,6 +1126,9 @@ Lua module: vim.lsp.client *lsp-client*
• {server_capabilities} (`lsp.ServerCapabilities?`) Response from the • {server_capabilities} (`lsp.ServerCapabilities?`) Response from the
server sent on `initialize` describing the server sent on `initialize` describing the
server's capabilities. server's capabilities.
• {server_info} (`lsp.ServerInfo?`) Response from the server
sent on `initialize` describing information
about the server.
• {progress} (`vim.lsp.Client.Progress`) A ring buffer • {progress} (`vim.lsp.Client.Progress`) A ring buffer
(|vim.ringbuf()|) containing progress messages (|vim.ringbuf()|) containing progress messages
sent by the server. See sent by the server. See

View File

@ -115,6 +115,7 @@ LSP
• |vim.lsp.util.make_position_params()|, |vim.lsp.util.make_range_params()| • |vim.lsp.util.make_position_params()|, |vim.lsp.util.make_range_params()|
and |vim.lsp.util.make_given_range_params()| now require the `position_encoding` and |vim.lsp.util.make_given_range_params()| now require the `position_encoding`
parameter. parameter.
• `:checkhealth vim.lsp` displays the server version (if available).
LUA LUA
@ -177,6 +178,7 @@ API
• |nvim__ns_set()| can set properties for a namespace • |nvim__ns_set()| can set properties for a namespace
• |vim.json.encode()| has an option to enable forward slash escaping • |vim.json.encode()| has an option to enable forward slash escaping
• Decoration provider has `on_range()` callback.
DEFAULTS DEFAULTS
@ -302,6 +304,7 @@ TREESITTER
• |treesitter-directive-trim!| can trim all whitespace (not just empty lines) • |treesitter-directive-trim!| can trim all whitespace (not just empty lines)
from both sides of a node. from both sides of a node.
• |vim.treesitter.get_captures_at_pos()| now returns the `id` of each capture • |vim.treesitter.get_captures_at_pos()| now returns the `id` of each capture
• |Query:iter_captures()| supports specifying starting and ending columns.
TUI TUI

View File

@ -1348,7 +1348,7 @@ parse({lang}, {query}) *vim.treesitter.query.parse()*
• |vim.treesitter.query.get()| • |vim.treesitter.query.get()|
*Query:iter_captures()* *Query:iter_captures()*
Query:iter_captures({node}, {source}, {start}, {stop}) Query:iter_captures({node}, {source}, {start}, {stop}, {opts})
Iterate over all captures from all matches inside {node} Iterate over all captures from all matches inside {node}
{source} is needed if the query contains predicates; then the caller must {source} is needed if the query contains predicates; then the caller must
@ -1382,9 +1382,11 @@ Query:iter_captures({node}, {source}, {start}, {stop})
`node:start()`. `node:start()`.
• {stop} (`integer?`) Stopping line for the search (end-exclusive). • {stop} (`integer?`) Stopping line for the search (end-exclusive).
Defaults to `node:end_()`. Defaults to `node:end_()`.
• {opts} (`{ col_begin?: integer, col_end?: integer }?`) Optional
parameters.
Return: ~ Return: ~
(`fun(end_line: integer?): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch`) (`fun(end_line: integer?, end_col: integer?): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch`)
capture id, capture node, metadata, match capture id, capture node, metadata, match
*Query:iter_matches()* *Query:iter_matches()*

File diff suppressed because it is too large Load Diff

View File

@ -2114,8 +2114,8 @@ function vim.api.nvim_set_current_win(window) end
--- Note: this function should not be called often. Rather, the callbacks --- Note: this function should not be called often. Rather, the callbacks
--- themselves can be used to throttle unneeded callbacks. the `on_start` --- themselves can be used to throttle unneeded callbacks. the `on_start`
--- callback can return `false` to disable the provider until the next redraw. --- callback can return `false` to disable the provider until the next redraw.
--- Similarly, return `false` in `on_win` will skip the `on_line` calls --- Similarly, return `false` in `on_win` will skip the `on_line` and `on_range`
--- for that window (but any extmarks set in `on_win` will still be used). --- calls for that window (but any extmarks set in `on_win` will still be used).
--- A plugin managing multiple sources of decoration should ideally only set --- A plugin managing multiple sources of decoration should ideally only set
--- one provider, and merge the sources internally. You can use multiple `ns_id` --- one provider, and merge the sources internally. You can use multiple `ns_id`
--- for the extmarks set/modified inside the callback anyway. --- for the extmarks set/modified inside the callback anyway.
@ -2126,7 +2126,8 @@ function vim.api.nvim_set_current_win(window) end
--- Doing `vim.rpcnotify` should be OK, but `vim.rpcrequest` is quite dubious --- Doing `vim.rpcnotify` should be OK, but `vim.rpcrequest` is quite dubious
--- for the moment. --- for the moment.
--- ---
--- Note: It is not allowed to remove or update extmarks in `on_line` callbacks. --- Note: It is not allowed to remove or update extmarks
--- in `on_line` and `on_range` callbacks.
--- ---
--- @param ns_id integer Namespace id from `nvim_create_namespace()` --- @param ns_id integer Namespace id from `nvim_create_namespace()`
--- @param opts vim.api.keyset.set_decoration_provider Table of callbacks: --- @param opts vim.api.keyset.set_decoration_provider Table of callbacks:
@ -2148,6 +2149,14 @@ function vim.api.nvim_set_current_win(window) end
--- ``` --- ```
--- ["line", winid, bufnr, row] --- ["line", winid, bufnr, row]
--- ``` --- ```
--- - on_range: called for each buffer range being redrawn.
--- Range is end-exclusive and may span multiple lines. Range
--- bounds point to the first byte of a character. An end position
--- of the form (lnum, 0), including (number of lines, 0), is valid
--- and indicates that EOL of the preceding line is included.
--- ```
--- ["range", winid, bufnr, begin_row, begin_col, end_row, end_col]
--- ```
--- - on_end: called at the end of a redraw cycle --- - on_end: called at the end of a redraw cycle
--- ``` --- ```
--- ["end", tick] --- ["end", tick]

View File

@ -231,6 +231,7 @@ error('Cannot require a meta file')
--- @field on_buf? fun(_: "buf", bufnr: integer, tick: integer) --- @field on_buf? fun(_: "buf", bufnr: integer, tick: integer)
--- @field on_win? fun(_: "win", winid: integer, bufnr: integer, toprow: integer, botrow: integer) --- @field on_win? fun(_: "win", winid: integer, bufnr: integer, toprow: integer, botrow: integer)
--- @field on_line? fun(_: "line", winid: integer, bufnr: integer, row: integer) --- @field on_line? fun(_: "line", winid: integer, bufnr: integer, row: integer)
--- @field on_range? fun(_: "range", winid: integer, bufnr: integer, row_begin: integer, col_begin: integer, row_end: integer, col_end: integer)
--- @field on_end? fun(_: "end", tick: integer) --- @field on_end? fun(_: "end", tick: integer)
--- @field _on_hl_def? fun(_: "hl_def") --- @field _on_hl_def? fun(_: "hl_def")
--- @field _on_spell_nav? fun(_: "spell_nav") --- @field _on_spell_nav? fun(_: "spell_nav")

View File

@ -211,7 +211,7 @@ local function reuse_client_default(client, config)
for _, config_folder in ipairs(config_folders) do for _, config_folder in ipairs(config_folders) do
local found = false local found = false
for _, client_folder in ipairs(client.workspace_folders) do for _, client_folder in ipairs(client.workspace_folders or {}) do
if config_folder.uri == client_folder.uri then if config_folder.uri == client_folder.uri then
found = true found = true
break break

View File

@ -174,6 +174,10 @@ local validate = vim.validate
--- capabilities. --- capabilities.
--- @field server_capabilities lsp.ServerCapabilities? --- @field server_capabilities lsp.ServerCapabilities?
--- ---
--- Response from the server sent on `initialize` describing information about
--- the server.
--- @field server_info lsp.ServerInfo?
---
--- A ring buffer (|vim.ringbuf()|) containing progress messages --- A ring buffer (|vim.ringbuf()|) containing progress messages
--- sent by the server. --- sent by the server.
--- @field progress vim.lsp.Client.Progress --- @field progress vim.lsp.Client.Progress
@ -556,6 +560,8 @@ function Client:initialize()
self.offset_encoding = self.server_capabilities.positionEncoding self.offset_encoding = self.server_capabilities.positionEncoding
end end
self.server_info = result.serverInfo
if next(self.settings) then if next(self.settings) then
self:notify(ms.workspace_didChangeConfiguration, { settings = self.settings }) self:notify(ms.workspace_didChangeConfiguration, { settings = self.settings })
end end

View File

@ -40,6 +40,8 @@ local function check_active_clients()
local clients = vim.lsp.get_clients() local clients = vim.lsp.get_clients()
if next(clients) then if next(clients) then
for _, client in pairs(clients) do for _, client in pairs(clients) do
local server_version = vim.tbl_get(client, 'server_info', 'version')
or '? (no serverInfo.version response)'
local cmd ---@type string local cmd ---@type string
local ccmd = client.config.cmd local ccmd = client.config.cmd
if type(ccmd) == 'table' then if type(ccmd) == 'table' then
@ -62,6 +64,7 @@ local function check_active_clients()
end end
report_info(table.concat({ report_info(table.concat({
string.format('%s (id: %d)', client.name, client.id), string.format('%s (id: %d)', client.name, client.id),
string.format('- Version: %s', server_version),
dirs_info, dirs_info,
string.format('- Command: %s', cmd), string.format('- Command: %s', cmd),
string.format('- Settings: %s', vim.inspect(client.settings, { newline = '\n ' })), string.format('- Settings: %s', vim.inspect(client.settings, { newline = '\n ' })),

View File

@ -71,8 +71,6 @@ function TSQueryCursor:next_match() end
--- @param node TSNode --- @param node TSNode
--- @param query TSQuery --- @param query TSQuery
--- @param start integer? --- @param opts? { row_begin?: integer, col_begin?: integer, row_end?: integer, col_end?: integer, max_start_depth?: integer, match_limit?: integer }
--- @param stop integer?
--- @param opts? { max_start_depth?: integer, match_limit?: integer}
--- @return TSQueryCursor --- @return TSQueryCursor
function vim._create_ts_querycursor(node, query, start, stop, opts) end function vim._create_ts_querycursor(node, query, opts) end

View File

@ -1,10 +1,13 @@
local api = vim.api local api = vim.api
local query = vim.treesitter.query local query = vim.treesitter.query
local Range = require('vim.treesitter._range') local Range = require('vim.treesitter._range')
local cmp_eq = Range.cmp_pos.eq
local cmp_lt = Range.cmp_pos.lt
local cmp_lte = Range.cmp_pos.le
local ns = api.nvim_create_namespace('treesitter/highlighter') local ns = api.nvim_create_namespace('treesitter/highlighter')
---@alias vim.treesitter.highlighter.Iter fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch ---@alias vim.treesitter.highlighter.Iter fun(end_line: integer|nil, end_col: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch
---@class (private) vim.treesitter.highlighter.Query ---@class (private) vim.treesitter.highlighter.Query
---@field private _query vim.treesitter.Query? ---@field private _query vim.treesitter.Query?
@ -55,6 +58,7 @@ end
---@class (private) vim.treesitter.highlighter.State ---@class (private) vim.treesitter.highlighter.State
---@field tstree TSTree ---@field tstree TSTree
---@field next_row integer ---@field next_row integer
---@field next_col integer
---@field iter vim.treesitter.highlighter.Iter? ---@field iter vim.treesitter.highlighter.Iter?
---@field highlighter_query vim.treesitter.highlighter.Query ---@field highlighter_query vim.treesitter.highlighter.Query
@ -197,6 +201,7 @@ function TSHighlighter:prepare_highlight_states(srow, erow)
table.insert(self._highlight_states, { table.insert(self._highlight_states, {
tstree = tstree, tstree = tstree,
next_row = 0, next_row = 0,
next_col = 0,
iter = nil, iter = nil,
highlighter_query = highlighter_query, highlighter_query = highlighter_query,
}) })
@ -278,86 +283,119 @@ end
---@param self vim.treesitter.highlighter ---@param self vim.treesitter.highlighter
---@param buf integer ---@param buf integer
---@param line integer ---@param range_br integer
---@param range_bc integer
---@param range_er integer
---@param range_ec integer
---@param is_spell_nav boolean ---@param is_spell_nav boolean
local function on_line_impl(self, buf, line, is_spell_nav) local function on_range_impl(self, buf, range_br, range_bc, range_er, range_ec, is_spell_nav)
self:for_each_highlight_state(function(state) self:for_each_highlight_state(function(state)
local root_node = state.tstree:root() local root_node = state.tstree:root()
local root_start_row, _, root_end_row, _ = root_node:range() local root_br, root_bc, root_er, root_ec = root_node:range()
-- Only consider trees that contain this line --- @type boolean
if root_start_row > line or root_end_row < line then local root_intersects
local range_empty = cmp_eq(range_br, range_bc, range_er, range_ec)
local root_empty = cmp_eq(root_br, root_bc, root_er, root_ec)
if not range_empty and not root_empty then
root_intersects = cmp_lt(range_br, range_bc, root_er, root_ec)
and cmp_lt(root_br, root_bc, range_er, range_ec)
else
root_intersects = cmp_lte(range_br, range_bc, root_er, root_ec)
and cmp_lte(root_br, root_bc, range_er, range_ec)
end
-- Only consider trees within the visible range
if not root_intersects then
return return
end end
if state.iter == nil or state.next_row < line then local next_row = state.next_row
local next_col = state.next_col
if state.iter == nil or cmp_lt(next_row, next_col, range_br, range_bc) then
-- Mainly used to skip over folds -- Mainly used to skip over folds
-- TODO(lewis6991): Creating a new iterator loses the cached predicate results for query -- TODO(lewis6991): Creating a new iterator loses the cached predicate results for query
-- matches. Move this logic inside iter_captures() so we can maintain the cache. -- matches. Move this logic inside iter_captures() so we can maintain the cache.
state.iter = state.iter = state.highlighter_query:query():iter_captures(
state.highlighter_query:query():iter_captures(root_node, self.bufnr, line, root_end_row + 1) root_node,
self.bufnr,
range_br,
root_er,
{ col_begin = range_bc, col_end = root_ec }
)
end end
while line >= state.next_row do while cmp_lt(next_row, next_col, range_er, range_ec) do
local capture, node, metadata, match = state.iter(line) local capture, node, metadata, match = state.iter(range_er, range_ec)
if not node then
local range = { root_end_row + 1, 0, root_end_row + 1, 0 } next_row = math.huge
if node then next_col = math.huge
range = vim.treesitter.get_range(node, buf, metadata and metadata[capture]) break
end end
local range = vim.treesitter.get_range(node, buf, metadata and metadata[capture])
local start_row, start_col, end_row, end_col = Range.unpack4(range) local start_row, start_col, end_row, end_col = Range.unpack4(range)
if capture then if cmp_lt(next_row, next_col, start_row, start_col) then
local hl = state.highlighter_query:get_hl_from_capture(capture) next_row = start_row
next_col = start_col
local capture_name = state.highlighter_query:query().captures[capture]
local spell, spell_pri_offset = get_spell(capture_name)
-- The "priority" attribute can be set at the pattern level or on a particular capture
local priority = (
tonumber(metadata.priority or metadata[capture] and metadata[capture].priority)
or vim.hl.priorities.treesitter
) + spell_pri_offset
-- The "conceal" attribute can be set at the pattern level or on a particular capture
local conceal = metadata.conceal or metadata[capture] and metadata[capture].conceal
local url = get_url(match, buf, capture, metadata)
if hl and end_row >= line and (not is_spell_nav or spell ~= nil) then
api.nvim_buf_set_extmark(buf, ns, start_row, start_col, {
end_line = end_row,
end_col = end_col,
hl_group = hl,
ephemeral = true,
priority = priority,
conceal = conceal,
spell = spell,
url = url,
})
end
end end
if start_row > line then if not capture then
state.next_row = start_row break
end
local hl = state.highlighter_query:get_hl_from_capture(capture)
local capture_name = state.highlighter_query:query().captures[capture]
local spell, spell_pri_offset = get_spell(capture_name)
-- The "priority" attribute can be set at the pattern level or on a particular capture
local priority = (
tonumber(metadata.priority or metadata[capture] and metadata[capture].priority)
or vim.hl.priorities.treesitter
) + spell_pri_offset
-- The "conceal" attribute can be set at the pattern level or on a particular capture
local conceal = metadata.conceal or metadata[capture] and metadata[capture].conceal
local url = get_url(match, buf, capture, metadata)
if not is_spell_nav or spell ~= nil then
api.nvim_buf_set_extmark(buf, ns, start_row, start_col, {
end_line = end_row,
end_col = end_col,
ephemeral = true,
priority = priority,
hl_group = hl,
conceal = conceal,
spell = spell,
url = url,
})
end end
end end
state.next_row = next_row
state.next_col = next_col
end) end)
end end
---@private ---@private
---@param _win integer ---@param _win integer
---@param buf integer ---@param buf integer
---@param line integer ---@param br integer
function TSHighlighter._on_line(_, _win, buf, line, _) ---@param bc integer
---@param er integer
---@param ec integer
function TSHighlighter._on_range(_, _win, buf, br, bc, er, ec, _)
local self = TSHighlighter.active[buf] local self = TSHighlighter.active[buf]
if not self then if not self then
return return
end end
on_line_impl(self, buf, line, false) on_range_impl(self, buf, br, bc, er, ec, false)
end end
---@private ---@private
@ -375,9 +413,7 @@ function TSHighlighter._on_spell_nav(_, _, buf, srow, _, erow, _)
local highlight_states = self._highlight_states local highlight_states = self._highlight_states
self:prepare_highlight_states(srow, erow) self:prepare_highlight_states(srow, erow)
for row = srow, erow do on_range_impl(self, buf, srow, 0, erow, 0, true)
on_line_impl(self, buf, row, true)
end
self._highlight_states = highlight_states self._highlight_states = highlight_states
end end
@ -399,7 +435,7 @@ end
api.nvim_set_decoration_provider(ns, { api.nvim_set_decoration_provider(ns, {
on_win = TSHighlighter._on_win, on_win = TSHighlighter._on_win,
on_line = TSHighlighter._on_line, on_range = TSHighlighter._on_range,
_on_spell_nav = TSHighlighter._on_spell_nav, _on_spell_nav = TSHighlighter._on_spell_nav,
}) })

View File

@ -1,6 +1,7 @@
local api = vim.api local api = vim.api
local language = require('vim.treesitter.language') local language = require('vim.treesitter.language')
local memoize = vim.func._memoize local memoize = vim.func._memoize
local cmp_ge = require('vim.treesitter._range').cmp_pos.ge
local M = {} local M = {}
@ -875,24 +876,33 @@ end
---@param source (integer|string) Source buffer or string to extract text from ---@param source (integer|string) Source buffer or string to extract text from
---@param start? integer Starting line for the search. Defaults to `node:start()`. ---@param start? integer Starting line for the search. Defaults to `node:start()`.
---@param stop? integer Stopping line for the search (end-exclusive). Defaults to `node:end_()`. ---@param stop? integer Stopping line for the search (end-exclusive). Defaults to `node:end_()`.
---@param opts? { col_begin?: integer, col_end?: integer } Optional parameters.
--- ---
---@return (fun(end_line: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch): ---@return (fun(end_line: integer|nil, end_col: integer|nil): integer, TSNode, vim.treesitter.query.TSMetadata, TSQueryMatch):
--- capture id, capture node, metadata, match --- capture id, capture node, metadata, match
--- ---
---@note Captures are only returned if the query pattern of a specific capture contained predicates. ---@note Captures are only returned if the query pattern of a specific capture contained predicates.
function Query:iter_captures(node, source, start, stop) function Query:iter_captures(node, source, start, stop, opts)
if type(source) == 'number' and source == 0 then if type(source) == 'number' and source == 0 then
source = api.nvim_get_current_buf() source = api.nvim_get_current_buf()
end end
opts = opts or {}
start, stop = value_or_node_range(start, stop, node) start, stop = value_or_node_range(start, stop, node)
local cursor = vim._create_ts_querycursor(node, self.query, start, stop, { match_limit = 256 }) local cursor_opts = {
row_begin = start,
col_begin = opts.col_begin,
row_end = stop,
col_end = opts.col_end,
match_limit = 256,
}
local cursor = vim._create_ts_querycursor(node, self.query, cursor_opts)
local apply_directives = memoize(match_id_hash, self.apply_directives, true) local apply_directives = memoize(match_id_hash, self.apply_directives, true)
local match_preds = memoize(match_id_hash, self.match_preds, true) local match_preds = memoize(match_id_hash, self.match_preds, true)
local function iter(end_line) local function iter(end_line, end_col)
local capture, captured_node, match = cursor:next_capture() local capture, captured_node, match = cursor:next_capture()
if not capture then if not capture then
@ -902,10 +912,11 @@ function Query:iter_captures(node, source, start, stop)
if not match_preds(self, match, source) then if not match_preds(self, match, source) then
local match_id = match:info() local match_id = match:info()
cursor:remove_match(match_id) cursor:remove_match(match_id)
if end_line and captured_node:range() > end_line then local row, col = captured_node:range()
if end_line and cmp_ge(row, col, end_line, end_col or 0) then
return nil, captured_node, nil, nil return nil, captured_node, nil, nil
end end
return iter(end_line) -- tail call: try next match return iter(end_line, end_col) -- tail call: try next match
end end
local metadata = apply_directives(self, match, source) local metadata = apply_directives(self, match, source)
@ -961,8 +972,12 @@ function Query:iter_matches(node, source, start, stop, opts)
end end
start, stop = value_or_node_range(start, stop, node) start, stop = value_or_node_range(start, stop, node)
opts.row_begin = start
opts.col_begin = 0
opts.row_end = stop
opts.col_end = 0
local cursor = vim._create_ts_querycursor(node, self.query, start, stop, opts) local cursor = vim._create_ts_querycursor(node, self.query, opts)
local function iter() local function iter()
local match = cursor:next_match() local match = cursor:next_match()

View File

@ -8,9 +8,9 @@ vim.api.nvim_create_user_command('Man', function(params)
if params.bang then if params.bang then
man.init_pager() man.init_pager()
else else
local ok, err = pcall(man.open_page, params.count, params.smods, params.fargs) local err = man.open_page(params.count, params.smods, params.fargs)
if not ok then if err then
vim.notify(man.errormsg or err, vim.log.levels.ERROR) vim.notify('man.lua: ' .. err, vim.log.levels.ERROR)
end end
end end
end, { end, {
@ -31,6 +31,9 @@ vim.api.nvim_create_autocmd('BufReadCmd', {
pattern = 'man://*', pattern = 'man://*',
nested = true, nested = true,
callback = function(params) callback = function(params)
require('man').read_page(vim.fn.matchstr(params.match, 'man://\\zs.*')) local err = require('man').read_page(assert(params.match:match('man://(.*)')))
if err then
vim.notify('man.lua: ' .. err, vim.log.levels.ERROR)
end
end, end,
}) })

View File

@ -992,8 +992,8 @@ void nvim_buf_clear_namespace(Buffer buffer, Integer ns_id, Integer line_start,
/// Note: this function should not be called often. Rather, the callbacks /// Note: this function should not be called often. Rather, the callbacks
/// themselves can be used to throttle unneeded callbacks. the `on_start` /// themselves can be used to throttle unneeded callbacks. the `on_start`
/// callback can return `false` to disable the provider until the next redraw. /// callback can return `false` to disable the provider until the next redraw.
/// Similarly, return `false` in `on_win` will skip the `on_line` calls /// Similarly, return `false` in `on_win` will skip the `on_line` and `on_range`
/// for that window (but any extmarks set in `on_win` will still be used). /// calls for that window (but any extmarks set in `on_win` will still be used).
/// A plugin managing multiple sources of decoration should ideally only set /// A plugin managing multiple sources of decoration should ideally only set
/// one provider, and merge the sources internally. You can use multiple `ns_id` /// one provider, and merge the sources internally. You can use multiple `ns_id`
/// for the extmarks set/modified inside the callback anyway. /// for the extmarks set/modified inside the callback anyway.
@ -1004,7 +1004,8 @@ void nvim_buf_clear_namespace(Buffer buffer, Integer ns_id, Integer line_start,
/// Doing `vim.rpcnotify` should be OK, but `vim.rpcrequest` is quite dubious /// Doing `vim.rpcnotify` should be OK, but `vim.rpcrequest` is quite dubious
/// for the moment. /// for the moment.
/// ///
/// Note: It is not allowed to remove or update extmarks in `on_line` callbacks. /// Note: It is not allowed to remove or update extmarks
/// in `on_line` and `on_range` callbacks.
/// ///
/// @param ns_id Namespace id from |nvim_create_namespace()| /// @param ns_id Namespace id from |nvim_create_namespace()|
/// @param opts Table of callbacks: /// @param opts Table of callbacks:
@ -1026,6 +1027,14 @@ void nvim_buf_clear_namespace(Buffer buffer, Integer ns_id, Integer line_start,
/// ``` /// ```
/// ["line", winid, bufnr, row] /// ["line", winid, bufnr, row]
/// ``` /// ```
/// - on_range: called for each buffer range being redrawn.
/// Range is end-exclusive and may span multiple lines. Range
/// bounds point to the first byte of a character. An end position
/// of the form (lnum, 0), including (number of lines, 0), is valid
/// and indicates that EOL of the preceding line is included.
/// ```
/// ["range", winid, bufnr, begin_row, begin_col, end_row, end_col]
/// ```
/// - on_end: called at the end of a redraw cycle /// - on_end: called at the end of a redraw cycle
/// ``` /// ```
/// ["end", tick] /// ["end", tick]
@ -1049,6 +1058,7 @@ void nvim_set_decoration_provider(Integer ns_id, Dict(set_decoration_provider) *
{ "on_buf", &opts->on_buf, &p->redraw_buf }, { "on_buf", &opts->on_buf, &p->redraw_buf },
{ "on_win", &opts->on_win, &p->redraw_win }, { "on_win", &opts->on_win, &p->redraw_win },
{ "on_line", &opts->on_line, &p->redraw_line }, { "on_line", &opts->on_line, &p->redraw_line },
{ "on_range", &opts->on_range, &p->redraw_range },
{ "on_end", &opts->on_end, &p->redraw_end }, { "on_end", &opts->on_end, &p->redraw_end },
{ "_on_hl_def", &opts->_on_hl_def, &p->hl_def }, { "_on_hl_def", &opts->_on_hl_def, &p->hl_def },
{ "_on_spell_nav", &opts->_on_spell_nav, &p->spell_nav }, { "_on_spell_nav", &opts->_on_spell_nav, &p->spell_nav },

View File

@ -17,6 +17,8 @@ typedef struct {
LuaRefOf(("buf" _, Integer bufnr, Integer tick)) on_buf; LuaRefOf(("buf" _, Integer bufnr, Integer tick)) on_buf;
LuaRefOf(("win" _, Integer winid, Integer bufnr, Integer toprow, Integer botrow)) on_win; LuaRefOf(("win" _, Integer winid, Integer bufnr, Integer toprow, Integer botrow)) on_win;
LuaRefOf(("line" _, Integer winid, Integer bufnr, Integer row)) on_line; LuaRefOf(("line" _, Integer winid, Integer bufnr, Integer row)) on_line;
LuaRefOf(("range" _, Integer winid, Integer bufnr, Integer row_begin, Integer col_begin,
Integer row_end, Integer col_end)) on_range;
LuaRefOf(("end" _, Integer tick)) on_end; LuaRefOf(("end" _, Integer tick)) on_end;
LuaRefOf(("hl_def" _)) _on_hl_def; LuaRefOf(("hl_def" _)) _on_hl_def;
LuaRefOf(("spell_nav" _)) _on_spell_nav; LuaRefOf(("spell_nav" _)) _on_spell_nav;

View File

@ -229,10 +229,9 @@ static Object _call_function(String fn, Array args, dict_T *self, Arena *arena,
funcexe.fe_selfdict = self; funcexe.fe_selfdict = self;
TRY_WRAP(err, { TRY_WRAP(err, {
// call_func() retval is deceptive, ignore it. Instead we set `msg_list` // call_func() retval is deceptive, ignore it. Instead TRY_WRAP sets `msg_list` to capture
// (see above) to capture abort-causing non-exception errors. // abort-causing non-exception errors.
call_func(fn.data, (int)fn.size, &rettv, (int)args.size, (void)call_func(fn.data, (int)fn.size, &rettv, (int)args.size, vim_args, &funcexe);
vim_args, &funcexe);
}); });
if (!ERROR_SET(err)) { if (!ERROR_SET(err)) {
@ -280,18 +279,23 @@ Object nvim_call_dict_function(Object dict, String fn, Array args, Arena *arena,
typval_T rettv; typval_T rettv;
bool mustfree = false; bool mustfree = false;
switch (dict.type) { switch (dict.type) {
case kObjectTypeString: case kObjectTypeString: {
int eval_ret;
TRY_WRAP(err, { TRY_WRAP(err, {
eval0(dict.data.string.data, &rettv, NULL, &EVALARG_EVALUATE); eval_ret = eval0(dict.data.string.data, &rettv, NULL, &EVALARG_EVALUATE);
clear_evalarg(&EVALARG_EVALUATE, NULL); clear_evalarg(&EVALARG_EVALUATE, NULL);
}); });
if (ERROR_SET(err)) { if (ERROR_SET(err)) {
return rv; return rv;
} }
if (eval_ret != OK) {
abort(); // Should not happen.
}
// Evaluation of the string arg created a new dict or increased the // Evaluation of the string arg created a new dict or increased the
// refcount of a dict. Not necessary for a RPC dict. // refcount of a dict. Not necessary for a RPC dict.
mustfree = true; mustfree = true;
break; break;
}
case kObjectTypeDict: case kObjectTypeDict:
object_to_vim(dict, &rettv, err); object_to_vim(dict, &rettv, err);
break; break;

View File

@ -140,6 +140,7 @@ typedef struct {
LuaRef redraw_buf; LuaRef redraw_buf;
LuaRef redraw_win; LuaRef redraw_win;
LuaRef redraw_line; LuaRef redraw_line;
LuaRef redraw_range;
LuaRef redraw_end; LuaRef redraw_end;
LuaRef hl_def; LuaRef hl_def;
LuaRef spell_nav; LuaRef spell_nav;

View File

@ -29,7 +29,7 @@ static kvec_t(DecorProvider) decor_providers = KV_INITIAL_VALUE;
#define DECORATION_PROVIDER_INIT(ns_id) (DecorProvider) \ #define DECORATION_PROVIDER_INIT(ns_id) (DecorProvider) \
{ ns_id, kDecorProviderDisabled, LUA_NOREF, LUA_NOREF, \ { ns_id, kDecorProviderDisabled, LUA_NOREF, LUA_NOREF, \
LUA_NOREF, LUA_NOREF, LUA_NOREF, \ LUA_NOREF, LUA_NOREF, LUA_NOREF, LUA_NOREF, \
LUA_NOREF, -1, false, false, 0 } LUA_NOREF, -1, false, false, 0 }
static void decor_provider_error(DecorProvider *provider, const char *name, const char *msg) static void decor_provider_error(DecorProvider *provider, const char *name, const char *msg)
@ -178,6 +178,32 @@ void decor_providers_invoke_line(win_T *wp, int row, bool *has_decor)
decor_state.running_decor_provider = false; decor_state.running_decor_provider = false;
} }
void decor_providers_invoke_range(win_T *wp, int sr, int sc, int er, int ec, bool *has_decor)
{
decor_state.running_decor_provider = true;
for (size_t i = 0; i < kv_size(decor_providers); i++) {
DecorProvider *p = &kv_A(decor_providers, i);
if (p->state == kDecorProviderActive && p->redraw_range != LUA_NOREF) {
MAXSIZE_TEMP_ARRAY(args, 6);
ADD_C(args, WINDOW_OBJ(wp->handle));
ADD_C(args, BUFFER_OBJ(wp->w_buffer->handle));
ADD_C(args, INTEGER_OBJ(sr));
ADD_C(args, INTEGER_OBJ(sc));
ADD_C(args, INTEGER_OBJ(er));
ADD_C(args, INTEGER_OBJ(ec));
if (decor_provider_invoke((int)i, "range", p->redraw_range, args, true)) {
*has_decor = true;
} else {
// return 'false' or error: skip rest of this window
kv_A(decor_providers, i).state = kDecorProviderWinDisabled;
}
hl_check_ns();
}
}
decor_state.running_decor_provider = false;
}
/// For each provider invoke the 'buf' callback for a given buffer. /// For each provider invoke the 'buf' callback for a given buffer.
/// ///
/// @param buf Buffer /// @param buf Buffer
@ -262,6 +288,7 @@ void decor_provider_clear(DecorProvider *p)
NLUA_CLEAR_REF(p->redraw_buf); NLUA_CLEAR_REF(p->redraw_buf);
NLUA_CLEAR_REF(p->redraw_win); NLUA_CLEAR_REF(p->redraw_win);
NLUA_CLEAR_REF(p->redraw_line); NLUA_CLEAR_REF(p->redraw_line);
NLUA_CLEAR_REF(p->redraw_range);
NLUA_CLEAR_REF(p->redraw_end); NLUA_CLEAR_REF(p->redraw_end);
NLUA_CLEAR_REF(p->spell_nav); NLUA_CLEAR_REF(p->spell_nav);
p->state = kDecorProviderDisabled; p->state = kDecorProviderDisabled;

View File

@ -1029,6 +1029,9 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, s
buf_T *buf = wp->w_buffer; buf_T *buf = wp->w_buffer;
const bool end_fill = (lnum == buf->b_ml.ml_line_count + 1); const bool end_fill = (lnum == buf->b_ml.ml_line_count + 1);
int decor_provider_end_col;
bool check_decor_providers = false;
if (col_rows == 0) { if (col_rows == 0) {
// To speed up the loop below, set extra_check when there is linebreak, // To speed up the loop below, set extra_check when there is linebreak,
// trailing white space and/or syntax processing to be done. // trailing white space and/or syntax processing to be done.
@ -1054,11 +1057,7 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, s
has_decor = decor_redraw_line(wp, lnum - 1, &decor_state); has_decor = decor_redraw_line(wp, lnum - 1, &decor_state);
if (!end_fill) { if (!end_fill) {
decor_providers_invoke_line(wp, lnum - 1, &has_decor); check_decor_providers = true;
}
if (has_decor) {
extra_check = true;
} }
// Check for columns to display for 'colorcolumn'. // Check for columns to display for 'colorcolumn'.
@ -1468,6 +1467,38 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, s
} }
} }
if (check_decor_providers) {
int const col = (int)(ptr - line);
// Approximate the number of bytes that will be drawn.
// Assume we're dealing with 1-cell ascii and ignore
// the effects of 'linebreak', 'breakindent', etc.
int rem_vcols;
if (wp->w_p_wrap) {
int width = wp->w_width_inner - win_col_off(wp);
int width2 = width + win_col_off2(wp);
int first_row_width = start_col == 0 ? width : width2;
rem_vcols = first_row_width + (endrow - startrow - 1) * width2;
} else {
rem_vcols = wp->w_width_inner - win_col_off(wp);
}
// Call it here since we need to invalidate the line pointer anyway.
bool added_decor = false;
decor_providers_invoke_line(wp, lnum - 1, &added_decor);
has_decor |= added_decor;
decor_provider_end_col = invoke_range_next(wp, lnum, col, rem_vcols + 1, &has_decor);
line = ml_get_buf(wp->w_buffer, lnum);
ptr = line + col;
}
if (has_decor) {
extra_check = true;
}
// Correct highlighting for cursor that can't be disabled. // Correct highlighting for cursor that can't be disabled.
// Avoids having to check this for each character. // Avoids having to check this for each character.
if (wlv.fromcol >= 0) { if (wlv.fromcol >= 0) {
@ -1517,6 +1548,16 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, s
bool did_decrement_ptr = false; bool did_decrement_ptr = false;
if (check_decor_providers && (int)(ptr - line) >= decor_provider_end_col) {
int const col = (int)(ptr - line);
decor_provider_end_col = invoke_range_next(wp, lnum, col, 100, &has_decor);
line = ml_get_buf(wp->w_buffer, lnum);
ptr = line + col;
}
if (has_decor) {
extra_check = true;
}
// Skip this quickly when working on the text. // Skip this quickly when working on the text.
if (draw_cols) { if (draw_cols) {
if (cul_screenline) { if (cul_screenline) {
@ -2999,3 +3040,27 @@ static void wlv_put_linebuf(win_T *wp, const winlinevars_T *wlv, int endcol, boo
grid_adjust(&grid, &row, &coloff); grid_adjust(&grid, &row, &coloff);
grid_put_linebuf(grid, row, coloff, startcol, endcol, clear_width, bg_attr, wlv->vcol - 1, flags); grid_put_linebuf(grid, row, coloff, startcol, endcol, clear_width, bg_attr, wlv->vcol - 1, flags);
} }
/// @return New begin column, or INT_MAX.
static int invoke_range_next(win_T *wp, int lnum, colnr_T begin_col, colnr_T col_off,
bool *has_decor)
{
char const *const line = ml_get_buf(wp->w_buffer, lnum);
int const line_len = ml_get_buf_len(wp->w_buffer, lnum);
col_off = MAX(col_off, 1);
bool added_decor = false;
colnr_T new_col;
if (col_off <= line_len - begin_col) {
int end_col = begin_col + col_off;
end_col += mb_off_next(line, line + end_col);
decor_providers_invoke_range(wp, lnum - 1, begin_col, lnum - 1, end_col, &added_decor);
new_col = end_col;
} else {
decor_providers_invoke_range(wp, lnum - 1, begin_col, lnum, 0, &added_decor);
new_col = INT_MAX;
}
*has_decor |= added_decor;
return new_col;
}

View File

@ -1287,40 +1287,69 @@ static struct luaL_Reg querycursor_meta[] = {
{ NULL, NULL } { NULL, NULL }
}; };
static inline bool set_uint32(lua_State *L, int idx, char const *name, uint32_t *res)
FUNC_ATTR_ALWAYS_INLINE FUNC_ATTR_NONNULL_ALL
{
lua_getfield(L, idx, name);
if (!lua_isnil(L, -1)) {
int64_t value = lua_tointeger(L, -1);
if (value < 0U) {
*res = 0U;
return true;
} else if (value > UINT32_MAX) {
*res = UINT32_MAX;
return true;
} else {
*res = (uint32_t)value;
return true;
}
}
lua_pop(L, 1);
return false;
}
int tslua_push_querycursor(lua_State *L) int tslua_push_querycursor(lua_State *L)
{ {
TSNode node = node_check(L, 1); TSNode node = node_check(L, 1);
TSQuery *query = query_check(L, 2); TSQuery *query = query_check(L, 2);
TSQueryCursor *cursor = ts_query_cursor_new(); TSQueryCursor *cursor = ts_query_cursor_new();
ts_query_cursor_exec(cursor, query, node);
if (lua_gettop(L) >= 3) { if (lua_gettop(L) >= 3 && !lua_isnil(L, 3)) {
uint32_t start = (uint32_t)luaL_checkinteger(L, 3); luaL_argcheck(L, lua_istable(L, 3), 3, "table expected");
uint32_t end = lua_gettop(L) >= 4 ? (uint32_t)luaL_checkinteger(L, 4) : MAXLNUM;
ts_query_cursor_set_point_range(cursor, (TSPoint){ start, 0 }, (TSPoint){ end, 0 });
}
if (lua_gettop(L) >= 5 && !lua_isnil(L, 5)) { TSPoint begin = (TSPoint){ 0, 0 };
luaL_argcheck(L, lua_istable(L, 5), 5, "table expected"); TSPoint end = (TSPoint){ UINT32_MAX, UINT32_MAX };
lua_pushnil(L); // [dict, ..., nil]
while (lua_next(L, 5)) { if (set_uint32(L, 3, "row_begin", &begin.row)) {
// [dict, ..., key, value] if (!set_uint32(L, 3, "col_begin", &begin.column)) {
if (lua_type(L, -2) == LUA_TSTRING) { begin.column = 0;
char *k = (char *)lua_tostring(L, -2);
if (strequal("max_start_depth", k)) {
uint32_t max_start_depth = (uint32_t)lua_tointeger(L, -1);
ts_query_cursor_set_max_start_depth(cursor, max_start_depth);
} else if (strequal("match_limit", k)) {
uint32_t match_limit = (uint32_t)lua_tointeger(L, -1);
ts_query_cursor_set_match_limit(cursor, match_limit);
}
} }
// pop the value; lua_next will pop the key. }
lua_pop(L, 1); // [dict, ..., key]
if (set_uint32(L, 3, "row_end", &end.row)) {
if (!set_uint32(L, 3, "col_end", &end.column)) {
end.column = 0;
}
}
ts_query_cursor_set_point_range(cursor, begin, end);
uint32_t max_start_depth;
if (set_uint32(L, 3, "max_start_depth", &max_start_depth)) {
ts_query_cursor_set_max_start_depth(cursor, max_start_depth);
}
uint32_t match_limit;
if (set_uint32(L, 3, "match_limit", &match_limit)) {
ts_query_cursor_set_match_limit(cursor, match_limit);
} }
} }
ts_query_cursor_exec(cursor, query, node);
TSQueryCursor **ud = lua_newuserdata(L, sizeof(*ud)); // [node, query, ..., udata] TSQueryCursor **ud = lua_newuserdata(L, sizeof(*ud)); // [node, query, ..., udata]
*ud = cursor; *ud = cursor;
lua_getfield(L, LUA_REGISTRYINDEX, TS_META_QUERYCURSOR); // [node, query, ..., udata, meta] lua_getfield(L, LUA_REGISTRYINDEX, TS_META_QUERYCURSOR); // [node, query, ..., udata, meta]

View File

@ -6,8 +6,7 @@ describe('decor perf', function()
before_each(n.clear) before_each(n.clear)
it('can handle long lines', function() it('can handle long lines', function()
local screen = Screen.new(100, 101) Screen.new(100, 101)
screen:attach()
local result = exec_lua [==[ local result = exec_lua [==[
local ephemeral_pattern = { local ephemeral_pattern = {
@ -99,4 +98,39 @@ describe('decor perf', function()
print('\nTotal ' .. fmt(total) .. '\nDecoration provider: ' .. fmt(provider)) print('\nTotal ' .. fmt(total) .. '\nDecoration provider: ' .. fmt(provider))
end) end)
it('can handle long lines with treesitter highlighting', function()
Screen.new(100, 51)
local result = exec_lua [==[
local long_line = 'local a = { ' .. ('a = 5, '):rep(2000) .. '}'
vim.api.nvim_buf_set_lines(0, 0, 0, false, { long_line })
vim.api.nvim_win_set_cursor(0, { 1, 0 })
vim.treesitter.start(0, 'lua')
local total = {}
for i = 1, 50 do
local tic = vim.uv.hrtime()
vim.cmd'redraw!'
local toc = vim.uv.hrtime()
table.insert(total, toc - tic)
end
return { total }
]==]
local total = unpack(result)
table.sort(total)
local ms = 1 / 1000000
local res = string.format(
'min, 25%%, median, 75%%, max:\n\t%0.1fms,\t%0.1fms,\t%0.1fms,\t%0.1fms,\t%0.1fms',
total[1] * ms,
total[1 + math.floor(#total * 0.25)] * ms,
total[1 + math.floor(#total * 0.5)] * ms,
total[1 + math.floor(#total * 0.75)] * ms,
total[#total] * ms
)
print('\nTotal ' .. res)
end)
end) end)

View File

@ -1,10 +1,11 @@
local n = require('test.functional.testnvim')() local n = require('test.functional.testnvim')()
local Screen = require('test.functional.ui.screen')
local clear = n.clear local clear = n.clear
local exec_lua = n.exec_lua local exec_lua = n.exec_lua
describe('treesitter perf', function() describe('treesitter perf', function()
setup(function() before_each(function()
clear() clear()
end) end)
@ -47,4 +48,149 @@ describe('treesitter perf', function()
return vim.uv.hrtime() - start return vim.uv.hrtime() - start
]] ]]
end) end)
local function test_long_line(pos, wrap, line, grid)
local screen = Screen.new(20, 11)
local result = exec_lua(
[==[
local pos, wrap, line = ...
vim.api.nvim_buf_set_lines(0, 0, 0, false, { line })
vim.api.nvim_win_set_cursor(0, pos)
vim.api.nvim_set_option_value('wrap', wrap, { win = 0 })
vim.treesitter.start(0, 'lua')
local total = {}
for i = 1, 100 do
local tic = vim.uv.hrtime()
vim.cmd'redraw!'
local toc = vim.uv.hrtime()
table.insert(total, toc - tic)
end
return { total }
]==],
pos,
wrap,
line
)
screen:expect({ grid = grid or '' })
local total = unpack(result)
table.sort(total)
local ms = 1 / 1000000
local res = string.format(
'min, 25%%, median, 75%%, max:\n\t%0.2fms,\t%0.2fms,\t%0.2fms,\t%0.2fms,\t%0.2fms',
total[1] * ms,
total[1 + math.floor(#total * 0.25)] * ms,
total[1 + math.floor(#total * 0.5)] * ms,
total[1 + math.floor(#total * 0.75)] * ms,
total[#total] * ms
)
print('\nTotal ' .. res)
end
local long_line = 'local a = { ' .. ('a = 5, '):rep(500) .. '}'
it('can redraw the beginning of a long line with wrapping', function()
local grid = [[
{15:^local} {25:a} {15:=} {16:{} {25:a} {15:=} {26:5}{16:,} {25:a}|
{15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} |
{25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,}|
{25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}|
{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} |
{26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=}|
{26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} |
{15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a}|
{15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} |
{25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,}|
|
]]
test_long_line({ 1, 0 }, true, long_line, grid)
end)
it('can redraw the middle of a long line with wrapping', function()
local grid = [[
{1:<<<}{26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} |
{25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,}|
{25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}|
{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} |
{26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=}|
{26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} |
{15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a}|
{15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} |
{25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,}|
{25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a}^ {15:=} {26:5}|
|
]]
test_long_line({ 1, math.floor(#long_line / 2) }, true, long_line, grid)
end)
it('can redraw the end of a long line with wrapping', function()
local grid = [[
{1:<<<}{25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=}|
{26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} |
{15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a}|
{15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} |
{25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,}|
{25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}|
{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} |
{26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=}|
{26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {25:a} |
{15:=} {26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {16:^}} |
|
]]
test_long_line({ 1, #long_line - 1 }, true, long_line, grid)
end)
it('can redraw the beginning of a long line without wrapping', function()
local grid = [[
{15:^local} {25:a} {15:=} {16:{} {25:a} {15:=} {26:5}{16:,} {25:a}|
|
{1:~ }|*8
|
]]
test_long_line({ 1, 0 }, false, long_line, grid)
end)
it('can redraw the middle of a long line without wrapping', function()
local grid = [[
{16:,} {25:a} {15:=} {26:5}{16:,} {25:a}^ {15:=} {26:5}{16:,} {25:a} {15:=} |
|
{1:~ }|*8
|
]]
test_long_line({ 1, math.floor(#long_line / 2) }, false, long_line, grid)
end)
it('can redraw the end of a long line without wrapping', function()
local grid = [[
{26:5}{16:,} {25:a} {15:=} {26:5}{16:,} {16:^}} |
|
{1:~ }|*8
|
]]
test_long_line({ 1, #long_line - 1 }, false, long_line, grid)
end)
local long_line_mb = 'local a = { ' .. ('À = 5, '):rep(500) .. '}'
it('can redraw the middle of a long line with multibyte characters', function()
local grid = [[
{1:<<<}{26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} |
{25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,}|
{25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}|
{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=} |
{26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=}|
{26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} |
{15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À}|
{15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} |
{25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,}|
{25:À} {15:=} {26:5}{16:,} {25:À} {15:=} {26:5}{16:,} {25:À}^ {15:=} {26:5}|
|
]]
test_long_line({ 1, math.floor(#long_line_mb / 2) }, true, long_line_mb, grid)
end)
end) end)

View File

@ -1854,6 +1854,20 @@ describe('LSP', function()
end, end,
} }
end) end)
it('vim.lsp.start when existing client has no workspace_folders', function()
exec_lua(create_server_definition)
eq(
{ 2, 'foo', 'foo' },
exec_lua(function()
local server = _G._create_server()
vim.lsp.start { cmd = server.cmd, name = 'foo' }
vim.lsp.start { cmd = server.cmd, name = 'foo', root_dir = 'bar' }
local foos = vim.lsp.get_clients()
return { #foos, foos[1].name, foos[2].name }
end)
)
end)
end) end)
describe('parsing tests', function() describe('parsing tests', function()

View File

@ -21,13 +21,12 @@ local function get_search_history(name)
local man = require('man') local man = require('man')
local res = {} local res = {}
--- @diagnostic disable-next-line:duplicate-set-field --- @diagnostic disable-next-line:duplicate-set-field
man.find_path = function(sect, name0) man._find_path = function(name0, sect)
table.insert(res, { sect, name0 }) table.insert(res, { sect, name0 })
return nil return nil
end end
local ok, rv = pcall(man.open_page, -1, { tab = 0 }, args) local err = man.open_page(-1, { tab = 0 }, args)
assert(not ok) assert(err and err:match('no manual entry'))
assert(rv and rv:match('no manual entry'))
return res return res
end) end)
end end
@ -225,7 +224,7 @@ describe(':Man', function()
matches('^/.+', actual_file) matches('^/.+', actual_file)
local args = { nvim_prog, '--headless', '+:Man ' .. actual_file, '+q' } local args = { nvim_prog, '--headless', '+:Man ' .. actual_file, '+q' }
matches( matches(
('Error detected while processing command line:\r\n' .. 'man.lua: "no manual entry for %s"'):format( ('Error detected while processing command line:\r\n' .. 'man.lua: no manual entry for %s'):format(
pesc(actual_file) pesc(actual_file)
), ),
fn.system(args, { '' }) fn.system(args, { '' })
@ -235,8 +234,8 @@ describe(':Man', function()
it('tries variants with spaces, underscores #22503', function() it('tries variants with spaces, underscores #22503', function()
eq({ eq({
{ '', 'NAME WITH SPACES' }, { vim.NIL, 'NAME WITH SPACES' },
{ '', 'NAME_WITH_SPACES' }, { vim.NIL, 'NAME_WITH_SPACES' },
}, get_search_history('NAME WITH SPACES')) }, get_search_history('NAME WITH SPACES'))
eq({ eq({
{ '3', 'some other man' }, { '3', 'some other man' },
@ -255,8 +254,8 @@ describe(':Man', function()
{ 'n', 'some_other_man' }, { 'n', 'some_other_man' },
}, get_search_history('n some other man')) }, get_search_history('n some other man'))
eq({ eq({
{ '', '123some other man' }, { vim.NIL, '123some other man' },
{ '', '123some_other_man' }, { vim.NIL, '123some_other_man' },
}, get_search_history('123some other man')) }, get_search_history('123some other man'))
eq({ eq({
{ '1', 'other_man' }, { '1', 'other_man' },
@ -265,11 +264,10 @@ describe(':Man', function()
end) end)
it('can complete', function() it('can complete', function()
t.skip(t.is_os('mac') and t.is_arch('x86_64'), 'not supported on intel mac')
eq( eq(
true, true,
exec_lua(function() exec_lua(function()
return #require('man').man_complete('f', 'Man g') > 0 return #require('man').man_complete('f', 'Man f') > 0
end) end)
) )
end) end)

View File

@ -717,6 +717,47 @@ void ui_refresh(void)
eq({ { 1, 10, 1, 13 } }, ret) eq({ { 1, 10, 1, 13 } }, ret)
end) end)
it('iter_captures supports columns', function()
local txt = [[
int aaa = 1, bbb = 2;
int foo = 1, bar = 2;
int baz = 3, qux = 4;
int ccc = 1, ddd = 2;
]]
local function test(opts)
local parser = vim.treesitter.get_string_parser(txt, 'c')
local nodes = {}
local query = vim.treesitter.query.parse('c', '((identifier) @foo)')
local root = assert(parser:parse()[1]:root())
local iter = query:iter_captures(root, txt, 1, 2, opts)
while true do
local capture, node = iter()
if not capture then
break
end
table.insert(nodes, { node:range() })
end
return nodes
end
local ret
ret = exec_lua(test, { col_begin = 7, col_end = 13 })
eq({ { 1, 13, 1, 16 }, { 2, 4, 2, 7 } }, ret)
ret = exec_lua(test, { col_begin = 7 })
eq({ { 1, 13, 1, 16 } }, ret)
ret = exec_lua(test, { col_end = 13 })
eq({ { 1, 4, 1, 7 }, { 1, 13, 1, 16 }, { 2, 4, 2, 7 } }, ret)
ret = exec_lua(test, {})
eq({ { 1, 4, 1, 7 }, { 1, 13, 1, 16 } }, ret)
end)
it('fails to load queries', function() it('fails to load queries', function()
local function test(exp, cquery) local function test(exp, cquery)
eq(exp, pcall_err(exec_lua, "vim.treesitter.query.parse('c', ...)", cquery)) eq(exp, pcall_err(exec_lua, "vim.treesitter.query.parse('c', ...)", cquery))

View File

@ -15,32 +15,36 @@ local eq = t.eq
local assert_alive = n.assert_alive local assert_alive = n.assert_alive
local pcall_err = t.pcall_err local pcall_err = t.pcall_err
local function setup_screen(screen)
screen:set_default_attr_ids {
[1] = {bold=true, foreground=Screen.colors.Blue};
[2] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red};
[3] = {foreground = Screen.colors.Brown};
[4] = {foreground = Screen.colors.Blue1};
[5] = {foreground = Screen.colors.Magenta};
[6] = {bold = true, foreground = Screen.colors.Brown};
[7] = {background = Screen.colors.Gray90};
[8] = {bold = true, reverse = true};
[9] = {reverse = true};
[10] = {italic = true, background = Screen.colors.Magenta};
[11] = {foreground = Screen.colors.Red, background = tonumber('0x005028')};
[12] = {foreground = tonumber('0x990000')};
[13] = {background = Screen.colors.LightBlue};
[14] = {background = Screen.colors.WebGray, foreground = Screen.colors.DarkBlue};
[15] = {special = Screen.colors.Blue, undercurl = true},
[16] = {special = Screen.colors.Red, undercurl = true},
[17] = {foreground = Screen.colors.Red},
[18] = {bold = true, foreground = Screen.colors.SeaGreen};
[19] = {bold = true};
}
end
describe('decorations providers', function() describe('decorations providers', function()
local screen local screen
before_each(function() before_each(function()
clear() clear()
screen = Screen.new(40, 8) screen = Screen.new(40, 8)
screen:set_default_attr_ids { setup_screen(screen)
[1] = {bold=true, foreground=Screen.colors.Blue};
[2] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red};
[3] = {foreground = Screen.colors.Brown};
[4] = {foreground = Screen.colors.Blue1};
[5] = {foreground = Screen.colors.Magenta};
[6] = {bold = true, foreground = Screen.colors.Brown};
[7] = {background = Screen.colors.Gray90};
[8] = {bold = true, reverse = true};
[9] = {reverse = true};
[10] = {italic = true, background = Screen.colors.Magenta};
[11] = {foreground = Screen.colors.Red, background = tonumber('0x005028')};
[12] = {foreground = tonumber('0x990000')};
[13] = {background = Screen.colors.LightBlue};
[14] = {background = Screen.colors.WebGray, foreground = Screen.colors.DarkBlue};
[15] = {special = Screen.colors.Blue, undercurl = true},
[16] = {special = Screen.colors.Red, undercurl = true},
[17] = {foreground = Screen.colors.Red},
[18] = {bold = true, foreground = Screen.colors.SeaGreen};
[19] = {bold = true};
}
end) end)
local mulholland = [[ local mulholland = [[
@ -64,7 +68,7 @@ describe('decorations providers', function()
]]) .. [[ ]]) .. [[
api.nvim_set_decoration_provider(_G.ns1, { api.nvim_set_decoration_provider(_G.ns1, {
on_start = on_do; on_buf = on_do; on_start = on_do; on_buf = on_do;
on_win = on_do; on_line = on_do; on_win = on_do; on_line = on_do; on_range = on_do;
on_end = on_do; _on_spell_nav = on_do; on_end = on_do; _on_spell_nav = on_do;
}) })
return _G.ns1 return _G.ns1
@ -107,12 +111,19 @@ describe('decorations providers', function()
{ "start", 4 }; { "start", 4 };
{ "win", 1000, 1, 0, 6 }; { "win", 1000, 1, 0, 6 };
{ "line", 1000, 1, 0 }; { "line", 1000, 1, 0 };
{ "range", 1000, 1, 0, 0, 1, 0 };
{ "line", 1000, 1, 1 }; { "line", 1000, 1, 1 };
{ "range", 1000, 1, 1, 0, 2, 0 };
{ "line", 1000, 1, 2 }; { "line", 1000, 1, 2 };
{ "range", 1000, 1, 2, 0, 3, 0 };
{ "line", 1000, 1, 3 }; { "line", 1000, 1, 3 };
{ "range", 1000, 1, 3, 0, 4, 0 };
{ "line", 1000, 1, 4 }; { "line", 1000, 1, 4 };
{ "range", 1000, 1, 4, 0, 5, 0 };
{ "line", 1000, 1, 5 }; { "line", 1000, 1, 5 };
{ "range", 1000, 1, 5, 0, 6, 0 };
{ "line", 1000, 1, 6 }; { "line", 1000, 1, 6 };
{ "range", 1000, 1, 6, 0, 7, 0 };
{ "end", 4 }; { "end", 4 };
} }
@ -132,6 +143,7 @@ describe('decorations providers', function()
{ "buf", 1, 5 }; { "buf", 1, 5 };
{ "win", 1000, 1, 0, 6 }; { "win", 1000, 1, 0, 6 };
{ "line", 1000, 1, 6 }; { "line", 1000, 1, 6 };
{ "range", 1000, 1, 6, 0, 7, 0 };
{ "end", 5 }; { "end", 5 };
} }
end) end)
@ -199,9 +211,13 @@ describe('decorations providers', function()
{ "start", 5 }; { "start", 5 };
{ "win", 1000, 1, 0, 3 }; { "win", 1000, 1, 0, 3 };
{ "line", 1000, 1, 0 }; { "line", 1000, 1, 0 };
{ "range", 1000, 1, 0, 0, 1, 0 };
{ "line", 1000, 1, 1 }; { "line", 1000, 1, 1 };
{ "range", 1000, 1, 1, 0, 2, 0 };
{ "line", 1000, 1, 2 }; { "line", 1000, 1, 2 };
{ "range", 1000, 1, 2, 0, 3, 0 };
{ "line", 1000, 1, 3 }; { "line", 1000, 1, 3 };
{ "range", 1000, 1, 3, 0, 4, 0 };
{ "end", 5 }; { "end", 5 };
} }
@ -694,6 +710,85 @@ describe('decorations providers', function()
]]) ]])
end) end)
it('on_range is invoked on all visible characters', function()
clear()
screen = Screen.new(20, 4)
setup_screen(screen)
local function record()
exec_lua [[
p_min = { math.huge, math.huge }
p_max = { -math.huge, -math.huge }
function pos_gt(a, b)
return a[1] > b[1] or (a[1] == b[1] and a[2] > b[2])
end
function pos_lt(a, b)
return a[1] < b[1] or (a[1] == b[1] and a[2] < b[2])
end
]]
setup_provider [[
local function on_do(kind, _, bufnr, br, bc, er, ec)
if kind == 'range' then
local b = { br, bc }
local e = { er, ec }
if pos_gt(p_min, b) then
p_min = b
end
if pos_lt(p_max, e) then
p_max = e
end
end
end
]]
end
local function check(min, max)
local function pos_gt(a, b)
return a[1] > b[1] or (a[1] == b[1] and a[2] > b[2])
end
local function pos_lt(a, b)
return a[1] < b[1] or (a[1] == b[1] and a[2] < b[2])
end
local p_min = exec_lua('return p_min')
assert(not pos_gt(p_min, min),
"minimum position " .. vim.inspect(p_min)
.. " should be before the first char")
local p_max = exec_lua('return p_max')
assert(not pos_lt(p_max, max),
"maximum position " .. vim.inspect(p_max)
.. " should be on or after the last char")
end
-- Multiple lines.
exec_lua([[
local lines = { ('a'):rep(40), ('b'):rep(40), ('c'):rep(40) }
vim.api.nvim_buf_set_lines(0, 0, -1, true, lines)
vim.api.nvim_win_set_cursor(0, { 2, 0 })
]])
record()
screen:expect([[
^bbbbbbbbbbbbbbbbbbbb|
bbbbbbbbbbbbbbbbbbbb|
ccccccccccccccccc{1:@@@}|
|
]])
check({ 1, 0 }, { 2, 21 })
-- One long line.
exec_lua([[
local lines = { ('a'):rep(100) }
vim.api.nvim_buf_set_lines(0, 0, -1, true, lines)
vim.api.nvim_win_set_cursor(0, { 1, 70 })
]])
record()
screen:expect([[
{1:<<<}aaaaaaaaaaaaaaaaa|
aaaaaaaaaaaaaaaaaaaa|
aaaaaaaaaa^aaaaaaaaaa|
|
]])
check({ 0, 20 }, { 0, 81 })
end)
it('errors gracefully', function() it('errors gracefully', function()
insert(mulholland) insert(mulholland)