Skip to content

Commit 990aa48

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

File tree

3 files changed

+184
-31
lines changed

3 files changed

+184
-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: 53 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)
@@ -85,6 +88,7 @@ local function add_inline_highlights(line, cached_lns, updated_lines, undo_delet
8588
-- Observation: when changing "line" to "tes", there should not be an offset (-2)
8689
-- after changing "lin" to "t" (because we are not modifying the line)
8790
highlight.column = highlight.column + col_offset
91+
highlight.hunk = nil
8892
table.insert(highlights, highlight)
8993

9094
if defer then
@@ -176,13 +180,14 @@ end
176180
-- Expose functions to tests
177181
M._preview_across_lines = get_diff_highlights
178182

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)
183+
local function run_cmd(cmd)
184+
local cursor_pos = vim.api.nvim_win_get_cursor(0)
185+
cursor_row, cursor_col = cursor_pos[1], cursor_pos[2]
186+
187+
log(function()
188+
return ("Previewing command: %s (l=%d,c=%d)"):format(cmd, cursor_row, cursor_col)
185189
end)
190+
return remote.run_cmd(chan_id, cmd, cursor_row, cursor_col)
186191
end
187192

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

215+
local updated_lines
210216
if opts.line1 == opts.line2 then
211-
run_buf_cmd(bufnr, ("%s %s"):format(command.cmd, args))
217+
updated_lines = run_cmd(("%s %s"):format(command.cmd, args))
212218
else
213-
run_buf_cmd(bufnr, ("%d,%d%s %s"):format(opts.line1, opts.line2, command.cmd, args))
219+
updated_lines = run_cmd(("%d,%d%s %s"):format(opts.line1, opts.line2, command.cmd, args))
214220
end
215221

216222
vim.v.errmsg = prev_errmsg
@@ -220,7 +226,6 @@ local function command_preview(opts, preview_ns, preview_buf)
220226
math.max(visible_line_range[2], vim.fn.line("w$")),
221227
}
222228

223-
local updated_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
224229
local set_lines = function(lines)
225230
-- TODO: is this worth optimizing?
226231
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
@@ -341,6 +346,39 @@ local validate_config = function(config)
341346
end
342347
end
343348

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

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-
})
396+
remote.init_rpc(function(id)
397+
chan_id = id
398+
end)
399+
create_autocmds()
366400

367401
M.debug = user_config.debug
368402

@@ -375,6 +409,6 @@ M.setup = function(user_config)
375409
end, { nargs = 0 })
376410
end
377411

378-
M.version = "1.2.1"
412+
M.version = "1.3.0"
379413

380414
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)