diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 93402e33a9..8c75f8e079 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -26,6 +26,7 @@ use terminal::{ }; use theme::{ActiveTheme, Theme, ThemeSettings}; use ui::{ParentElement, Tooltip}; +use util::ResultExt; use workspace::Workspace; use std::mem; @@ -47,6 +48,7 @@ pub struct LayoutState { hyperlink_tooltip: Option, gutter: Pixels, block_below_cursor_element: Option, + base_text_style: TextStyle, } /// Helper struct for converting data between Alacritty's cursor points, and displayed cursor points. @@ -898,6 +900,7 @@ impl Element for TerminalElement { hyperlink_tooltip, gutter, block_below_cursor_element, + base_text_style: text_style, } }, ) @@ -919,8 +922,14 @@ impl Element for TerminalElement { let origin = bounds.origin + Point::new(layout.gutter, px(0.)) - Point::new(px(0.), scroll_top); + let marked_text_cloned: Option = { + let ime_state = self.terminal_view.read(cx); + ime_state.marked_text.clone() + }; + let terminal_input_handler = TerminalInputHandler { terminal: self.terminal.clone(), + terminal_view: self.terminal_view.clone(), cursor_bounds: layout .cursor .as_ref() @@ -938,7 +947,7 @@ impl Element for TerminalElement { window.set_cursor_style(gpui::CursorStyle::IBeam, Some(&layout.hitbox)); } - let cursor = layout.cursor.take(); + let original_cursor = layout.cursor.take(); let hyperlink_tooltip = layout.hyperlink_tooltip.take(); let block_below_cursor_element = layout.block_below_cursor_element.take(); self.interactivity.paint( @@ -988,8 +997,41 @@ impl Element for TerminalElement { cell.paint(origin, &layout.dimensions, bounds, window, cx); } - if self.cursor_visible { - if let Some(mut cursor) = cursor { + if let Some(text_to_mark) = &marked_text_cloned { + if !text_to_mark.is_empty() { + if let Some(cursor_layout) = &original_cursor { + let ime_position = cursor_layout.bounding_rect(origin).origin; + let mut ime_style = layout.base_text_style.clone(); + ime_style.underline = Some(UnderlineStyle { + color: Some(ime_style.color), + thickness: px(1.0), + wavy: false, + }); + + let shaped_line = window + .text_system() + .shape_line( + text_to_mark.clone().into(), + ime_style.font_size.to_pixels(window.rem_size()), + &[TextRun { + len: text_to_mark.len(), + font: ime_style.font(), + color: ime_style.color, + background_color: None, + underline: ime_style.underline, + strikethrough: None, + }], + ) + .unwrap(); + shaped_line + .paint(ime_position, layout.dimensions.line_height, window, cx) + .log_err(); + } + } + } + + if self.cursor_visible && marked_text_cloned.is_none() { + if let Some(mut cursor) = original_cursor { cursor.paint(origin, window, cx); } } @@ -1017,6 +1059,7 @@ impl IntoElement for TerminalElement { struct TerminalInputHandler { terminal: Entity, + terminal_view: Entity, workspace: WeakEntity, cursor_bounds: Option>, } @@ -1044,8 +1087,12 @@ impl InputHandler for TerminalInputHandler { } } - fn marked_text_range(&mut self, _: &mut Window, _: &mut App) -> Option> { - None + fn marked_text_range( + &mut self, + _window: &mut Window, + cx: &mut App, + ) -> Option> { + self.terminal_view.read(cx).marked_text_range() } fn text_for_range( @@ -1065,8 +1112,9 @@ impl InputHandler for TerminalInputHandler { window: &mut Window, cx: &mut App, ) { - self.terminal.update(cx, |terminal, _| { - terminal.input(text); + self.terminal_view.update(cx, |view, view_cx| { + view.clear_marked_text(view_cx); + view.commit_text(text, view_cx); }); self.workspace @@ -1082,22 +1130,37 @@ impl InputHandler for TerminalInputHandler { fn replace_and_mark_text_in_range( &mut self, _range_utf16: Option>, - _new_text: &str, - _new_selected_range: Option>, + new_text: &str, + new_marked_range: Option>, _window: &mut Window, - _cx: &mut App, + cx: &mut App, ) { + if let Some(range) = new_marked_range { + self.terminal_view.update(cx, |view, view_cx| { + view.set_marked_text(new_text.to_string(), range, view_cx); + }); + } } - fn unmark_text(&mut self, _window: &mut Window, _cx: &mut App) {} + fn unmark_text(&mut self, _window: &mut Window, cx: &mut App) { + self.terminal_view.update(cx, |view, view_cx| { + view.clear_marked_text(view_cx); + }); + } fn bounds_for_range( &mut self, - _range_utf16: std::ops::Range, + range_utf16: std::ops::Range, _window: &mut Window, - _cx: &mut App, + cx: &mut App, ) -> Option> { - self.cursor_bounds + let term_bounds = self.terminal_view.read(cx).terminal_bounds(cx); + + let mut bounds = self.cursor_bounds?; + let offset_x = term_bounds.cell_width * range_utf16.start as f32; + bounds.origin.x += offset_x; + + Some(bounds) } fn apple_press_and_hold_enabled(&mut self) -> bool { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 7623656272..e1c95e315e 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -52,7 +52,7 @@ use zed_actions::assistant::InlineAssist; use std::{ cmp, - ops::RangeInclusive, + ops::{Range, RangeInclusive}, path::{Path, PathBuf}, rc::Rc, sync::Arc, @@ -126,6 +126,8 @@ pub struct TerminalView { scroll_handle: TerminalScrollHandle, show_scrollbar: bool, hide_scrollbar_task: Option>, + marked_text: Option, + marked_range_utf16: Option>, _subscriptions: Vec, _terminal_subscriptions: Vec, } @@ -218,6 +220,8 @@ impl TerminalView { show_scrollbar: !Self::should_autohide_scrollbar(cx), hide_scrollbar_task: None, cwd_serialized: false, + marked_text: None, + marked_range_utf16: None, _subscriptions: vec![ focus_in, focus_out, @@ -227,6 +231,45 @@ impl TerminalView { } } + /// Sets the marked (pre-edit) text from the IME. + pub(crate) fn set_marked_text( + &mut self, + text: String, + range: Range, + cx: &mut Context, + ) { + self.marked_text = Some(text); + self.marked_range_utf16 = Some(range); + cx.notify(); + } + + /// Gets the current marked range (UTF-16). + pub(crate) fn marked_text_range(&self) -> Option> { + self.marked_range_utf16.clone() + } + + /// Clears the marked (pre-edit) text state. + pub(crate) fn clear_marked_text(&mut self, cx: &mut Context) { + if self.marked_text.is_some() { + self.marked_text = None; + self.marked_range_utf16 = None; + cx.notify(); + } + } + + /// Commits (sends) the given text to the PTY. Called by InputHandler::replace_text_in_range. + pub(crate) fn commit_text(&mut self, text: &str, cx: &mut Context) { + if !text.is_empty() { + self.terminal.update(cx, |term, _| { + term.input(text.to_string()); + }); + } + } + + pub(crate) fn terminal_bounds(&self, cx: &App) -> TerminalBounds { + self.terminal.read(cx).last_content().terminal_bounds + } + pub fn entity(&self) -> &Entity { &self.terminal }