Skip to content

Commit c5b796e

Browse files
committed
fix: use remote server for executing commands
1 parent ce4b104 commit c5b796e

File tree

3 files changed

+183
-31
lines changed

3 files changed

+183
-31
lines changed

README.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# live-command.nvim
2-
![version](https://img.shields.io/badge/version-1.2.1-brightgreen)
2+
![version](https://img.shields.io/badge/version-1.3.0-brightgreen)
33

44
Text editing in Neovim with immediate visual feedback: view the effects of any command on your buffer contents live. Preview macros, the `:norm` command & more!
55

@@ -9,24 +9,26 @@ Text editing in Neovim with immediate visual feedback: view the effects of any c
99
<p><sub>Theme: <a href="https://github.com/folke/tokyonight.nvim">tokyonight.nvim</a></sub></p>
1010

1111
## :sparkles: Motivation and Features
12-
In version 0.8, Neovim has introduced the `command-preview` feature.
13-
Contrary to what "command preview" suggests, previewing any given
14-
command does not work out of the box: you need to manually update the buffer text and set
15-
highlights *for every command*.
12+
In Neovim version 0.8, the `command-preview` feature has been introduced.
13+
Despite its name, it does not enable automatic previewing of any command.
14+
Instead, users must manually update the buffer text and set highlights *for each command*.
1615

17-
This plugin tries to change that: it provides a **simple API for creating previewable commands**
18-
in Neovim. Just specify the command you want to run and live-command will do all the
19-
work for you. This includes viewing **individual insertions, changes and deletions** as you
20-
type.
16+
This plugin aims to address this issue by offering a **simple API for creating previewable commands**
17+
in Neovim. Simply provide the command you want to preview and live-command will do all the
18+
work for you. This includes viewing **individual insertions, changes and deletions** as you type.
19+
20+
After the most recent update, live-command now spawns a separate Neovim instance to execute commands.
21+
This avoids many issues encountered when running the command directly in the current Neovim instance
22+
([#6](https://github.com/smjonas/live-command.nvim/issues/6), [#16](https://github.com/smjonas/live-command.nvim/issues/16), [#24](https://github.com/smjonas/live-command.nvim/issues/24), [#28](https://github.com/smjonas/live-command.nvim/issues/28)).
2123

2224
## Requirements
2325
Neovim 0.8+
2426

2527
## :rocket: Getting started
2628
Install using your favorite package manager and call the setup function with a table of
27-
commands to create. Here is an example that creates a previewable `:Norm` command:
29+
commands to create. Here is an example for `lazy.nvim` that creates a previewable `:Norm` command:
2830
```lua
29-
use {
31+
{
3032
"smjonas/live-command.nvim",
3133
-- live-command supports semantic versioning via tags
3234
-- tag = "1.*",
@@ -50,7 +52,7 @@ Here is a list of available settings:
5052
| ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------
5153
| cmd | string | The name of an existing command to preview.
5254
| args | string? \| function(arg: string?, opts: table) -> string | Arguments passed to the command. If a function, takes in the options passed to the command and must return the transformed argument(s) `cmd` will be called with. `opts` has the same structure as the `opts` table passed to the `nvim_create_user_command` callback function. If `nil`, the arguments are supplied from the command-line while the user is typing the command.
53-
| range | string? | The range to prepend to the command. Set this to `""` if you don't want the new command to receive a count, e.g. when turning `:9Reg a` into `:norm 9@a`. If `nil`, the range will be supplied from the command entered.
55+
| range | string? | The range to prepend to the command. Set this to `""` if you don't want the new command to receive a count, e.g. when turning `:9Reg a` into `:norm 9@a`. If `nil`, the range will be supplied from the command entered.
5456

5557
### Example
5658
The following example creates a `:Reg` command which allows you to preview the effects of macros (e.g. `:5Reg a` to run macro `a` five times).

lua/live-command/init.lua

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ M.defaults = {
1313
local should_cache_lines = true
1414
local cached_lines
1515
local prev_lazyredraw
16+
local remote, chan_id
17+
-- local server_address, chan_id
18+
local cursor_row, cursor_col
1619

1720
local logs = {}
1821
local function log(msg, level)
@@ -176,13 +179,14 @@ end
176179
-- Expose functions to tests
177180
M._preview_across_lines = get_diff_highlights
178181

179-
local function run_buf_cmd(buf, cmd)
180-
vim.api.nvim_buf_call(buf, function()
181-
log(function()
182-
return ("Previewing command: %s (current line = %d)"):format(cmd, vim.api.nvim_win_get_cursor(0)[1])
183-
end)
184-
vim.cmd(cmd)
182+
local function run_cmd(cmd)
183+
local cursor_pos = vim.api.nvim_win_get_cursor(0)
184+
cursor_row, cursor_col = cursor_pos[1], cursor_pos[2]
185+
186+
log(function()
187+
return ("Previewing command: %s (l=%d,c=%d)"):format(cmd, cursor_row, cursor_col)
185188
end)
189+
return remote.run_cmd(chan_id, cmd, cursor_row, cursor_col)
186190
end
187191

188192
-- Called when the user is still typing the command or the command arguments
@@ -207,10 +211,11 @@ local function command_preview(opts, preview_ns, preview_buf)
207211
local prev_errmsg = vim.v.errmsg
208212
local visible_line_range = { vim.fn.line("w0"), vim.fn.line("w$") }
209213

214+
local updated_lines
210215
if opts.line1 == opts.line2 then
211-
run_buf_cmd(bufnr, ("%s %s"):format(command.cmd, args))
216+
updated_lines = run_cmd(("%s %s"):format(command.cmd, args))
212217
else
213-
run_buf_cmd(bufnr, ("%d,%d%s %s"):format(opts.line1, opts.line2, command.cmd, args))
218+
updated_lines = run_cmd(("%d,%d%s %s"):format(opts.line1, opts.line2, command.cmd, args))
214219
end
215220

216221
vim.v.errmsg = prev_errmsg
@@ -220,7 +225,6 @@ local function command_preview(opts, preview_ns, preview_buf)
220225
math.max(visible_line_range[2], vim.fn.line("w$")),
221226
}
222227

223-
local updated_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
224228
local set_lines = function(lines)
225229
-- TODO: is this worth optimizing?
226230
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
@@ -341,6 +345,39 @@ local validate_config = function(config)
341345
end
342346
end
343347

348+
local create_autocmds = function()
349+
local id = vim.api.nvim_create_augroup("command_preview.nvim", { clear = true })
350+
351+
vim.api.nvim_create_autocmd("CmdlineEnter", {
352+
group = id,
353+
callback = remote.on_cmdline_enter,
354+
})
355+
356+
-- We need to be able to tell when the command was cancelled so the buffer lines are refetched next time.
357+
vim.api.nvim_create_autocmd("CmdLineLeave", {
358+
group = id,
359+
-- Schedule wrap to run after a potential command execution
360+
callback = vim.schedule_wrap(function()
361+
restore_buffer_state()
362+
end),
363+
})
364+
365+
vim.api.nvim_create_autocmd("VimLeavePre", {
366+
group = id,
367+
callback = function()
368+
if chan_id then
369+
vim.fn.chanclose(chan_id)
370+
end
371+
remote.shutdown()
372+
end,
373+
})
374+
375+
vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI", "BufEnter" }, {
376+
group = id,
377+
callback = remote.on_text_changed,
378+
})
379+
end
380+
344381
M.setup = function(user_config)
345382
if vim.fn.has("nvim-0.8.0") ~= 1 then
346383
vim.notify(
@@ -353,16 +390,12 @@ M.setup = function(user_config)
353390
local config = vim.tbl_deep_extend("force", M.defaults, user_config or {})
354391
validate_config(config)
355392
create_user_commands(config.commands)
393+
remote = require("live-command.remote")
356394

357-
local id = vim.api.nvim_create_augroup("command_preview.nvim", { clear = true })
358-
-- We need to be able to tell when the command was cancelled so the buffer lines are refetched next time.
359-
vim.api.nvim_create_autocmd({ "CmdLineLeave" }, {
360-
group = id,
361-
-- Schedule wrap to run after a potential command execution
362-
callback = vim.schedule_wrap(function()
363-
restore_buffer_state()
364-
end),
365-
})
395+
remote.init_rpc(function(id)
396+
chan_id = id
397+
end)
398+
create_autocmds()
366399

367400
M.debug = user_config.debug
368401

@@ -375,6 +408,6 @@ M.setup = function(user_config)
375408
end, { nargs = 0 })
376409
end
377410

378-
M.version = "1.2.1"
411+
M.version = "1.3.0"
379412

380413
return M

lua/live-command/remote.lua

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
local M = {}
2+
3+
local tmp_file = os.tmpname()
4+
local tmp_file_watcher
5+
local uv = vim.loop
6+
local dirty = true
7+
8+
local watch_tmp_file = function(chan_id)
9+
tmp_file_watcher = uv.new_fs_event()
10+
tmp_file_watcher:start(
11+
tmp_file,
12+
{},
13+
vim.schedule_wrap(function(_, fname, status)
14+
if dirty then
15+
-- Open the current file in the remote Nvim instance
16+
vim.rpcrequest(
17+
chan_id,
18+
"nvim_exec",
19+
-- Store the current sequence number that can be
20+
("e! %s | lua vim.g._seq_cur = vim.fn.undotree().seq_cur"):format(tmp_file),
21+
false
22+
)
23+
dirty = false
24+
end
25+
end)
26+
)
27+
end
28+
29+
-- Starts a new Nvim instance and connects to it via RPC
30+
M.init_rpc = function(on_chan_id)
31+
-- Avoid an infinite loop
32+
if vim.env.LIVECOMMAND_NVIM_SERVER == "1" then
33+
return
34+
end
35+
36+
local server_address = vim.fs.normalize(vim.fn.stdpath("cache")) .. "/live_command_server_%d.pipe"
37+
-- Randomize address to allow multiple Neovim instances running live-command at the same time
38+
server_address = server_address:format(vim.fn.rand())
39+
40+
-- Use environment variables from parent process
41+
local env = { "LIVECOMMAND_NVIM_SERVER=1" }
42+
for k, v in pairs(uv.os_environ()) do
43+
table.insert(env, k .. "=" .. v)
44+
end
45+
46+
local handle
47+
handle, _ = uv.spawn(
48+
vim.v.progpath,
49+
{
50+
args = { "--listen", server_address, "-n" },
51+
env = env,
52+
cwd = vim.fn.getcwd(),
53+
},
54+
vim.schedule_wrap(function(_, _) -- on exit
55+
handle:close()
56+
end)
57+
)
58+
59+
local basename = vim.fs.basename(server_address)
60+
local watcher = uv.new_fs_event()
61+
watcher:start(
62+
vim.fs.dirname(server_address),
63+
{},
64+
vim.schedule_wrap(function(_, fname, status)
65+
if status.rename and fname == basename then
66+
local chan_id = vim.fn.sockconnect("pipe", server_address, { rpc = true })
67+
on_chan_id(chan_id)
68+
watch_tmp_file(chan_id)
69+
watcher:stop()
70+
end
71+
end)
72+
)
73+
end
74+
75+
M.shutdown = function()
76+
if tmp_file_watcher then
77+
tmp_file_watcher:stop()
78+
end
79+
end
80+
81+
M.on_text_changed = function()
82+
dirty = true
83+
end
84+
85+
M.on_cmdline_enter = function()
86+
if dirty then
87+
-- Synchronize buffers by writing out the current buffer contents to a temporary file
88+
vim.cmd("keepalt w! " .. tmp_file)
89+
end
90+
end
91+
92+
-- Runs a command on the remote server and returns the updates buffer lines.
93+
-- This is superior to running vim.cmd from the preview callback in the original Nvim instance
94+
-- as that has a lot of side effects, e.g. https://github.com/neovim/neovim/issues/21495.
95+
M.run_cmd = function(chan_id, cmd, cursor_row, cursor_col)
96+
-- Restore the buffer state since the last write and move the cursor to the correct position
97+
vim.rpcrequest(
98+
chan_id,
99+
"nvim_exec_lua",
100+
("vim.api.nvim_win_set_cursor(0, {%d, %d})"):format(cursor_row, cursor_col),
101+
{}
102+
)
103+
-- Execute the command asynchronously using rpcnotify as it may block
104+
vim.rpcnotify(chan_id, "nvim_exec", cmd, false)
105+
106+
return vim.rpcrequest(
107+
chan_id,
108+
"nvim_exec_lua",
109+
[[local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
110+
vim.cmd.undo({count = vim.g._seq_cur})
111+
return lines
112+
]],
113+
{}
114+
)
115+
end
116+
117+
return M

0 commit comments

Comments
 (0)