From 3c53832141dac7ae67949e659086d31aafc8a81f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Thu, 29 Aug 2024 10:26:24 +0800 Subject: [PATCH 1/3] windows: Implement single instance (#15371) This PR implements a single instance mechanism using the `CreateEventW` function to create a mutex. If the identifier name begins with `Local`, the single instance applies only to processes under the same user. If the identifier begins with `Global`, it applies to all users. Additionally, I was thinking that perhaps we should integrate the single instance functionality into `gpui`. I believe applications developed using `gpui` would benefit from this feature. Furthermore, incorporating the single instance implementation into `gpui` would facilitate the `set_dock_menu` functionality. As I mentioned in #12068, the implementation of `set_dock_menu` on Windows depends on the single instance feature. When a user clicks the "dock menu", Windows will open a new application instance. To achieve behavior similar to macOS, we need to prevent the new instance from launching and instead pass the parameters to the existing instance. Any advice and suggestions are welcome. https://github.com/user-attachments/assets/c46f7e92-4411-4fa9-830e-383798a9dd93 Release Notes: - N/A --- Cargo.lock | 1 + crates/zed/Cargo.toml | 7 +++-- crates/zed/src/main.rs | 11 +++++++- crates/zed/src/zed.rs | 4 ++- crates/zed/src/zed/single_instance.rs | 39 +++++++++++++++++++++++++++ 5 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 crates/zed/src/zed/single_instance.rs diff --git a/Cargo.lock b/Cargo.lock index f2e3d128f2..e7b6bff119 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14212,6 +14212,7 @@ dependencies = [ "uuid", "vim", "welcome", + "windows 0.58.0", "winresource", "workspace", "zed_actions", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index c4647593e9..0fbdbe1ca7 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -67,7 +67,7 @@ log.workspace = true markdown_preview.workspace = true menu.workspace = true mimalloc = { version = "0.1", optional = true } -nix = {workspace = true, features = ["pthread", "signal"] } +nix = { workspace = true, features = ["pthread", "signal"] } node_runtime.workspace = true notifications.workspace = true outline.workspace = true @@ -99,7 +99,7 @@ tab_switcher.workspace = true supermaven.workspace = true task.workspace = true tasks_ui.workspace = true -time.workspace = true +time.workspace = true telemetry_events.workspace = true terminal_view.workspace = true theme.workspace = true @@ -114,6 +114,9 @@ welcome.workspace = true workspace.workspace = true zed_actions.workspace = true +[target.'cfg(target_os = "windows")'.dependencies] +windows.workspace = true + [target.'cfg(target_os = "windows")'.build-dependencies] winresource = "0.1" diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e23ed69584..d9ffb29c39 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -318,6 +318,15 @@ fn init_ui( } fn main() { + #[cfg(target_os = "windows")] + { + use zed::single_instance::*; + if !check_single_instance() { + println!("zed is already running"); + return; + } + } + let start_time = std::time::Instant::now(); menu::init(); zed_actions::init(); @@ -360,7 +369,7 @@ fn main() { } } } - #[cfg(not(target_os = "linux"))] + #[cfg(not(any(target_os = "linux", target_os = "windows")))] { use zed::only_instance::*; if ensure_only_instance() != IsOnlyInstance::Yes { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 1b9b8ee042..9e0d061884 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2,9 +2,11 @@ mod app_menus; pub mod inline_completion_registry; #[cfg(target_os = "linux")] pub(crate) mod linux_prompts; -#[cfg(not(target_os = "linux"))] +#[cfg(not(any(target_os = "linux", target_os = "windows")))] pub(crate) mod only_instance; mod open_listener; +#[cfg(target_os = "windows")] +pub(crate) mod single_instance; pub use app_menus::*; use assistant::PromptBuilder; diff --git a/crates/zed/src/zed/single_instance.rs b/crates/zed/src/zed/single_instance.rs new file mode 100644 index 0000000000..e8d32e7ed0 --- /dev/null +++ b/crates/zed/src/zed/single_instance.rs @@ -0,0 +1,39 @@ +use release_channel::ReleaseChannel; +use windows::{ + core::HSTRING, + Win32::{ + Foundation::{GetLastError, ERROR_ALREADY_EXISTS}, + System::Threading::CreateEventW, + }, +}; + +fn retrieve_app_instance_event_identifier() -> &'static str { + match *release_channel::RELEASE_CHANNEL { + ReleaseChannel::Dev => "Local\\Zed-Editor-Dev-Instance-Event", + ReleaseChannel::Nightly => "Local\\Zed-Editor-Nightly-Instance-Event", + ReleaseChannel::Preview => "Local\\Zed-Editor-Preview-Instance-Event", + ReleaseChannel::Stable => "Local\\Zed-Editor-Stable-Instance-Event", + } +} + +pub fn check_single_instance() -> bool { + if *db::ZED_STATELESS || *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev { + return true; + } + + check_single_instance_event() +} + +fn check_single_instance_event() -> bool { + unsafe { + CreateEventW( + None, + false, + false, + &HSTRING::from(retrieve_app_instance_event_identifier()), + ) + .expect("Unable to create instance sync event") + }; + let last_err = unsafe { GetLastError() }; + last_err != ERROR_ALREADY_EXISTS +} From e8dfc303147202b601495a0f946b0083fab90341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Thu, 29 Aug 2024 10:29:53 +0800 Subject: [PATCH 2/3] windows: Fix IME window position on Win10 (#15471) On Windows, different input methods use different APIs to set their window positions: - The Japanese input method on Windows 11 uses `ImmSetCandidateWindow`. - The Chinese input method on Windows 10 uses `ImmSetCompositionWindow`. - The Chinese input method on Windows 11 can use either. Therefore, this PR calls both functions to cover the various scenarios. Additionally, introduced a helper function `with_input_handler` to improve code readability. Release Notes: - N/A --- crates/gpui/src/platform/windows/events.rs | 171 ++++++++++++--------- 1 file changed, 101 insertions(+), 70 deletions(-) diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index c831b89dff..cc55601963 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -386,22 +386,18 @@ fn handle_char_msg( keystroke, is_held: lparam.0 & (0x1 << 30) > 0, }; - let dispatch_event_result = func(PlatformInput::KeyDown(event)); - let mut lock = state_ptr.state.borrow_mut(); - lock.callbacks.input = Some(func); + state_ptr.state.borrow_mut().callbacks.input = Some(func); + if dispatch_event_result.default_prevented || !dispatch_event_result.propagate { return Some(0); } let Some(ime_char) = ime_key else { return Some(1); }; - let Some(mut input_handler) = lock.input_handler.take() else { - return Some(1); - }; - drop(lock); - input_handler.replace_text_in_range(None, &ime_char); - state_ptr.state.borrow_mut().input_handler = Some(input_handler); + with_input_handler(&state_ptr, |input_handler| { + input_handler.replace_text_in_range(None, &ime_char); + }); Some(0) } @@ -581,33 +577,41 @@ fn handle_mouse_horizontal_wheel_msg( } } +fn retrieve_caret_position(state_ptr: &Rc) -> Option { + with_input_handler_and_scale_factor(state_ptr, |input_handler, scale_factor| { + let caret_range = input_handler.selected_text_range()?; + let caret_position = input_handler.bounds_for_range(caret_range)?; + Some(POINT { + // logical to physical + x: (caret_position.origin.x.0 * scale_factor) as i32, + y: (caret_position.origin.y.0 * scale_factor) as i32 + + ((caret_position.size.height.0 * scale_factor) as i32 / 2), + }) + }) +} + fn handle_ime_position(handle: HWND, state_ptr: Rc) -> Option { unsafe { - let mut lock = state_ptr.state.borrow_mut(); let ctx = ImmGetContext(handle); - let Some(mut input_handler) = lock.input_handler.take() else { - return Some(1); - }; - let scale_factor = lock.scale_factor; - drop(lock); - - let Some(caret_range) = input_handler.selected_text_range() else { - state_ptr.state.borrow_mut().input_handler = Some(input_handler); + let Some(caret_position) = retrieve_caret_position(&state_ptr) else { return Some(0); }; - let caret_position = input_handler.bounds_for_range(caret_range).unwrap(); - state_ptr.state.borrow_mut().input_handler = Some(input_handler); - let config = CANDIDATEFORM { - dwStyle: CFS_CANDIDATEPOS, - // logical to physical - ptCurrentPos: POINT { - x: (caret_position.origin.x.0 * scale_factor) as i32, - y: (caret_position.origin.y.0 * scale_factor) as i32 - + ((caret_position.size.height.0 * scale_factor) as i32 / 2), - }, - ..Default::default() - }; - ImmSetCandidateWindow(ctx, &config as _).ok().log_err(); + { + let config = COMPOSITIONFORM { + dwStyle: CFS_POINT, + ptCurrentPos: caret_position, + ..Default::default() + }; + ImmSetCompositionWindow(ctx, &config as _).ok().log_err(); + } + { + let config = CANDIDATEFORM { + dwStyle: CFS_CANDIDATEPOS, + ptCurrentPos: caret_position, + ..Default::default() + }; + ImmSetCandidateWindow(ctx, &config as _).ok().log_err(); + } ImmReleaseContext(handle, ctx).ok().log_err(); Some(0) } @@ -617,35 +621,46 @@ fn handle_ime_composition( handle: HWND, lparam: LPARAM, state_ptr: Rc, +) -> Option { + let ctx = unsafe { ImmGetContext(handle) }; + let result = handle_ime_composition_inner(ctx, lparam, state_ptr); + unsafe { ImmReleaseContext(handle, ctx).ok().log_err() }; + result +} + +fn handle_ime_composition_inner( + ctx: HIMC, + lparam: LPARAM, + state_ptr: Rc, ) -> Option { let mut ime_input = None; if lparam.0 as u32 & GCS_COMPSTR.0 > 0 { - let (comp_string, string_len) = parse_ime_compostion_string(handle)?; - let mut input_handler = state_ptr.state.borrow_mut().input_handler.take()?; - input_handler.replace_and_mark_text_in_range( - None, - &comp_string, - Some(string_len..string_len), - ); - state_ptr.state.borrow_mut().input_handler = Some(input_handler); + let (comp_string, string_len) = parse_ime_compostion_string(ctx)?; + with_input_handler(&state_ptr, |input_handler| { + input_handler.replace_and_mark_text_in_range( + None, + &comp_string, + Some(string_len..string_len), + ); + })?; ime_input = Some(comp_string); } if lparam.0 as u32 & GCS_CURSORPOS.0 > 0 { let comp_string = &ime_input?; - let caret_pos = retrieve_composition_cursor_position(handle); - let mut input_handler = state_ptr.state.borrow_mut().input_handler.take()?; - input_handler.replace_and_mark_text_in_range(None, comp_string, Some(caret_pos..caret_pos)); - state_ptr.state.borrow_mut().input_handler = Some(input_handler); + let caret_pos = retrieve_composition_cursor_position(ctx); + with_input_handler(&state_ptr, |input_handler| { + input_handler.replace_and_mark_text_in_range( + None, + comp_string, + Some(caret_pos..caret_pos), + ); + })?; } if lparam.0 as u32 & GCS_RESULTSTR.0 > 0 { - let comp_result = parse_ime_compostion_result(handle)?; - let mut lock = state_ptr.state.borrow_mut(); - let Some(mut input_handler) = lock.input_handler.take() else { - return Some(1); - }; - drop(lock); - input_handler.replace_text_in_range(None, &comp_result); - state_ptr.state.borrow_mut().input_handler = Some(input_handler); + let comp_result = parse_ime_compostion_result(ctx)?; + with_input_handler(&state_ptr, |input_handler| { + input_handler.replace_text_in_range(None, &comp_result); + })?; return Some(0); } // currently, we don't care other stuff @@ -1218,11 +1233,10 @@ fn parse_char_msg_keystroke(wparam: WPARAM) -> Option { } } -fn parse_ime_compostion_string(handle: HWND) -> Option<(String, usize)> { +fn parse_ime_compostion_string(ctx: HIMC) -> Option<(String, usize)> { unsafe { - let ctx = ImmGetContext(handle); let string_len = ImmGetCompositionStringW(ctx, GCS_COMPSTR, None, 0); - let result = if string_len >= 0 { + if string_len >= 0 { let mut buffer = vec![0u8; string_len as usize + 2]; ImmGetCompositionStringW( ctx, @@ -1238,26 +1252,19 @@ fn parse_ime_compostion_string(handle: HWND) -> Option<(String, usize)> { Some((string, string_len as usize / 2)) } else { None - }; - ImmReleaseContext(handle, ctx).ok().log_err(); - result + } } } -fn retrieve_composition_cursor_position(handle: HWND) -> usize { - unsafe { - let ctx = ImmGetContext(handle); - let ret = ImmGetCompositionStringW(ctx, GCS_CURSORPOS, None, 0); - ImmReleaseContext(handle, ctx).ok().log_err(); - ret as usize - } +#[inline] +fn retrieve_composition_cursor_position(ctx: HIMC) -> usize { + unsafe { ImmGetCompositionStringW(ctx, GCS_CURSORPOS, None, 0) as usize } } -fn parse_ime_compostion_result(handle: HWND) -> Option { +fn parse_ime_compostion_result(ctx: HIMC) -> Option { unsafe { - let ctx = ImmGetContext(handle); let string_len = ImmGetCompositionStringW(ctx, GCS_RESULTSTR, None, 0); - let result = if string_len >= 0 { + if string_len >= 0 { let mut buffer = vec![0u8; string_len as usize + 2]; ImmGetCompositionStringW( ctx, @@ -1273,9 +1280,7 @@ fn parse_ime_compostion_result(handle: HWND) -> Option { Some(string) } else { None - }; - ImmReleaseContext(handle, ctx).ok().log_err(); - result + } } } @@ -1323,3 +1328,29 @@ pub(crate) fn current_modifiers() -> Modifiers { function: false, } } + +fn with_input_handler(state_ptr: &Rc, f: F) -> Option +where + F: FnOnce(&mut PlatformInputHandler) -> R, +{ + let mut input_handler = state_ptr.state.borrow_mut().input_handler.take()?; + let result = f(&mut input_handler); + state_ptr.state.borrow_mut().input_handler = Some(input_handler); + Some(result) +} + +fn with_input_handler_and_scale_factor( + state_ptr: &Rc, + f: F, +) -> Option +where + F: FnOnce(&mut PlatformInputHandler, f32) -> Option, +{ + let mut lock = state_ptr.state.borrow_mut(); + let mut input_handler = lock.input_handler.take()?; + let scale_factor = lock.scale_factor; + drop(lock); + let result = f(&mut input_handler, scale_factor); + state_ptr.state.borrow_mut().input_handler = Some(input_handler); + result +} From 462808e5b0fbdce5413256d9350f8bfcec0d56a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Thu, 29 Aug 2024 10:32:15 +0800 Subject: [PATCH 3/3] windows: Fix extensions couldn't start if the path contained spaces (#15489) Closes #15441 . Fixed the issue where extensions couldn't start if the path contained spaces. Additionally, this PR introduces the `node_environment_path` function to obtain the PATH environment variable which includes the node path. Release Notes: - N/A --- crates/languages/src/tailwind.rs | 21 +++++---------- crates/node_runtime/src/node_runtime.rs | 36 ++++++++++++++++--------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index e4486efe8e..39ccc8afa1 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -23,10 +23,16 @@ const SERVER_PATH: &str = "node_modules/.bin/tailwindcss-language-server.ps1"; #[cfg(not(target_os = "windows"))] const SERVER_PATH: &str = "node_modules/.bin/tailwindcss-language-server"; +#[cfg(not(target_os = "windows"))] fn server_binary_arguments(server_path: &Path) -> Vec { vec![server_path.into(), "--stdio".into()] } +#[cfg(target_os = "windows")] +fn server_binary_arguments(server_path: &Path) -> Vec { + vec!["-File".into(), server_path.into(), "--stdio".into()] +} + pub struct TailwindLspAdapter { node: Arc, } @@ -113,20 +119,7 @@ impl LspAdapter for TailwindLspAdapter { #[cfg(target_os = "windows")] { - let mut env_path = vec![self - .node - .binary_path() - .await? - .parent() - .expect("invalid node binary path") - .to_path_buf()]; - - if let Some(existing_path) = std::env::var_os("PATH") { - let mut paths = std::env::split_paths(&existing_path).collect::>(); - env_path.append(&mut paths); - } - - let env_path = std::env::join_paths(env_path)?; + let env_path = self.node.node_environment_path().await?; let mut env = HashMap::default(); env.insert("PATH".to_string(), env_path.to_string_lossy().to_string()); diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index cc4bacc54d..8ea392d1a7 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -10,6 +10,7 @@ use semver::Version; use serde::Deserialize; use smol::io::BufReader; use smol::{fs, lock::Mutex, process::Command}; +use std::ffi::OsString; use std::io; use std::process::{Output, Stdio}; use std::{ @@ -55,6 +56,7 @@ pub struct NpmInfoDistTags { #[async_trait::async_trait] pub trait NodeRuntime: Send + Sync { async fn binary_path(&self) -> Result; + async fn node_environment_path(&self) -> Result; async fn run_npm_subcommand( &self, @@ -216,6 +218,22 @@ impl NodeRuntime for RealNodeRuntime { Ok(installation_path.join(NODE_PATH)) } + async fn node_environment_path(&self) -> Result { + let installation_path = self.install_if_needed().await?; + let node_binary = installation_path.join(NODE_PATH); + let mut env_path = vec![node_binary + .parent() + .expect("invalid node binary path") + .to_path_buf()]; + + if let Some(existing_path) = std::env::var_os("PATH") { + let mut paths = std::env::split_paths(&existing_path).collect::>(); + env_path.append(&mut paths); + } + + Ok(std::env::join_paths(env_path).context("failed to create PATH env variable")?) + } + async fn run_npm_subcommand( &self, directory: Option<&Path>, @@ -224,21 +242,9 @@ impl NodeRuntime for RealNodeRuntime { ) -> Result { let attempt = || async move { let installation_path = self.install_if_needed().await?; - let node_binary = installation_path.join(NODE_PATH); let npm_file = installation_path.join(NPM_PATH); - let mut env_path = vec![node_binary - .parent() - .expect("invalid node binary path") - .to_path_buf()]; - - if let Some(existing_path) = std::env::var_os("PATH") { - let mut paths = std::env::split_paths(&existing_path).collect::>(); - env_path.append(&mut paths); - } - - let env_path = - std::env::join_paths(env_path).context("failed to create PATH env variable")?; + let env_path = self.node_environment_path().await?; if smol::fs::metadata(&node_binary).await.is_err() { return Err(anyhow!("missing node binary file")); @@ -423,6 +429,10 @@ impl NodeRuntime for FakeNodeRuntime { unreachable!() } + async fn node_environment_path(&self) -> anyhow::Result { + unreachable!() + } + async fn run_npm_subcommand( &self, _: Option<&Path>,