linux: Fix IME panel position while enumerating input methods (#12495)

Fernando Tagawa and Mikayla Maki created

Release Notes:

- N/A

This updates the IME position every time the selection changes, this is
probably only useful when you enumerate languages with your IME.

TODO:
- ~There is a rare chance that the ime panel is not updated because the
window input handler is None.~
- ~Update IME panel in vim mode.~
- ~Update IME panel when leaving Buffer search input.~

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>

Change summary

crates/editor/src/editor.rs                      | 31 ++++++++---
crates/gpui/examples/input.rs                    | 11 +++
crates/gpui/src/input.rs                         | 19 +++++-
crates/gpui/src/platform.rs                      | 37 +++++++++++++-
crates/gpui/src/platform/linux/wayland/client.rs | 18 +++++++
crates/gpui/src/platform/linux/wayland/window.rs | 15 +++++
crates/gpui/src/platform/linux/x11/client.rs     | 46 ++++++++++++++++-
crates/gpui/src/platform/linux/x11/window.rs     | 11 +++
crates/gpui/src/platform/mac/window.rs           | 17 +++++-
crates/gpui/src/platform/test/window.rs          |  2 
crates/gpui/src/platform/windows/events.rs       |  5 +
crates/gpui/src/platform/windows/window.rs       |  4 +
crates/gpui/src/window.rs                        | 12 ++++
crates/search/src/buffer_search.rs               | 14 +++-
crates/terminal_view/src/terminal_element.rs     | 15 ++++-
crates/terminal_view/src/terminal_view.rs        |  6 +
crates/workspace/src/workspace.rs                |  7 ++
crates/zed/src/main.rs                           | 23 ++++----
crates/zed/src/zed.rs                            |  6 +-
crates/zed/src/zed/mac_only_instance.rs          |  0 
crates/zed/src/zed/windows_only_instance.rs      |  0 
21 files changed, 244 insertions(+), 55 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -76,8 +76,8 @@ use gpui::{
     FocusOutEvent, FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText,
     KeyContext, ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render,
     SharedString, Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle,
-    UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext,
-    WeakFocusHandle, WeakView, WindowContext,
+    UTF16Selection, UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler,
+    VisualContext, WeakFocusHandle, WeakView, WindowContext,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
@@ -2365,6 +2365,8 @@ impl Editor {
         show_completions: bool,
         cx: &mut ViewContext<Self>,
     ) {
+        cx.invalidate_character_coordinates();
+
         // Copy selections to primary selection buffer
         #[cfg(target_os = "linux")]
         if local {
@@ -11839,12 +11841,12 @@ impl Editor {
 
         let snapshot = buffer.read(cx).snapshot();
         let range = self
-            .selected_text_range(cx)
-            .and_then(|selected_range| {
-                if selected_range.is_empty() {
+            .selected_text_range(false, cx)
+            .and_then(|selection| {
+                if selection.range.is_empty() {
                     None
                 } else {
-                    Some(selected_range)
+                    Some(selection.range)
                 }
             })
             .unwrap_or_else(|| 0..snapshot.len());
@@ -12796,15 +12798,24 @@ impl ViewInputHandler for Editor {
         )
     }
 
-    fn selected_text_range(&mut self, cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
+    fn selected_text_range(
+        &mut self,
+        ignore_disabled_input: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<UTF16Selection> {
         // Prevent the IME menu from appearing when holding down an alphabetic key
         // while input is disabled.
-        if !self.input_enabled {
+        if !ignore_disabled_input && !self.input_enabled {
             return None;
         }
 
-        let range = self.selections.newest::<OffsetUtf16>(cx).range();
-        Some(range.start.0..range.end.0)
+        let selection = self.selections.newest::<OffsetUtf16>(cx);
+        let range = selection.range();
+
+        Some(UTF16Selection {
+            range: range.start.0..range.end.0,
+            reversed: selection.reversed,
+        })
     }
 
     fn marked_text_range(&self, cx: &mut ViewContext<Self>) -> Option<Range<usize>> {

crates/gpui/examples/input.rs 🔗

@@ -225,8 +225,15 @@ impl ViewInputHandler for TextInput {
         Some(self.content[range].to_string())
     }
 
-    fn selected_text_range(&mut self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
-        Some(self.range_to_utf16(&self.selected_range))
+    fn selected_text_range(
+        &mut self,
+        _ignore_disabled_input: bool,
+        _cx: &mut ViewContext<Self>,
+    ) -> Option<UTF16Selection> {
+        Some(UTF16Selection {
+            range: self.range_to_utf16(&self.selected_range),
+            reversed: self.selection_reversed,
+        })
     }
 
     fn marked_text_range(&self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {

crates/gpui/src/input.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{Bounds, InputHandler, Pixels, View, ViewContext, WindowContext};
+use crate::{Bounds, InputHandler, Pixels, UTF16Selection, View, ViewContext, WindowContext};
 use std::ops::Range;
 
 /// Implement this trait to allow views to handle textual input when implementing an editor, field, etc.
@@ -13,7 +13,11 @@ pub trait ViewInputHandler: 'static + Sized {
         -> Option<String>;
 
     /// See [`InputHandler::selected_text_range`] for details
-    fn selected_text_range(&mut self, cx: &mut ViewContext<Self>) -> Option<Range<usize>>;
+    fn selected_text_range(
+        &mut self,
+        ignore_disabled_input: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<UTF16Selection>;
 
     /// See [`InputHandler::marked_text_range`] for details
     fn marked_text_range(&self, cx: &mut ViewContext<Self>) -> Option<Range<usize>>;
@@ -68,9 +72,14 @@ impl<V: 'static> ElementInputHandler<V> {
 }
 
 impl<V: ViewInputHandler> InputHandler for ElementInputHandler<V> {
-    fn selected_text_range(&mut self, cx: &mut WindowContext) -> Option<Range<usize>> {
-        self.view
-            .update(cx, |view, cx| view.selected_text_range(cx))
+    fn selected_text_range(
+        &mut self,
+        ignore_disabled_input: bool,
+        cx: &mut WindowContext,
+    ) -> Option<UTF16Selection> {
+        self.view.update(cx, |view, cx| {
+            view.selected_text_range(ignore_disabled_input, cx)
+        })
     }
 
     fn marked_text_range(&mut self, cx: &mut WindowContext) -> Option<Range<usize>> {

crates/gpui/src/platform.rs 🔗

@@ -383,6 +383,8 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
     fn gpu_specs(&self) -> Option<GPUSpecs>;
     fn fps(&self) -> Option<f32>;
 
+    fn update_ime_position(&self, _bounds: Bounds<Pixels>);
+
     #[cfg(any(test, feature = "test-support"))]
     fn as_test(&mut self) -> Option<&mut TestWindow> {
         None
@@ -526,9 +528,9 @@ impl PlatformInputHandler {
         Self { cx, handler }
     }
 
-    fn selected_text_range(&mut self) -> Option<Range<usize>> {
+    fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
         self.cx
-            .update(|cx| self.handler.selected_text_range(cx))
+            .update(|cx| self.handler.selected_text_range(ignore_disabled_input, cx))
             .ok()
             .flatten()
     }
@@ -589,6 +591,31 @@ impl PlatformInputHandler {
     pub(crate) fn dispatch_input(&mut self, input: &str, cx: &mut WindowContext) {
         self.handler.replace_text_in_range(None, input, cx);
     }
+
+    pub fn selected_bounds(&mut self, cx: &mut WindowContext) -> Option<Bounds<Pixels>> {
+        let Some(selection) = self.handler.selected_text_range(true, cx) else {
+            return None;
+        };
+        self.handler.bounds_for_range(
+            if selection.reversed {
+                selection.range.start..selection.range.start
+            } else {
+                selection.range.end..selection.range.end
+            },
+            cx,
+        )
+    }
+}
+
+/// A struct representing a selection in a text buffer, in UTF16 characters.
+/// This is different from a range because the head may be before the tail.
+pub struct UTF16Selection {
+    /// The range of text in the document this selection corresponds to
+    /// in UTF16 characters.
+    pub range: Range<usize>,
+    /// Whether the head of this selection is at the start (true), or end (false)
+    /// of the range
+    pub reversed: bool,
 }
 
 /// Zed's interface for handling text input from the platform's IME system
@@ -600,7 +627,11 @@ pub trait InputHandler: 'static {
     /// Corresponds to [selectedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438242-selectedrange)
     ///
     /// Return value is in terms of UTF-16 characters, from 0 to the length of the document
-    fn selected_text_range(&mut self, cx: &mut WindowContext) -> Option<Range<usize>>;
+    fn selected_text_range(
+        &mut self,
+        ignore_disabled_input: bool,
+        cx: &mut WindowContext,
+    ) -> Option<UTF16Selection>;
 
     /// Get the range of the currently marked text, if any
     /// Corresponds to [markedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438250-markedrange)

crates/gpui/src/platform/linux/wayland/client.rs 🔗

@@ -312,6 +312,23 @@ impl WaylandClientStatePtr {
         }
     }
 
+    pub fn update_ime_position(&self, bounds: Bounds<Pixels>) {
+        let client = self.get_client();
+        let mut state = client.borrow_mut();
+        if state.composing || state.text_input.is_none() {
+            return;
+        }
+
+        let text_input = state.text_input.as_ref().unwrap();
+        text_input.set_cursor_rectangle(
+            bounds.origin.x.0 as i32,
+            bounds.origin.y.0 as i32,
+            bounds.size.width.0 as i32,
+            bounds.size.height.0 as i32,
+        );
+        text_input.commit();
+    }
+
     pub fn drop_window(&self, surface_id: &ObjectId) {
         let mut client = self.get_client();
         let mut state = client.borrow_mut();
@@ -1353,6 +1370,7 @@ impl Dispatch<zwp_text_input_v3::ZwpTextInputV3, ()> for WaylandClientStatePtr {
                         }
                     }
                 } else {
+                    state.composing = false;
                     drop(state);
                     window.handle_ime(ImeInput::DeleteText);
                 }

crates/gpui/src/platform/linux/wayland/window.rs 🔗

@@ -622,8 +622,12 @@ impl WaylandWindowStatePtr {
         let mut bounds: Option<Bounds<Pixels>> = None;
         if let Some(mut input_handler) = state.input_handler.take() {
             drop(state);
-            if let Some(range) = input_handler.selected_text_range() {
-                bounds = input_handler.bounds_for_range(range);
+            if let Some(selection) = input_handler.selected_text_range(true) {
+                bounds = input_handler.bounds_for_range(if selection.reversed {
+                    selection.range.start..selection.range.start
+                } else {
+                    selection.range.end..selection.range.end
+                });
             }
             self.state.borrow_mut().input_handler = Some(input_handler);
         }
@@ -1006,6 +1010,13 @@ impl PlatformWindow for WaylandWindow {
         }
     }
 
+    fn update_ime_position(&self, bounds: Bounds<Pixels>) {
+        let state = self.borrow();
+        let client = state.client.clone();
+        drop(state);
+        client.update_ime_position(bounds);
+    }
+
     fn gpu_specs(&self) -> Option<GPUSpecs> {
         self.borrow().renderer.gpu_specs().into()
     }

crates/gpui/src/platform/linux/x11/client.rs 🔗

@@ -148,8 +148,12 @@ pub struct X11ClientState {
 pub struct X11ClientStatePtr(pub Weak<RefCell<X11ClientState>>);
 
 impl X11ClientStatePtr {
+    fn get_client(&self) -> X11Client {
+        X11Client(self.0.upgrade().expect("client already dropped"))
+    }
+
     pub fn drop_window(&self, x_window: u32) {
-        let client = X11Client(self.0.upgrade().expect("client already dropped"));
+        let client = self.get_client();
         let mut state = client.0.borrow_mut();
 
         if let Some(window_ref) = state.windows.remove(&x_window) {
@@ -167,6 +171,42 @@ impl X11ClientStatePtr {
             state.common.signal.stop();
         }
     }
+
+    pub fn update_ime_position(&self, bounds: Bounds<Pixels>) {
+        let client = self.get_client();
+        let mut state = client.0.borrow_mut();
+        if state.composing || state.ximc.is_none() {
+            return;
+        }
+
+        let mut ximc = state.ximc.take().unwrap();
+        let xim_handler = state.xim_handler.take().unwrap();
+        let ic_attributes = ximc
+            .build_ic_attributes()
+            .push(
+                xim::AttributeName::InputStyle,
+                xim::InputStyle::PREEDIT_CALLBACKS
+                    | xim::InputStyle::STATUS_NOTHING
+                    | xim::InputStyle::PREEDIT_POSITION,
+            )
+            .push(xim::AttributeName::ClientWindow, xim_handler.window)
+            .push(xim::AttributeName::FocusWindow, xim_handler.window)
+            .nested_list(xim::AttributeName::PreeditAttributes, |b| {
+                b.push(
+                    xim::AttributeName::SpotLocation,
+                    xim::Point {
+                        x: u32::from(bounds.origin.x + bounds.size.width) as i16,
+                        y: u32::from(bounds.origin.y + bounds.size.height) as i16,
+                    },
+                );
+            })
+            .build();
+        let _ = ximc
+            .set_ic_values(xim_handler.im_id, xim_handler.ic_id, ic_attributes)
+            .log_err();
+        state.ximc = Some(ximc);
+        state.xim_handler = Some(xim_handler);
+    }
 }
 
 #[derive(Clone)]
@@ -1029,13 +1069,13 @@ impl X11Client {
 
     fn xim_handle_preedit(&self, window: xproto::Window, text: String) -> Option<()> {
         let window = self.get_window(window).unwrap();
-        window.handle_ime_preedit(text);
 
         let mut state = self.0.borrow_mut();
         let mut ximc = state.ximc.take().unwrap();
         let mut xim_handler = state.xim_handler.take().unwrap();
-        state.composing = true;
+        state.composing = !text.is_empty();
         drop(state);
+        window.handle_ime_preedit(text);
 
         if let Some(area) = window.get_ime_area() {
             let ic_attributes = ximc

crates/gpui/src/platform/linux/x11/window.rs 🔗

@@ -873,8 +873,8 @@ impl X11WindowStatePtr {
         let mut bounds: Option<Bounds<Pixels>> = None;
         if let Some(mut input_handler) = state.input_handler.take() {
             drop(state);
-            if let Some(range) = input_handler.selected_text_range() {
-                bounds = input_handler.bounds_for_range(range);
+            if let Some(selection) = input_handler.selected_text_range(true) {
+                bounds = input_handler.bounds_for_range(selection.range);
             }
             let mut state = self.state.borrow_mut();
             state.input_handler = Some(input_handler);
@@ -1396,6 +1396,13 @@ impl PlatformWindow for X11Window {
         }
     }
 
+    fn update_ime_position(&self, bounds: Bounds<Pixels>) {
+        let mut state = self.0.state.borrow_mut();
+        let client = state.client.clone();
+        drop(state);
+        client.update_ime_position(bounds);
+    }
+
     fn gpu_specs(&self) -> Option<GPUSpecs> {
         self.0.state.borrow().renderer.gpu_specs().into()
     }

crates/gpui/src/platform/mac/window.rs 🔗

@@ -1120,6 +1120,13 @@ impl PlatformWindow for MacWindow {
         None
     }
 
+    fn update_ime_position(&self, _bounds: Bounds<Pixels>) {
+        unsafe {
+            let input_context: id = msg_send![class!(NSTextInputContext), currentInputContext];
+            let _: () = msg_send![input_context, invalidateCharacterCoordinates];
+        }
+    }
+
     fn fps(&self) -> Option<f32> {
         Some(self.0.lock().renderer.fps())
     }
@@ -1311,7 +1318,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
                     // enter it will still swallow certain keys (e.g. 'f', 'j') and not others
                     // (e.g. 'n'). This is a problem for certain kinds of views, like the terminal.
                     with_input_handler(this, |input_handler| {
-                        if input_handler.selected_text_range().is_none() {
+                        if input_handler.selected_text_range(false).is_none() {
                             handled = true;
                             input_handler.replace_text_in_range(None, &text)
                         }
@@ -1683,10 +1690,12 @@ extern "C" fn marked_range(this: &Object, _: Sel) -> NSRange {
 }
 
 extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange {
-    let selected_range_result =
-        with_input_handler(this, |input_handler| input_handler.selected_text_range()).flatten();
+    let selected_range_result = with_input_handler(this, |input_handler| {
+        input_handler.selected_text_range(false)
+    })
+    .flatten();
 
-    selected_range_result.map_or(NSRange::invalid(), |range| range.into())
+    selected_range_result.map_or(NSRange::invalid(), |selection| selection.range.into())
 }
 
 extern "C" fn first_rect_for_character_range(

crates/gpui/src/platform/test/window.rs 🔗

@@ -279,6 +279,8 @@ impl PlatformWindow for TestWindow {
         unimplemented!()
     }
 
+    fn update_ime_position(&self, _bounds: Bounds<Pixels>) {}
+
     fn gpu_specs(&self) -> Option<GPUSpecs> {
         None
     }

crates/gpui/src/platform/windows/events.rs 🔗

@@ -579,8 +579,8 @@ fn handle_mouse_horizontal_wheel_msg(
 
 fn retrieve_caret_position(state_ptr: &Rc<WindowsWindowStatePtr>) -> Option<POINT> {
     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)?;
+        let caret_range = input_handler.selected_text_range(false)?;
+        let caret_position = input_handler.bounds_for_range(caret_range.range)?;
         Some(POINT {
             // logical to physical
             x: (caret_position.origin.x.0 * scale_factor) as i32,
@@ -593,6 +593,7 @@ fn retrieve_caret_position(state_ptr: &Rc<WindowsWindowStatePtr>) -> Option<POIN
 fn handle_ime_position(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
     unsafe {
         let ctx = ImmGetContext(handle);
+
         let Some(caret_position) = retrieve_caret_position(&state_ptr) else {
             return Some(0);
         };

crates/gpui/src/platform/windows/window.rs 🔗

@@ -676,6 +676,10 @@ impl PlatformWindow for WindowsWindow {
         Some(self.0.state.borrow().renderer.gpu_specs())
     }
 
+    fn update_ime_position(&self, _bounds: Bounds<Pixels>) {
+        // todo(windows)
+    }
+
     fn fps(&self) -> Option<f32> {
         None
     }

crates/gpui/src/window.rs 🔗

@@ -3575,6 +3575,18 @@ impl<'a> WindowContext<'a> {
         self.window.platform_window.toggle_fullscreen();
     }
 
+    /// Updates the IME panel position suggestions for languages like japanese, chinese.
+    pub fn invalidate_character_coordinates(&mut self) {
+        self.on_next_frame(|cx| {
+            if let Some(mut input_handler) = cx.window.platform_window.take_input_handler() {
+                if let Some(bounds) = input_handler.selected_bounds(cx) {
+                    cx.window.platform_window.update_ime_position(bounds);
+                }
+                cx.window.platform_window.set_input_handler(input_handler);
+            }
+        });
+    }
+
     /// Present a platform dialog.
     /// The provided message will be presented, along with buttons for each answer.
     /// When a button is clicked, the returned Receiver will receive the index of the clicked button.

crates/search/src/buffer_search.rs 🔗

@@ -552,7 +552,7 @@ impl BufferSearchBar {
             active_editor.search_bar_visibility_changed(false, cx);
             active_editor.toggle_filtered_search_ranges(false, cx);
             let handle = active_editor.focus_handle(cx);
-            cx.focus(&handle);
+            self.focus(&handle, cx);
         }
         cx.emit(Event::UpdateLocation);
         cx.emit(ToolbarItemEvent::ChangeLocation(
@@ -1030,7 +1030,7 @@ impl BufferSearchBar {
         } else {
             return;
         };
-        cx.focus(&focus_handle);
+        self.focus(&focus_handle, cx);
         cx.stop_propagation();
     }
 
@@ -1043,7 +1043,7 @@ impl BufferSearchBar {
         } else {
             return;
         };
-        cx.focus(&focus_handle);
+        self.focus(&focus_handle, cx);
         cx.stop_propagation();
     }
 
@@ -1081,6 +1081,12 @@ impl BufferSearchBar {
         }
     }
 
+    fn focus(&self, handle: &gpui::FocusHandle, cx: &mut ViewContext<Self>) {
+        cx.on_next_frame(|_, cx| {
+            cx.invalidate_character_coordinates();
+        });
+        cx.focus(handle);
+    }
     fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
         if let Some(_) = &self.active_searchable_item {
             self.replace_enabled = !self.replace_enabled;
@@ -1089,7 +1095,7 @@ impl BufferSearchBar {
             } else {
                 self.query_editor.focus_handle(cx)
             };
-            cx.focus(&handle);
+            self.focus(&handle, cx);
             cx.notify();
         }
     }

crates/terminal_view/src/terminal_element.rs 🔗

@@ -5,7 +5,7 @@ use gpui::{
     HighlightStyle, Hitbox, Hsla, InputHandler, InteractiveElement, Interactivity, IntoElement,
     LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels,
     Point, ShapedLine, StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, TextStyle,
-    UnderlineStyle, View, WeakView, WhiteSpace, WindowContext, WindowTextSystem,
+    UTF16Selection, UnderlineStyle, View, WeakView, WhiteSpace, WindowContext, WindowTextSystem,
 };
 use itertools::Itertools;
 use language::CursorShape;
@@ -976,7 +976,11 @@ struct TerminalInputHandler {
 }
 
 impl InputHandler for TerminalInputHandler {
-    fn selected_text_range(&mut self, cx: &mut WindowContext) -> Option<std::ops::Range<usize>> {
+    fn selected_text_range(
+        &mut self,
+        _ignore_disabled_input: bool,
+        cx: &mut WindowContext,
+    ) -> Option<UTF16Selection> {
         if self
             .terminal
             .read(cx)
@@ -986,7 +990,10 @@ impl InputHandler for TerminalInputHandler {
         {
             None
         } else {
-            Some(0..0)
+            Some(UTF16Selection {
+                range: 0..0,
+                reversed: false,
+            })
         }
     }
 
@@ -1014,6 +1021,8 @@ impl InputHandler for TerminalInputHandler {
 
         self.workspace
             .update(cx, |this, cx| {
+                cx.invalidate_character_coordinates();
+
                 let telemetry = this.project().read(cx).client().telemetry().clone();
                 telemetry.log_edit_event("terminal");
             })

crates/terminal_view/src/terminal_view.rs 🔗

@@ -749,7 +749,10 @@ fn subscribe_for_terminal_events(
             },
             Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs),
             Event::CloseTerminal => cx.emit(ItemEvent::CloseItem),
-            Event::SelectionsChanged => cx.emit(SearchEvent::ActiveMatchChanged),
+            Event::SelectionsChanged => {
+                cx.invalidate_character_coordinates();
+                cx.emit(SearchEvent::ActiveMatchChanged)
+            }
         });
     vec![terminal_subscription, terminal_events_subscription]
 }
@@ -887,6 +890,7 @@ impl TerminalView {
     fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
         self.terminal.read(cx).focus_in();
         self.blink_cursors(self.blink_epoch, cx);
+        cx.invalidate_character_coordinates();
         cx.notify();
     }
 

crates/workspace/src/workspace.rs 🔗

@@ -2960,6 +2960,10 @@ impl Workspace {
                 self.remove_pane(pane, focus_on_pane.clone(), cx)
             }
             pane::Event::ActivateItem { local } => {
+                cx.on_next_frame(|_, cx| {
+                    cx.invalidate_character_coordinates();
+                });
+
                 pane.model.update(cx, |pane, _| {
                     pane.track_alternate_file_items();
                 });
@@ -2993,6 +2997,9 @@ impl Workspace {
                 }
             }
             pane::Event::Focus => {
+                cx.on_next_frame(|_, cx| {
+                    cx.invalidate_character_coordinates();
+                });
                 self.handle_pane_focused(pane.clone(), cx);
             }
             pane::Event::ZoomIn => {

crates/zed/src/main.rs 🔗

@@ -318,15 +318,6 @@ 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();
@@ -369,9 +360,19 @@ fn main() {
             }
         }
     }
-    #[cfg(not(any(target_os = "linux", target_os = "windows")))]
+
+    #[cfg(target_os = "windows")]
+    {
+        use zed::windows_only_instance::*;
+        if !check_single_instance() {
+            println!("zed is already running");
+            return;
+        }
+    }
+
+    #[cfg(target_os = "macos")]
     {
-        use zed::only_instance::*;
+        use zed::mac_only_instance::*;
         if ensure_only_instance() != IsOnlyInstance::Yes {
             println!("zed is already running");
             return;

crates/zed/src/zed.rs 🔗

@@ -2,11 +2,11 @@ mod app_menus;
 pub mod inline_completion_registry;
 #[cfg(target_os = "linux")]
 pub(crate) mod linux_prompts;
-#[cfg(not(any(target_os = "linux", target_os = "windows")))]
-pub(crate) mod only_instance;
+#[cfg(target_os = "macos")]
+pub(crate) mod mac_only_instance;
 mod open_listener;
 #[cfg(target_os = "windows")]
-pub(crate) mod single_instance;
+pub(crate) mod windows_only_instance;
 
 pub use app_menus::*;
 use assistant::PromptBuilder;