From c56646ffdfffc27d0aab8b3940ef93c4153375be Mon Sep 17 00:00:00 2001 From: Gavin Luo Date: Sat, 4 Jul 2026 04:25:16 +0800 Subject: [PATCH] terminal: Fix IME candidate window not following cursor in TUI apps (#59911) # Objective Fix IME candidate window not following cursor position in the integrated terminal when running fullscreen TUI applications like opencode. When using IME (Input Method Editor) in Zed's terminal with fullscreen TUI applications (e.g., opencode), the candidate window does not follow the cursor position. This issue does not occur in normal terminal usage (e.g., bash shell). # Solution The root cause was three-layer blocking preventing IME position updates in ALT_SCREEN mode: 1. **ALT_SCREEN blocking**: `selected_text_range()` returned `None` in ALT_SCREEN mode, causing `selected_bounds()` to return `None` 2. **Missing trigger**: `Event::Wakeup` did not call `invalidate_character_coordinates()`, so cursor movement did not trigger IME position updates 3. **Composition blocking**: `update_ime_position()` skipped updates when `state.composing` was true Fixes: - Remove ALT_SCREEN check in `selected_text_range()` so IME position updates work in fullscreen TUI apps - Add `window.invalidate_character_coordinates()` in `Event::Wakeup` to trigger IME position updates when terminal cursor moves - Remove `state.composing` check in `update_ime_position()` to allow IME position updates during text-input-v3 composition - Remove unused `terminal` field from `TerminalInputHandler` struct # Testing **Did you test these changes? If so, how?** - Yes, tested on Linux GNOME Wayland with iBus input method - Verified IME candidate window correctly follows cursor in opencode - Verified normal terminal usage with IME still works correctly **Are there any parts that need more testing?** - Other IMEs (fcitx, etc.) may need testing **How can other people (reviewers) test your changes?** 1. Open Zed's integrated terminal 2. Run a fullscreen TUI application like `opencode` 3. Activate IME (e.g., iBus with Chinese input) 4. Type text and observe the IME candidate window follows the cursor **What platforms did you test these changes on?** - Linux (GNOME Wayland) - tested - macOS - not tested (may have different IME behavior) - Windows - not tested (different code path) # Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [ ] Unsafe blocks (if any) have justifying comments - [ ] The content adheres to Zed's UI standards (UX/UI and icon guidelines) - [ ] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable # Showcase
Before
After
--- Release Notes: - Fixed IME candidate window not following cursor in terminal TUI apps --- crates/gpui_linux/src/linux/wayland/client.rs | 2 +- crates/terminal_view/src/terminal_element.rs | 25 ++++++------------- crates/terminal_view/src/terminal_view.rs | 1 + 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index ac637d3fc4..256875bed1 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -388,7 +388,7 @@ impl WaylandClientStatePtr { pub fn update_ime_position(&self, bounds: Bounds) { let client = self.get_client(); let state = client.borrow_mut(); - if state.composing || state.text_input.is_none() || state.pre_edit_text.is_some() { + if state.text_input.is_none() || state.pre_edit_text.is_some() { return; } diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index cd0ed9241f..b74d28593d 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1349,7 +1349,6 @@ impl Element for TerminalElement { }; let terminal_input_handler = TerminalInputHandler { - terminal: self.terminal.clone(), terminal_view: self.terminal_view.clone(), cursor_bounds: layout.ime_cursor_bounds.map(|bounds| bounds + origin), workspace: self.workspace.clone(), @@ -1510,7 +1509,6 @@ impl IntoElement for TerminalElement { } struct TerminalInputHandler { - terminal: Entity, terminal_view: Entity, workspace: WeakEntity, cursor_bounds: Option>, @@ -1521,22 +1519,15 @@ impl InputHandler for TerminalInputHandler { &mut self, _ignore_disabled_input: bool, _: &mut Window, - cx: &mut App, + _cx: &mut App, ) -> Option { - if self - .terminal - .read(cx) - .last_content - .mode - .contains(Modes::ALT_SCREEN) - { - None - } else { - Some(UTF16Selection { - range: 0..0, - reversed: false, - }) - } + // Always return a valid selection for IME positioning, + // even in ALT_SCREEN mode (fullscreen TUI apps like opencode, vim, etc.) + // The terminal still has a cursor position that should be used for IME candidate window placement. + Some(UTF16Selection { + range: 0..0, + reversed: false, + }) } fn marked_text_range( diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index ea888d564c..3b1e6db97f 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1129,6 +1129,7 @@ fn subscribe_for_terminal_events( match event { Event::Wakeup => { cx.notify(); + window.invalidate_character_coordinates(); cx.emit(Event::Wakeup); cx.emit(ItemEvent::UpdateTab); cx.emit(SearchEvent::MatchesInvalidated);