diff --git a/crates/ui/src/input/code_highlighter.rs b/crates/ui/src/input/code_highlighter.rs index 4e783d69..772bf4f2 100644 --- a/crates/ui/src/input/code_highlighter.rs +++ b/crates/ui/src/input/code_highlighter.rs @@ -6,14 +6,32 @@ use crate::highlighter::Highlighter; #[derive(Debug, Clone)] pub(crate) struct LineHighlightStyle { + offset: usize, pub(crate) styles: Rc, HighlightStyle)>>, } impl LineHighlightStyle { - pub(super) fn to_run(&self, text_style: &TextStyle) -> Vec { + pub(super) fn to_run( + &self, + text_style: &TextStyle, + marked_range: &Option>, + marked_run: &TextRun, + ) -> Vec { self.styles .iter() - .map(|(range, style)| text_style.clone().highlight(*style).to_run(range.len())) + .map(|(range, style)| { + let mut run = text_style.clone().highlight(*style).to_run(range.len()); + if let Some(marked_range) = marked_range { + if self.offset + range.start >= marked_range.start + && self.offset + range.end <= marked_range.end + { + run.color = marked_run.color; + run.strikethrough = marked_run.strikethrough; + run.underline = marked_run.underline; + } + } + run + }) // Add last `\n` Run with len 1 .chain(std::iter::once(text_style.clone().to_run(1))) .collect() @@ -52,21 +70,29 @@ impl CodeHighlighter { } let mut lines = vec![]; + let mut offset = 0; let mut new_cache = HashMap::new(); for line in text.split('\n') { let cache_key = gpui::hash(&line); // cache hit if let Some(line_style) = self.cache.get(&cache_key) { - new_cache.insert(cache_key, line_style.clone()); - lines.push(line_style.clone()); + let new_style = LineHighlightStyle { + offset, + styles: line_style.styles.clone(), + }; + new_cache.insert(cache_key, new_style.clone()); + lines.push(new_style); } else { // cache miss let styles = Rc::new(self.highlighter.highlight(line)); - let line_style = LineHighlightStyle { styles }; + let line_style = LineHighlightStyle { offset, styles }; new_cache.insert(cache_key, line_style.clone()); lines.push(line_style); } + + // +1 for '\n' + offset += line.len() + 1; } // Ensure to recreate cache to remove unused caches. diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs index 84e560ec..94d3836e 100644 --- a/crates/ui/src/input/element.rs +++ b/crates/ui/src/input/element.rs @@ -1,5 +1,3 @@ -use std::rc::Rc; - use gpui::{ fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler, Entity, GlobalElementId, IntoElement, LayoutId, MouseButton, MouseMoveEvent, PaintQuad, Path, @@ -58,7 +56,11 @@ impl TextElement { cx: &mut App, ) -> (Option, Point, usize) { let input = self.input.read(cx); - let selected_range = &input.selected_range; + let mut selected_range = input.selected_range.clone(); + if let Some(marked_range) = &input.marked_range { + selected_range = marked_range.end..marked_range.end; + } + let cursor_offset = input.cursor_offset(); let mut current_line_index = 0; let mut scroll_offset = input.scroll_handle.offset(); @@ -193,7 +195,12 @@ impl TextElement { cx: &mut App, ) -> Option> { let input = self.input.read(cx); - let selected_range = &input.selected_range; + let mut selected_range = input.selected_range.clone(); + if let Some(marked_range) = &input.marked_range { + if !marked_range.is_empty() { + selected_range = marked_range.end..marked_range.end; + } + } if selected_range.is_empty() { return None; } @@ -322,13 +329,6 @@ impl TextElement { _ => None, }) } - - fn highlighter(&self, cx: &App) -> Option>> { - match &self.input.read(cx).mode { - InputMode::CodeEditor { highlighter, .. } => Some(highlighter.highlighter.clone()), - _ => None, - } - } } pub(super) struct PrepaintState { @@ -513,8 +513,31 @@ impl Element for TextElement { underline: None, strikethrough: None, }; + let marked_run = TextRun { + len: 0, + font: style.font(), + color: text_color, + background_color: None, + underline: Some(UnderlineStyle { + thickness: px(1.), + color: Some(text_color), + wavy: false, + }), + strikethrough: None, + }; - let runs = if let Some(marked_range) = input.marked_range.as_ref() { + let runs = if !is_empty { + if let Some(highlight_lines) = highlight_lines { + let mut runs = vec![]; + for style in highlight_lines { + runs.extend(style.to_run(&text_style, &input.marked_range, &marked_run)); + } + runs.into_iter().filter(|run| run.len > 0).collect() + } else { + vec![run] + } + } else if let Some(marked_range) = &input.marked_range { + // IME marked text vec![ TextRun { len: marked_range.start, @@ -522,11 +545,7 @@ impl Element for TextElement { }, TextRun { len: marked_range.end - marked_range.start, - underline: Some(UnderlineStyle { - color: Some(run.color), - thickness: px(1.0), - wavy: false, - }), + underline: marked_run.underline, ..run.clone() }, TextRun { @@ -537,16 +556,6 @@ impl Element for TextElement { .into_iter() .filter(|run| run.len > 0) .collect() - } else if !is_empty { - if let Some(highlight_lines) = highlight_lines { - let mut runs = vec![]; - for style in highlight_lines { - runs.extend(style.to_run(&text_style)); - } - runs.into_iter().filter(|run| run.len > 0).collect() - } else { - vec![run] - } } else { vec![run] }; @@ -667,11 +676,6 @@ impl Element for TextElement { } }); - // Paint selections - if let Some(path) = prepaint.selection_path.take() { - window.paint_path(path, cx.theme().selection); - } - // Paint multi line text let line_height = window.line_height(); let origin = bounds.origin; @@ -690,10 +694,14 @@ impl Element for TextElement { for (ix, line) in line_numbers.iter().enumerate() { let p = point(origin.x, origin.y + offset_y); let line_size = line.size(line_height); + // Paint the current line background if prepaint.current_line_index == ix { if let Some(bg_color) = self - .highlighter(cx) + .input + .read(cx) + .mode + .highlighter() .and_then(|h| h.theme.settings().line_highlight) .map(crate::highlighter::color_to_hsla) { @@ -709,6 +717,12 @@ impl Element for TextElement { } } + // Paint selections + if let Some(path) = prepaint.selection_path.take() { + window.paint_path(path, cx.theme().selection); + } + + // Paint text let mut offset_y = px(0.); for line in prepaint.lines.iter() { let p = point(origin.x + prepaint.line_number_width, origin.y + offset_y); diff --git a/crates/ui/src/input/mode.rs b/crates/ui/src/input/mode.rs index 4d971349..d0d6910a 100644 --- a/crates/ui/src/input/mode.rs +++ b/crates/ui/src/input/mode.rs @@ -1,3 +1,5 @@ +use std::rc::Rc; + use gpui::{DefiniteLength, SharedString}; use super::code_highlighter::CodeHighlighter; @@ -148,6 +150,13 @@ impl InputMode { _ => None, } } + + pub(super) fn highlighter(&self) -> Option>> { + match &self { + InputMode::CodeEditor { highlighter, .. } => Some(highlighter.highlighter.clone()), + _ => None, + } + } } #[cfg(test)] diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index ab90be3d..598ff35d 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -225,6 +225,7 @@ pub struct InputState { pub(super) selection_reversed: bool, /// The index of the current line, zero-based. pub(super) current_line_index: Option, + /// The marked range is the temporary insert text on IME typing. pub(super) marked_range: Option>, pub(super) last_layout: Option>, pub(super) last_cursor_offset: Option, @@ -1302,6 +1303,9 @@ impl InputState { } pub(super) fn escape(&mut self, _: &Escape, window: &mut Window, cx: &mut Context) { + if self.marked_range.is_some() { + self.unmark_text(window, cx); + } if self.selected_range.len() > 0 { return self.unselect(window, cx); } @@ -1319,6 +1323,14 @@ impl InputState { window: &mut Window, cx: &mut Context, ) { + // If there have IME marked range and is empty (Means pressed Esc to abort IME typing) + // Clear the marked range. + if let Some(marked_range) = &self.marked_range { + if marked_range.len() == 0 { + self.marked_range = None; + } + } + self.selecting = true; let offset = self.index_for_mouse_position(event.position, window, cx); // Double click to select word @@ -1468,6 +1480,10 @@ impl InputState { } pub(super) fn cursor_offset(&self) -> usize { + if let Some(marked_range) = &self.marked_range { + return marked_range.end; + } + if self.selection_reversed { self.selected_range.start } else { @@ -1938,6 +1954,7 @@ impl EntityInputHandler for InputState { cx.notify(); } + /// Mark text is the IME temporary insert on typing. fn replace_and_mark_text_in_range( &mut self, range_utf16: Option>, @@ -1963,12 +1980,20 @@ impl EntityInputHandler for InputState { self.push_history(&range, new_text, window, cx); self.text = pending_text; - self.marked_range = Some(range.start..range.start + new_text.len()); - self.selected_range = new_selected_range_utf16 - .as_ref() - .map(|range_utf16| self.range_from_utf16(range_utf16)) - .map(|new_range| new_range.start + range.start..new_range.end + range.end) - .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len()); + self.text_wrapper.update(self.text.clone(), false, cx); + if new_text.is_empty() { + // Cancel selection, when cancel IME input. + self.selected_range = range.start..range.start; + self.marked_range = None; + } else { + self.marked_range = Some(range.start..range.start + new_text.len()); + self.selected_range = new_selected_range_utf16 + .as_ref() + .map(|range_utf16| self.range_from_utf16(range_utf16)) + .map(|new_range| new_range.start + range.start..new_range.end + range.end) + .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len()); + } + self.mode.update_auto_grow(&self.text_wrapper); cx.emit(InputEvent::Change(self.unmask_value())); cx.notify(); } @@ -1988,36 +2013,44 @@ impl EntityInputHandler for InputState { let mut start_origin = None; let mut end_origin = None; + let line_number_origin = point(self.line_number_width, px(0.)); let mut y_offset = px(0.); let mut index_offset = 0; for line in lines.iter() { - if let Some(p) = - line.position_for_index(range.start.saturating_sub(index_offset), line_height) - { - start_origin = Some(p + point(px(0.), y_offset)); - } - if let Some(p) = - line.position_for_index(range.end.saturating_sub(index_offset), line_height) - { - end_origin = Some(p + point(px(0.), y_offset)); - } - - y_offset += line.size(line_height).height; if start_origin.is_some() && end_origin.is_some() { break; } - index_offset += line.len(); + if start_origin.is_none() { + if let Some(p) = + line.position_for_index(range.start.saturating_sub(index_offset), line_height) + { + start_origin = Some(p + point(px(0.), y_offset)); + } + } + + if end_origin.is_none() { + if let Some(p) = + line.position_for_index(range.end.saturating_sub(index_offset), line_height) + { + end_origin = Some(p + point(px(0.), y_offset)); + } + } + + index_offset += line.len() + 1; + y_offset += line.size(line_height).height; } let start_origin = start_origin.unwrap_or_default(); - let end_origin = end_origin.unwrap_or_default(); + let mut end_origin = end_origin.unwrap_or_default(); + // Ensure at same line. + end_origin.y = start_origin.y; Some(Bounds::from_corners( - bounds.origin + start_origin, + bounds.origin + line_number_origin + start_origin, // + line_height for show IME panel under the cursor line. - bounds.origin + point(end_origin.x, end_origin.y + line_height), + bounds.origin + line_number_origin + point(end_origin.x, end_origin.y + line_height), )) }