Skip to content

Commit

Permalink
feat: add preview functionality to user commands
Browse files Browse the repository at this point in the history
Adds a Lua-only `preview` flag to user commands which allows the command to be incrementally previewed like `:substitute` when 'inccommand' is set.
  • Loading branch information
famiu committed May 29, 2022
1 parent e9803e1 commit 3c14cb1
Show file tree
Hide file tree
Showing 26 changed files with 996 additions and 346 deletions.
2 changes: 2 additions & 0 deletions runtime/doc/api.txt
Expand Up @@ -760,6 +760,8 @@ nvim_create_user_command({name}, {command}, {*opts})
when a Lua function is used for {command}.
• force: (boolean, default true) Override any
previous definition.
• preview: (function) Preview callback for
'inccommand' |:command-preview|

nvim_del_current_line() *nvim_del_current_line()*
Deletes the current line.
Expand Down
139 changes: 139 additions & 0 deletions runtime/doc/map.txt
Expand Up @@ -1430,6 +1430,145 @@ Possible values are (second column is the short name used in listing):
-addr=other ? other kind of range


Incremental preview ~
*:command-preview* {nvim-api}
It's possible to allow commands to be incrementally previewed when
'inccommand' is enabled by using the preview command flag. The preview
flag may only be used through the Lua API (see |nvim_create_user_command()|).

The preview callback must be a Lua function. When command preview is
triggered, the preview callback will be called with 3 arguments, an `opts` table
which is the same as the table passed to |nvim_create_user_command()| callbacks,
the preview namespace number that's used to set highlights for the preview,
and the preview buffer number that's used for the preview window when
`inccommand=split`. Note that if `inccommand=nosplit`, the preview buffer number
will be `nil`.

In order for command preview to be shown, these steps need to be followed:

1. Modify the current buffer as required for the preview (see
|nvim_buf_set_text()| and |nvim_buf_set_lines()|).
2. If preview buffer is provided, add necessary text to the preview buffer.
3. Add required highlights to the current buffer. If preview buffer is
provided, add required highlights to the the preview buffer as well. All
highlights must be added to the preview namespace which is provided as an
argument to the preview callback (see |nvim_buf_add_highlight()| and
|nvim_buf_set_extmark()| for help on how to add highlights to a namespace).
4. Return an integer which represents the preview type. The preview type
tells Nvim what kind of preview is being shown, which allows it to carry
out the appropriate tasks to show the preview. There preview type can
either be 0, 1 or 2. Here's what the return values mean:
0: No preview is shown.
1: Preview is shown but preview window isn't opened even if
`inccommand=split`.
2: Preview is shown and preview window is opened if `inccommand=split`.

Note that if `inccommand=nosplit`, there's no difference between a return
value of 1 and a return value of 2 from the preview callback.

Once preview is over, Nvim will automatically revert any changes made to the
current buffer and preview buffer during the preview. All highlights set in
the preview namespace will also be cleared.

