fix: update neovim listen_addr restart protocol and keep open args per route (#3441)

the old :restart progpath/argv payload is not the protocol anymore
see: https://github.com/neovim/neovim/pull/35223

the contract is now simpler:

  - the server starts the new neovim instance
  - the UI gets a restart event with the new listen address
  - the UI follows that address instead of rebuilding argv

impl the new protocol exposes the second bug. if a reused Neovide process
opens a new embedded window, that route has to start with the files it was
actually asked to open, otherwise :restart will attempt to restart the
process startup args from the first window, or an empty session.
This commit is contained in:
Alexsander Falcucci 2026-03-31 22:03:25 +02:00 committed by GitHub
parent 458bfd0578
commit 32f223ae75
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 154 additions and 192 deletions

View file

@ -5,13 +5,35 @@ use std::path::PathBuf;
use std::process::Command as StdCommand;
use tokio::process::Command as TokioCommand;
use crate::{
bridge::RestartDetails, cmd_line::CmdLineSettings, settings::*, utils::handle_wslpaths,
};
use crate::{cmd_line::CmdLineSettings, utils::handle_wslpaths};
#[cfg(target_os = "macos")]
const FORKED_FROM_TTY_ENV_VAR: &str = "NEOVIDE_FORKED_FROM_TTY";
/// For route-local startup targets for an embedded nvim instance.
///
/// we keep these separate from process-global cmd line settings because a reused
/// neovide process can open additional windows with their own launch context.
/// that context needs to be part of the actual nvim argv so e.g :restart replays
/// the same buffers/tabs, instead of only preserving the initial empty startup.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OpenArgs {
pub files_to_open: Vec<String>,
pub tabs: bool,
}
/// Mode is how neovide should populate argv when launching an embedded nvim instance.
///
/// Startup uses the original process command line targets,
/// Args uses route-local targets such as macOS handoff new windows,
/// None launches a blank embedded instance.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OpenMode {
None,
Startup,
Args(OpenArgs),
}
#[derive(Clone)]
struct CommandSpec {
program: String,
@ -37,28 +59,8 @@ impl CommandSpec {
}
}
pub fn create_restart_nvim_command(
settings: &Settings,
details: &RestartDetails,
cwd: Option<&Path>,
) -> TokioCommand {
let cmdline_settings = settings.get::<CmdLineSettings>();
let spec = create_restart_command_spec(details, &cmdline_settings);
#[allow(unused_mut)]
let mut cmd = tokio_command_from_spec(spec);
if let Some(dir) = command_cwd(&cmdline_settings, cwd) {
cmd.current_dir(dir);
}
#[cfg(target_os = "windows")]
cmd.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
cmd
}
pub fn create_blocking_nvim_command(cmdline_settings: &CmdLineSettings, embed: bool) -> StdCommand {
let (bin, args) = build_nvim_command_parts(cmdline_settings, embed);
let (bin, args) = build_nvim_command_parts(cmdline_settings, embed, OpenMode::Startup);
let spec = create_command_spec(&bin, &args, cmdline_settings);
let mut cmd = std_command_from_spec(spec);
if let Some(dir) = command_cwd(cmdline_settings, None) {
@ -71,8 +73,9 @@ pub fn create_tokio_nvim_command(
cmdline_settings: &CmdLineSettings,
embed: bool,
cwd: Option<&Path>,
mode: OpenMode,
) -> TokioCommand {
let (bin, args) = build_nvim_command_parts(cmdline_settings, embed);
let (bin, args) = build_nvim_command_parts(cmdline_settings, embed, mode);
let spec = create_command_spec(&bin, &args, cmdline_settings);
let mut cmd = tokio_command_from_spec(spec);
if let Some(dir) = command_cwd(cmdline_settings, cwd) {
@ -88,6 +91,7 @@ fn command_cwd(settings: &CmdLineSettings, cwd: Option<&Path>) -> Option<PathBuf
fn build_nvim_command_parts(
cmdline_settings: &CmdLineSettings,
embed: bool,
mode: OpenMode,
) -> (String, Vec<String>) {
let bin = cmdline_settings.neovim_bin.clone().unwrap_or_else(|| "nvim".to_owned());
let mut args = cmdline_settings.neovim_args.clone();
@ -95,46 +99,21 @@ fn build_nvim_command_parts(
append_embed_arg(&mut args);
}
args.extend(build_auto_open_args(cmdline_settings));
args.extend(build_open_args(cmdline_settings, mode));
(bin, args)
}
fn create_restart_command_spec(
details: &RestartDetails,
cmdline_settings: &CmdLineSettings,
) -> CommandSpec {
let (program, args) = build_restart_command_parts(details, cmdline_settings);
create_command_spec(&program, &args, cmdline_settings)
}
fn build_open_args(cmdline_settings: &CmdLineSettings, open_mode: OpenMode) -> Vec<String> {
let (files_to_open, tabs) = match open_mode {
OpenMode::None => return Vec::new(),
OpenMode::Startup => (cmdline_settings.files_to_open.clone(), cmdline_settings.tabs),
OpenMode::Args(args) => (args.files_to_open, args.tabs),
};
fn build_restart_command_parts(
details: &RestartDetails,
cmdline_settings: &CmdLineSettings,
) -> (String, Vec<String>) {
if should_replay_startup_command(cmdline_settings) {
return build_nvim_command_parts(cmdline_settings, true);
}
build_inner_restart_command_parts(details)
}
fn should_replay_startup_command(cmdline_settings: &CmdLineSettings) -> bool {
cmdline_settings.server.is_none()
}
fn build_inner_restart_command_parts(details: &RestartDetails) -> (String, Vec<String>) {
let mut args = details.argv.iter().skip(1).cloned().collect::<Vec<_>>();
prepend_embed_arg(&mut args);
(details.progpath.clone(), args)
}
fn build_auto_open_args(cmdline_settings: &CmdLineSettings) -> Vec<String> {
cmdline_settings
.tabs
.then(|| "-p".to_string())
tabs.then(|| "-p".to_string())
.into_iter()
.chain(handle_wslpaths(cmdline_settings.files_to_open.clone(), cmdline_settings.wsl))
.chain(handle_wslpaths(files_to_open, cmdline_settings.wsl))
.collect()
}
@ -144,12 +123,6 @@ fn append_embed_arg(args: &mut Vec<String>) {
}
}
fn prepend_embed_arg(args: &mut Vec<String>) {
if !args.iter().any(|arg| arg == "--embed") {
args.insert(0, "--embed".to_string());
}
}
fn tokio_command_from_spec(spec: CommandSpec) -> TokioCommand {
let CommandSpec {
program,
@ -274,7 +247,7 @@ fn create_command_spec(
mod tests {
use std::path::{Path, PathBuf};
use crate::cmd_line::handle_command_line_arguments;
use crate::{cmd_line::handle_command_line_arguments, settings::Settings};
use super::*;
@ -285,23 +258,38 @@ mod tests {
settings.get::<CmdLineSettings>()
}
fn parse_settings(args: &[&str]) -> Settings {
let settings = Settings::new();
let args = args.iter().map(|arg| arg.to_string()).collect();
handle_command_line_arguments(args, &settings).expect("Could not parse arguments");
settings
}
#[test]
fn build_nvim_command_parts_places_embed_before_auto_open_args() {
let cmdline_settings =
parse_cmdline_settings(&["neovide", "./foo.txt", "./bar.md", "--grid=420x240"]);
let (_, args) = build_nvim_command_parts(&cmdline_settings, true);
let (_, args) = build_nvim_command_parts(&cmdline_settings, true, OpenMode::Startup);
assert_eq!(args, vec!["--embed", "-p", "./foo.txt", "./bar.md"]);
}
#[test]
fn build_nvim_command_parts_skips_auto_open_args_when_requested() {
let cmdline_settings =
parse_cmdline_settings(&["neovide", "./foo.txt", "./bar.md", "--grid=420x240"]);
let (_, args) = build_nvim_command_parts(&cmdline_settings, true, OpenMode::None);
assert_eq!(args, vec!["--embed"]);
}
#[test]
fn build_nvim_command_parts_uses_route_auto_open_args() {
let cmdline_settings =
parse_cmdline_settings(&["neovide", "./foo.txt", "./bar.md", "--grid=420x240"]);
let open_args = OpenArgs { files_to_open: vec!["/tmp/project".to_string()], tabs: false };
let (_, args) =
build_nvim_command_parts(&cmdline_settings, true, OpenMode::Args(open_args));
assert_eq!(args, vec!["--embed", "/tmp/project"]);
}
#[test]
fn build_nvim_command_parts_preserves_launcher_args_before_embed() {
let cmdline_settings = parse_cmdline_settings(&[
@ -314,62 +302,12 @@ mod tests {
"nvim",
]);
let (bin, args) = build_nvim_command_parts(&cmdline_settings, true);
let (bin, args) = build_nvim_command_parts(&cmdline_settings, true, OpenMode::Startup);
assert_eq!(bin, "ssh");
assert_eq!(args, vec!["my-server", "nvim", "--embed"]);
}
#[test]
fn build_restart_command_parts_replays_original_launcher_command() {
let cmdline_settings = parse_cmdline_settings(&[
"neovide",
"--no-tabs",
"--neovim-bin",
"ssh",
"--",
"my-server",
"nvim",
]);
let restart_details = RestartDetails {
progpath: "/usr/bin/nvim".to_string(),
argv: vec!["nvim".to_string(), "--clean".to_string()],
};
let (program, args) = build_restart_command_parts(&restart_details, &cmdline_settings);
assert_eq!(program, "ssh");
assert_eq!(args, vec!["my-server", "nvim", "--embed"]);
}
#[test]
fn build_restart_command_parts_replays_original_auto_open_args() {
let cmdline_settings =
parse_cmdline_settings(&["neovide", "./foo.txt", "./bar.md", "--grid=420x240"]);
let restart_details = RestartDetails {
progpath: "nvim".to_string(),
argv: vec!["nvim".to_string(), "--clean".to_string()],
};
let (_, args) = build_restart_command_parts(&restart_details, &cmdline_settings);
assert_eq!(args, vec!["--embed", "-p", "./foo.txt", "./bar.md"]);
}
#[test]
fn build_restart_command_parts_keeps_embed_before_restart_args_for_server_mode() {
let cmdline_settings = parse_cmdline_settings(&["neovide", "--server", "127.0.0.1:7777"]);
let restart_details = RestartDetails {
progpath: "nvim".to_string(),
argv: vec!["nvim".to_string(), "-p".to_string(), "foo.txt".to_string()],
};
let (program, args) = build_restart_command_parts(&restart_details, &cmdline_settings);
assert_eq!(program, "nvim");
assert_eq!(args, vec!["--embed", "-p", "foo.txt"]);
}
#[test]
fn command_cwd_prefers_override() {
let cmdline_settings = parse_cmdline_settings(&["neovide", "--chdir", "/random/path"]);
@ -386,31 +324,4 @@ mod tests {
assert_eq!(command_cwd(&cmdline_settings, None), Some(PathBuf::from("/random/path")));
}
#[test]
fn create_restart_nvim_command_prefers_route_cwd() {
let settings = parse_settings(&["neovide", "--chdir", "/cmdline/cwd"]);
let restart_details = RestartDetails {
progpath: "nvim".to_string(),
argv: vec!["nvim".to_string(), "--clean".to_string()],
};
let command =
create_restart_nvim_command(&settings, &restart_details, Some(Path::new("/route/cwd")));
assert_eq!(command.as_std().get_current_dir(), Some(Path::new("/route/cwd")));
}
#[test]
fn create_restart_nvim_command_falls_back_to_cmdline_cwd() {
let settings = parse_settings(&["neovide", "--chdir", "/cmdline/cwd"]);
let restart_details = RestartDetails {
progpath: "nvim".to_string(),
argv: vec!["nvim".to_string(), "--clean".to_string()],
};
let command = create_restart_nvim_command(&settings, &restart_details, None);
assert_eq!(command.as_std().get_current_dir(), Some(Path::new("/cmdline/cwd")));
}
}

View file

@ -42,10 +42,9 @@ use tokio::{
};
use winit::event_loop::EventLoopProxy;
pub use command::create_blocking_nvim_command;
use command::create_restart_nvim_command;
#[cfg(test)]
pub use command::create_tokio_nvim_command;
pub use command::{OpenArgs, OpenMode, create_blocking_nvim_command};
pub use events::*;
pub use restart::RestartDetails;
pub use session::NeovimWriter;
@ -98,9 +97,10 @@ async fn neovim_instance(
settings: &Settings,
restart: Option<&RestartDetails>,
cwd: Option<&Path>,
mode: OpenMode,
) -> Result<NeovimInstance> {
if let Some(info) = restart {
return Ok(NeovimInstance::Embedded(create_restart_nvim_command(settings, info, cwd)));
return Ok(NeovimInstance::Server { address: info.listen_addr.clone() });
}
if let Some(address) = settings.get::<CmdLineSettings>().server {
@ -108,7 +108,12 @@ async fn neovim_instance(
}
let cmdline_settings = settings.get::<CmdLineSettings>();
Ok(NeovimInstance::Embedded(command::create_tokio_nvim_command(&cmdline_settings, true, cwd)))
Ok(NeovimInstance::Embedded(command::create_tokio_nvim_command(
&cmdline_settings,
true,
cwd,
mode,
)))
}
pub async fn show_error_message(
@ -135,6 +140,7 @@ pub async fn show_error_message(
nvim.echo(prepared_lines, true, nvim_dict! {}).await
}
#[allow(clippy::too_many_arguments)]
async fn create_neovim_session(
route_id: RouteId,
handler: NeovimHandler,
@ -143,8 +149,9 @@ async fn create_neovim_session(
background: &str,
restart_details: Option<&RestartDetails>,
cwd: Option<&Path>,
mode: OpenMode,
) -> Result<NeovimSession> {
let neovim_instance = neovim_instance(settings.as_ref(), restart_details, cwd).await?;
let neovim_instance = neovim_instance(settings.as_ref(), restart_details, cwd, mode).await?;
#[allow(unused_mut)]
let mut session = NeovimSession::new(neovim_instance, handler.clone())
.await
@ -163,8 +170,7 @@ async fn create_neovim_session(
let cmdline_settings = settings.get::<CmdLineSettings>();
let remote =
cmdline_settings.wsl || (cmdline_settings.server.is_some() && restart_details.is_none());
let remote = cmdline_settings.wsl || cmdline_settings.server.is_some();
// This is too verbose to keep enabled all the time
// log::info!("Api information {:#?}", api_information);
setup_neovide_specific_state(&session.neovim, remote, &api_information, &settings).await?;
@ -206,14 +212,15 @@ async fn create_neovim_session(
// Triggers loading the user config
let grid_size = grid_size.map_or(DEFAULT_GRID_SIZE, |v| clamped_grid_size(&v));
let res = session
session
.neovim
.ui_attach(grid_size.width as i64, grid_size.height as i64, &options)
.await
.context("Could not attach ui to neovim process");
.context("Could not attach ui to neovim process")?;
info!("Neovim process attached");
res.map(|()| session)
Ok(session)
}
async fn run(route_id: RouteId, session: NeovimSession, proxy: EventLoopProxy<EventPayload>) {
@ -327,6 +334,7 @@ impl NeovimRuntime {
settings: Arc<Settings>,
config: &Config,
cwd: Option<&Path>,
mode: OpenMode,
) -> Result<NeovimHandler> {
let mut colorscheme_stream = self.colorscheme_stream();
let editor_handler = start_editor_handler(
@ -352,6 +360,7 @@ impl NeovimRuntime {
&initial_background,
None,
cwd,
mode,
)) {
Ok(session) => session,
Err(err) => {
@ -397,6 +406,7 @@ impl NeovimRuntime {
&background,
Some(&restart_details),
cwd,
OpenMode::None,
))?;
self.runtime().spawn(run(route_id, session, event_loop_proxy));

View file

@ -2,24 +2,42 @@ use rmpv::Value;
#[derive(Clone, Debug, PartialEq)]
pub struct RestartDetails {
pub progpath: String,
pub argv: Vec<String>,
pub listen_addr: String,
}
impl RestartDetails {
pub fn from_values(arguments: &[Value]) -> Option<Self> {
if arguments.len() < 2 {
return None;
match arguments {
[listen_addr] => Some(Self { listen_addr: listen_addr.as_str()?.to_string() }),
_ => None,
}
let progpath = arguments[0].as_str()?.to_string();
let argv = arguments
.get(1)?
.as_array()?
.iter()
.filter_map(|value| value.as_str().map(|s| s.to_string()))
.collect::<Vec<_>>();
Some(Self { progpath, argv })
}
}
#[cfg(test)]
mod tests {
use super::RestartDetails;
use rmpv::Value;
#[test]
fn parses_listen_addr_restart_payload() {
let details = RestartDetails::from_values(&[Value::from("/tmp/nvim.sock")]);
assert_eq!(details, Some(RestartDetails { listen_addr: "/tmp/nvim.sock".to_string() }));
}
#[test]
fn rejects_restart_payload_with_extra_arguments() {
let details =
RestartDetails::from_values(&[Value::from("/tmp/nvim.sock"), Value::from("echo 1")]);
assert_eq!(details, None);
}
#[test]
fn rejects_restart_payload_with_invalid_type() {
let details = RestartDetails::from_values(&[Value::from("/tmp/nvim.sock"), Value::from(1)]);
assert_eq!(details, None);
}
}

View file

@ -263,7 +263,7 @@ mod tests {
use super::*;
use crate::{
bridge::{
create_tokio_nvim_command,
OpenMode, create_tokio_nvim_command,
session::{NeovimInstance, NeovimSession},
},
cmd_line::CmdLineSettings,
@ -369,7 +369,7 @@ mod tests {
settings.set::<CmdLineSettings>(&CmdLineSettings::default());
let cmdline_settings = settings.get::<CmdLineSettings>();
let command = create_tokio_nvim_command(&cmdline_settings, true, None);
let command = create_tokio_nvim_command(&cmdline_settings, true, None, OpenMode::Startup);
let instance = NeovimInstance::Embedded(command);
let NeovimSession { neovim: nvim, .. } = NeovimSession::new(instance, NeovimHandler())
.await

View file

@ -5,7 +5,7 @@ use std::{
#[cfg(target_os = "macos")]
use {
crate::bridge::{send_or_queue_file_drop, set_active_route_handler},
crate::bridge::{OpenArgs, send_or_queue_file_drop, set_active_route_handler},
crate::utils::resolve_relative_path,
std::path::Path,
};
@ -260,14 +260,31 @@ impl Application {
event_loop: &ActiveEventLoop,
new_window: bool,
cwd: Option<&Path>,
args: OpenArgs,
) {
if new_window {
self.window_wrapper.try_create_window(event_loop, &self.proxy, cwd);
self.mark_should_render_all();
if !new_window {
self.activate_focused_route();
self.send_file_drops(args);
return;
}
self.activate_focused_route();
if self.settings.get::<CmdLineSettings>().server.is_some() {
self.window_wrapper.try_create_window(event_loop, &self.proxy, cwd, None);
self.mark_should_render_all();
self.send_file_drops(args);
return;
}
let open_args = (!args.files_to_open.is_empty()).then_some(args);
self.window_wrapper.try_create_window(event_loop, &self.proxy, cwd, open_args);
self.mark_should_render_all();
}
#[cfg(target_os = "macos")]
fn send_file_drops(&self, args: OpenArgs) {
for path in args.files_to_open {
send_or_queue_file_drop(path, Some(args.tabs));
}
}
fn handle_app_config_changed(&mut self, config: AppHotReloadConfigs) {
@ -360,7 +377,7 @@ impl Application {
#[cfg(feature = "profiling")]
self.aggregate_should_render().plot_tracy();
if self.create_window_allowed && self.window_wrapper.has_pending_window_creation() {
self.window_wrapper.try_create_window(event_loop, &self.proxy, None);
self.window_wrapper.try_create_window(event_loop, &self.proxy, None, None);
}
event_loop.set_control_flow(ControlFlow::WaitUntil(self.get_event_deadline()));
}
@ -671,12 +688,15 @@ impl ApplicationHandler<EventPayload> for Application {
UserEvent::OpenFiles { files, cwd, caller_cwd, tabs, new_window } => {
let cwd = cwd.as_deref().map(Path::new);
let caller_cwd = caller_cwd.as_deref().map(Path::new);
self.prepare_open_files(event_loop, new_window, cwd);
let open_args = OpenArgs {
files_to_open: files
.into_iter()
.map(|path| resolve_relative_path(&path, caller_cwd))
.collect(),
tabs,
};
for path in files {
let path = resolve_relative_path(&path, caller_cwd);
send_or_queue_file_drop(path, Some(tabs));
}
self.prepare_open_files(event_loop, new_window, cwd, open_args);
}
UserEvent::NeovimExited => {
let route_id = self.route_id_for_target(target);
@ -767,7 +787,7 @@ impl ApplicationHandler<EventPayload> for Application {
}
#[cfg(target_os = "macos")]
UserEvent::CreateWindow => {
self.window_wrapper.try_create_window(event_loop, &self.proxy, None);
self.window_wrapper.try_create_window(event_loop, &self.proxy, None, None);
self.sync_render_states();
self.mark_should_render_all();
}

View file

@ -41,8 +41,8 @@ use {
use crate::{
CmdLineSettings,
bridge::{
NeovimHandler, NeovimRuntime, ParallelCommand, RestartDetails, SerialCommand, send_ui,
set_active_route_handler, unregister_route_handler,
NeovimHandler, NeovimRuntime, OpenArgs, OpenMode, ParallelCommand, RestartDetails,
SerialCommand, send_ui, set_active_route_handler, unregister_route_handler,
},
clipboard::ClipboardHandle,
cmd_line::{GeometryArgs, MouseCursorIcon},
@ -282,6 +282,7 @@ impl WinitWindowWrapper {
self.settings.clone(),
&config,
None,
OpenMode::Startup,
)
.expect("Failed to launch neovim runtime");
@ -1390,6 +1391,7 @@ impl WinitWindowWrapper {
event_loop: &ActiveEventLoop,
proxy: &EventLoopProxy<EventPayload>,
cwd: Option<&Path>,
args: Option<OpenArgs>,
) {
let creating_initial_window = self.routes.is_empty();
let route_id = if creating_initial_window {
@ -1507,6 +1509,7 @@ impl WinitWindowWrapper {
self.settings.clone(),
&config,
cwd,
args.map_or(OpenMode::None, OpenMode::Args),
)
.expect("Failed to launch neovim runtime");