From 5c7abadf2d1b7c6e6a3e1d42bd430436549c7332 Mon Sep 17 00:00:00 2001 From: koh-sh <34917718+koh-sh@users.noreply.github.com> Date: Mon, 12 Jan 2026 18:59:51 +0900 Subject: [PATCH] terminal: Fix IME position when cursor is hidden (#46592) Closes https://github.com/zed-industries/zed/issues/45553 ## Summary When using applications that hide the terminal cursor (e.g., via ANSI escape sequence `\e[?25l`), the IME candidate window appeared at the bottom-left of the screen, and the composition text was not displayed at all. This affects tools like Claude Code that hide the cursor during operation. The root cause was that cursor position calculation was skipped when the cursor shape was `Hidden`. This fix separates cursor visibility from IME position calculation by always computing cursor bounds for IME positioning. ## Movies ### Zed 0.218.6 https://github.com/user-attachments/assets/a66d569a-178d-4bca-a577-fcc65e9fd2c4 ### With this fix https://github.com/user-attachments/assets/456323d1-6c18-45ea-88e8-fb5375c00f98 Release Notes: - Fixed IME candidate window and composition text position in terminal when cursor is hidden --- crates/terminal_view/src/terminal_element.rs | 92 ++++++++++---------- 1 file changed, 48 insertions(+), 44 deletions(-) diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index d6b5588771e5d67e6e47ea9659bd76dfafb39e05..fc1c63879cc4bebb476daff2e54a2cb48305f5ad 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -43,6 +43,7 @@ pub struct LayoutState { rects: Vec, relative_highlighted_ranges: Vec<(RangeInclusive, Hsla)>, cursor: Option, + ime_cursor_bounds: Option>, background_color: Hsla, dimensions: TerminalBounds, mode: TermMode, @@ -1130,49 +1131,54 @@ impl Element for TerminalElement { // Layout cursor. Rectangle is used for IME, so we should lay it out even // if we don't end up showing it. + let cursor_point = DisplayCursor::from(cursor.point, display_offset); + let cursor_text = { + let str_trxt = cursor_char.to_string(); + let len = str_trxt.len(); + window.text_system().shape_line( + str_trxt.into(), + text_style.font_size.to_pixels(window.rem_size()), + &[TextRun { + len, + font: text_style.font(), + color: theme.colors().terminal_ansi_background, + ..Default::default() + }], + None, + ) + }; + + let ime_cursor_bounds = + TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map( + |(cursor_position, block_width)| Bounds { + origin: cursor_position, + size: size(block_width, dimensions.line_height), + }, + ); + let cursor = if let AlacCursorShape::Hidden = cursor.shape { None } else { - let cursor_point = DisplayCursor::from(cursor.point, display_offset); - let cursor_text = { - let str_trxt = cursor_char.to_string(); - let len = str_trxt.len(); - window.text_system().shape_line( - str_trxt.into(), - text_style.font_size.to_pixels(window.rem_size()), - &[TextRun { - len, - font: text_style.font(), - color: theme.colors().terminal_ansi_background, - ..Default::default() - }], - None, - ) - }; - let focused = self.focused; - TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map( - move |(cursor_position, block_width)| { - let (shape, text) = match cursor.shape { - AlacCursorShape::Block if !focused => (CursorShape::Hollow, None), - AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)), - AlacCursorShape::Underline => (CursorShape::Underline, None), - AlacCursorShape::Beam => (CursorShape::Bar, None), - AlacCursorShape::HollowBlock => (CursorShape::Hollow, None), - //This case is handled in the if wrapping the whole cursor layout - AlacCursorShape::Hidden => unreachable!(), - }; + ime_cursor_bounds.map(move |bounds| { + let (shape, text) = match cursor.shape { + AlacCursorShape::Block if !focused => (CursorShape::Hollow, None), + AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)), + AlacCursorShape::Underline => (CursorShape::Underline, None), + AlacCursorShape::Beam => (CursorShape::Bar, None), + AlacCursorShape::HollowBlock => (CursorShape::Hollow, None), + AlacCursorShape::Hidden => unreachable!(), + }; - CursorLayout::new( - cursor_position, - block_width, - dimensions.line_height, - theme.players().local().cursor, - shape, - text, - ) - }, - ) + CursorLayout::new( + bounds.origin, + bounds.size.width, + bounds.size.height, + theme.players().local().cursor, + shape, + text, + ) + }) }; let block_below_cursor_element = if let Some(block) = &self.block_below_cursor { @@ -1211,6 +1217,7 @@ impl Element for TerminalElement { hitbox, batched_text_runs, cursor, + ime_cursor_bounds, background_color, dimensions, rects, @@ -1253,10 +1260,7 @@ impl Element for TerminalElement { let terminal_input_handler = TerminalInputHandler { terminal: self.terminal.clone(), terminal_view: self.terminal_view.clone(), - cursor_bounds: layout - .cursor - .as_ref() - .map(|cursor| cursor.bounding_rect(origin)), + cursor_bounds: layout.ime_cursor_bounds.map(|bounds| bounds + origin), workspace: self.workspace.clone(), }; @@ -1336,8 +1340,8 @@ impl Element for TerminalElement { if let Some(text_to_mark) = &marked_text_cloned && !text_to_mark.is_empty() - && let Some(cursor_layout) = &original_cursor { - let ime_position = cursor_layout.bounding_rect(origin).origin; + && let Some(ime_bounds) = layout.ime_cursor_bounds { + let ime_position = (ime_bounds + origin).origin; let mut ime_style = layout.base_text_style.clone(); ime_style.underline = Some(UnderlineStyle { color: Some(ime_style.color),