From 32f223ae753a3eb080df668e47ca102d9fb67842 Mon Sep 17 00:00:00 2001 From: Alexsander Falcucci Date: Tue, 31 Mar 2026 22:03:25 +0200 Subject: [PATCH] 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. --- src/bridge/command.rs | 215 ++++++++++------------------------- src/bridge/mod.rs | 30 +++-- src/bridge/restart.rs | 46 +++++--- src/settings/mod.rs | 4 +- src/window/application.rs | 44 +++++-- src/window/window_wrapper.rs | 7 +- 6 files changed, 154 insertions(+), 192 deletions(-) diff --git a/src/bridge/command.rs b/src/bridge/command.rs index 755675fd..8710aeea 100644 --- a/src/bridge/command.rs +++ b/src/bridge/command.rs @@ -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, + 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::(); - 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 (String, Vec) { 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 { + 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) { - 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) { - let mut args = details.argv.iter().skip(1).cloned().collect::>(); - prepend_embed_arg(&mut args); - (details.progpath.clone(), args) -} - -fn build_auto_open_args(cmdline_settings: &CmdLineSettings) -> Vec { - 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) { } } -fn prepend_embed_arg(args: &mut Vec) { - 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::() } - 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"))); - } } diff --git a/src/bridge/mod.rs b/src/bridge/mod.rs index 4def55f4..3c848d58 100644 --- a/src/bridge/mod.rs +++ b/src/bridge/mod.rs @@ -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 { 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::().server { @@ -108,7 +108,12 @@ async fn neovim_instance( } let cmdline_settings = settings.get::(); - 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 { - 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::(); - 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) { @@ -327,6 +334,7 @@ impl NeovimRuntime { settings: Arc, config: &Config, cwd: Option<&Path>, + mode: OpenMode, ) -> Result { 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)); diff --git a/src/bridge/restart.rs b/src/bridge/restart.rs index 0c059982..0f90ddb0 100644 --- a/src/bridge/restart.rs +++ b/src/bridge/restart.rs @@ -2,24 +2,42 @@ use rmpv::Value; #[derive(Clone, Debug, PartialEq)] pub struct RestartDetails { - pub progpath: String, - pub argv: Vec, + pub listen_addr: String, } impl RestartDetails { pub fn from_values(arguments: &[Value]) -> Option { - 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::>(); - - 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); } } diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 975a4992..1fa6f2a7 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -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::default()); let cmdline_settings = settings.get::(); - 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 diff --git a/src/window/application.rs b/src/window/application.rs index 26816887..c6e00905 100644 --- a/src/window/application.rs +++ b/src/window/application.rs @@ -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::().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 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 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(); } diff --git a/src/window/window_wrapper.rs b/src/window/window_wrapper.rs index 2816d2c6..398a1674 100644 --- a/src/window/window_wrapper.rs +++ b/src/window/window_wrapper.rs @@ -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, cwd: Option<&Path>, + args: Option, ) { 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");