Here's an example of a command to trim trailing whitespace from lines that
supports incremental command preview:
>
-- Trims trailing whitespace from lines
-- Shows preview if called as a preview callback
-- (which means preview_ns isn't nil)
local function TrimTrailingWhitespace(opts, preview_ns, preview_buf)
local line1 = opts.line1
local line2 = opts.line2
local buf = vim.api.nvim_get_current_buf()
local lines = vim.api.nvim_buf_get_lines(
buf,
line1 - 1,
line2,
0
)
local new_lines = {}
local preview_buf_line = 0
for i, line in ipairs(lines) do
local startidx, endidx = string.find(line, '%s+$')
if startidx ~= nil then
-- Highlight the match if in command preview mode
if preview_ns ~= nil then
vim.api.nvim_buf_add_highlight(
buf,
preview_ns,
'Substitute',
line1 + i - 2,
startidx - 1,
endidx
)
-- Add lines and highlight to the preview buffer
-- if inccommand=split
if preview_buf ~= nil then
local prefix = string.format('|%d| ', line1 + i - 1)
vim.api.nvim_buf_set_lines(
preview_buf,
preview_buf_line,
preview_buf_line,
0,
{ prefix .. line }
)
vim.api.nvim_buf_add_highlight(
preview_buf,
preview_ns,
'Substitute',
preview_buf_line,
#prefix + startidx - 1,
#prefix + endidx
)
preview_buf_line = preview_buf_line + 1
end
end
end
if not preview_ns then
new_lines[#new_lines+1] = string.gsub(
line,
'%s+$',
''
)
end
end
-- Don't make any changes to the buffer if previewing
if not preview_ns then
vim.api.nvim_buf_set_lines(
buf,
line1 - 1,
line2,
0,
new_lines
)
end
-- When called as a preview callback, return the value of the
-- preview type
if preview_ns ~= nil then
return 2
end
end
-- Create the user command
vim.api.nvim_create_user_command(
'TrimTrailingWhitespace',
TrimTrailingWhitespace,
{ nargs = '?', range = '%', addr = 'lines',
preview = TrimTrailingWhitespace }
)
<
Note that in this case, the same function is used as both the command callback
and the preview callback, and preview is shown only when the `preview_ns`
argument is non-nil. It's also possible to set the preview callback to a
separate function in order to handle showing the preview separately from the
actual command.


Special cases ~
*:command-bang* *:command-bar*
*:command-register* *:command-buffer*
Expand Down
10 changes: 6 additions & 4 deletions runtime/doc/options.txt
Expand Up @@ -3266,17 +3266,19 @@ A jump table for the options with a short description can be found at |Q_op|.
'inccommand' 'icm' string (default "nosplit")
global

When nonempty, shows the effects of |:substitute|, |:smagic|, and
|:snomagic| as you type.
When nonempty, shows the effects of |:substitute|, |:smagic|,
|:snomagic| and user commands with the |:command-preview| flag as you
type.

Possible values:
nosplit Shows the effects of a command incrementally in the
buffer.
split Like "nosplit", but also shows partial off-screen
results in a preview window.

If the preview is too slow (exceeds 'redrawtime') then 'inccommand' is
automatically disabled until |Command-line-mode| is done.
If the preview for built-in commands is too slow (exceeds
'redrawtime') then 'inccommand' is automatically disabled until
|Command-line-mode| is done.

*'include'* *'inc'*
'include' 'inc' string (default "^\s*#\s*include")
Expand Down
2 changes: 2 additions & 0 deletions runtime/doc/vim_diff.txt
Expand Up @@ -183,6 +183,7 @@ Commands:
|:sign-define| accepts a `numhl` argument, to highlight the line number
|:match| can be invoked before highlight group is defined
|:source| works with Lua and anonymous (no file) scripts
User commands can support |:command-preview| to show results as you type

Events:
|RecordingEnter|
Expand Down Expand Up @@ -235,6 +236,7 @@ Options:
"horizdown", "vertleft", "vertright", "verthoriz"
'foldcolumn' supports up to 9 dynamic/fixed columns
'inccommand' shows interactive results for |:substitute|-like commands
and |:command-preview| commands
'laststatus' global statusline support
'pumblend' pseudo-transparent popupmenu
'scrollback'
Expand Down
1 change: 1 addition & 0 deletions src/nvim/api/keysets.lua
Expand Up @@ -53,6 +53,7 @@ return {
"force";
"keepscript";
"nargs";
"preview";
"range";
"register";
};
Expand Down
11 changes: 10 additions & 1 deletion src/nvim/api/private/helpers.c
Expand Up @@ -1438,6 +1438,7 @@ void create_user_command(String name, Object command, Dict(user_command) *opts,
char *rep = NULL;
LuaRef luaref = LUA_NOREF;
LuaRef compl_luaref = LUA_NOREF;
LuaRef preview_luaref = LUA_NOREF;

if (!uc_validate_name(name.data)) {
api_set_error(err, kErrorTypeValidation, "Invalid command name");
Expand Down Expand Up @@ -1592,6 +1593,14 @@ void create_user_command(String name, Object command, Dict(user_command) *opts,
goto err;
}

if (opts->preview.type == kObjectTypeLuaRef) {
argt |= EX_PREVIEW;
preview_luaref = api_new_luaref(opts->preview.data.luaref);
} else if (HAS_KEY(opts->preview)) {
api_set_error(err, kErrorTypeValidation, "Invalid value for 'preview'");
goto err;
}

switch (command.type) {
case kObjectTypeLuaRef:
luaref = api_new_luaref(command.data.luaref);
Expand All @@ -1611,7 +1620,7 @@ void create_user_command(String name, Object command, Dict(user_command) *opts,
}

if (uc_add_command(name.data, name.size, rep, argt, def, flags, compl, compl_arg, compl_luaref,
addr_type_arg, luaref, force) != OK) {
preview_luaref, addr_type_arg, luaref, force) != OK) {
api_set_error(err, kErrorTypeException, "Failed to create user command");
// Do not goto err, since uc_add_command now owns luaref, compl_luaref, and compl_arg
}
Expand Down
1 change: 1 addition & 0 deletions src/nvim/api/vim.c
Expand Up @@ -2511,6 +2511,7 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error *
/// - desc: (string) Used for listing the command when a Lua function is used for
/// {command}.
/// - force: (boolean, default true) Override any previous definition.
/// - preview: (function) Preview callback for 'inccommand' |:command-preview|
/// @param[out] err Error details, if any.
void nvim_create_user_command(String name, Object command, Dict(user_command) *opts, Error *err)
FUNC_API_SINCE(9)
Expand Down
2 changes: 1 addition & 1 deletion src/nvim/api/vimscript.c
Expand Up @@ -1311,7 +1311,7 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Error
}

WITH_SCRIPT_CONTEXT(channel_id, {
execute_cmd(&ea, &cmdinfo);
execute_cmd(&ea, &cmdinfo, false);
});

if (output) {
Expand Down
3 changes: 3 additions & 0 deletions src/nvim/buffer_defs.h
Expand Up @@ -906,6 +906,9 @@ struct file_buffer {
int flush_count;

int b_diff_failed; // internal diff failed for this buffer

// Whether to not send b:changedtick for buffer updates
bool no_send_tick;
};

/*
Expand Down
10 changes: 5 additions & 5 deletions src/nvim/buffer_updates.c
Expand Up @@ -187,7 +187,7 @@ void buf_updates_unload(buf_T *buf, bool can_reload)
}

void buf_updates_send_changes(buf_T *buf, linenr_T firstline, int64_t num_added,
int64_t num_removed, bool send_tick)
int64_t num_removed)
{
size_t deleted_codepoints, deleted_codeunits;
size_t deleted_bytes = ml_flush_deleted_bytes(buf, &deleted_codepoints,
Expand All @@ -213,7 +213,7 @@ void buf_updates_send_changes(buf_T *buf, linenr_T firstline, int64_t num_added,
args.items[0] = BUFFER_OBJ(buf->handle);

// next argument is b:changedtick
args.items[1] = send_tick ? INTEGER_OBJ(buf_get_changedtick(buf)) : NIL;
args.items[1] = !buf->no_send_tick ? INTEGER_OBJ(buf_get_changedtick(buf)) : NIL;

// the first line that changed (zero-indexed)
args.items[2] = INTEGER_OBJ(firstline - 1);
Expand Down Expand Up @@ -253,7 +253,7 @@ void buf_updates_send_changes(buf_T *buf, linenr_T firstline, int64_t num_added,
for (size_t i = 0; i < kv_size(buf->update_callbacks); i++) {
BufUpdateCallbacks cb = kv_A(buf->update_callbacks, i);
bool keep = true;
if (cb.on_lines != LUA_NOREF && (cb.preview || !(State & MODE_CMDPREVIEW))) {
if (cb.on_lines != LUA_NOREF && (cb.preview || !cmdpreview)) {
Array args = ARRAY_DICT_INIT;
Object items[8];
args.size = 6; // may be increased to 8 below
Expand All @@ -263,7 +263,7 @@ void buf_updates_send_changes(buf_T *buf, linenr_T firstline, int64_t num_added,
args.items[0] = BUFFER_OBJ(buf->handle);

// next argument is b:changedtick
args.items[1] = send_tick ? INTEGER_OBJ(buf_get_changedtick(buf)) : NIL;
args.items[1] = !buf->no_send_tick ? INTEGER_OBJ(buf_get_changedtick(buf)) : NIL;

// the first line that changed (zero-indexed)
args.items[2] = INTEGER_OBJ(firstline - 1);
Expand Down Expand Up @@ -312,7 +312,7 @@ void buf_updates_send_splice(buf_T *buf, int start_row, colnr_T start_col, bcoun
for (size_t i = 0; i < kv_size(buf->update_callbacks); i++) {
BufUpdateCallbacks cb = kv_A(buf->update_callbacks, i);
bool keep = true;
if (cb.on_bytes != LUA_NOREF && (cb.preview || !(State & MODE_CMDPREVIEW))) {
if (cb.on_bytes != LUA_NOREF && (cb.preview || !cmdpreview)) {
FIXED_TEMP_ARRAY(args, 11);

// the first argument is always the buffer handle
Expand Down
4 changes: 2 additions & 2 deletions src/nvim/change.c
Expand Up @@ -351,7 +351,7 @@ void changed_bytes(linenr_T lnum, colnr_T col)
changedOneline(curbuf, lnum);
changed_common(lnum, col, lnum + 1, 0L);
// notify any channels that are watching
buf_updates_send_changes(curbuf, lnum, 1, 1, true);
buf_updates_send_changes(curbuf, lnum, 1, 1);

// Diff highlighting in other diff windows may need to be updated too.
if (curwin->w_p_diff) {
Expand Down Expand Up @@ -501,7 +501,7 @@ void changed_lines(linenr_T lnum, colnr_T col, linenr_T lnume, long xtra, bool d
if (do_buf_event) {
int64_t num_added = (int64_t)(lnume + xtra - lnum);
int64_t num_removed = lnume - lnum;
buf_updates_send_changes(curbuf, lnum, num_added, num_removed, true);
buf_updates_send_changes(curbuf, lnum, num_added, num_removed);
}
}

Expand Down

0 comments on commit 3c14cb1

Please sign in to comment.