From 7c993f943539dabd9938b4bba20a18aebee10f50 Mon Sep 17 00:00:00 2001 From: Dongdong Zhou Date: Wed, 5 Oct 2022 22:34:32 +0100 Subject: [PATCH] IME Support for macOS (#1440) --- CHANGELOG.md | 22 ++ Cargo.lock | 6 +- lapce-data/src/document.rs | 513 ++++++++++++++++++------------------ lapce-ui/Cargo.toml | 6 +- lapce-ui/src/editor.rs | 21 ++ lapce-ui/src/editor/view.rs | 156 ++++++++--- lapce-ui/src/ime.rs | 223 ++++++++++++++++ lapce-ui/src/lib.rs | 1 + 8 files changed, 652 insertions(+), 296 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 lapce-ui/src/ime.rs diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..64549bb1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog + +## Unreleased + +### Added + +- Added Dockerfile, C# & Nix tree sitter hightlight [#1104] +- Added DLang LSP langue ids [#1122] +- Implemented logic for getting the installation progress [#1281] +- Render whitespace [#1284] +- Implemented syntax aware selection [#1353] +- Added autosave configuration [#1358] +- IME support for macOS [#1440] + +### Fixed + +- Fixed high CPU issue when editor font family is empy [#1030] +- Fixed an issue that sometimes Lapce can't open [bf5a98a6d432f9d2abdc1737da2d075e204771fb] +- Much improved tree sitter highlight [#957] +- Fixed terminal issues under flatpak [#1135] +- Fixed auto-completion crash [#1366] +- Fixed hover hints + show multiple hover hint items [#1381] \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9e79757e..01420427 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1008,7 +1008,7 @@ dependencies = [ [[package]] name = "druid" version = "0.7.0" -source = "git+https://github.com/lapce/druid?branch=shell_opengl#d8f036bc8100b19980dfcbd3067fcae600407219" +source = "git+https://github.com/lapce/druid?branch=shell_opengl#063db199131ddd714db630700076bf127c8e06c1" dependencies = [ "console_error_panic_hook", "druid-derive", @@ -1031,7 +1031,7 @@ dependencies = [ [[package]] name = "druid-derive" version = "0.4.0" -source = "git+https://github.com/lapce/druid?branch=shell_opengl#d8f036bc8100b19980dfcbd3067fcae600407219" +source = "git+https://github.com/lapce/druid?branch=shell_opengl#063db199131ddd714db630700076bf127c8e06c1" dependencies = [ "proc-macro2", "quote", @@ -1041,7 +1041,7 @@ dependencies = [ [[package]] name = "druid-shell" version = "0.7.0" -source = "git+https://github.com/lapce/druid?branch=shell_opengl#d8f036bc8100b19980dfcbd3067fcae600407219" +source = "git+https://github.com/lapce/druid?branch=shell_opengl#063db199131ddd714db630700076bf127c8e06c1" dependencies = [ "anyhow", "bitflags", diff --git a/lapce-data/src/document.rs b/lapce-data/src/document.rs index 52170031..471e0328 100644 --- a/lapce-data/src/document.rs +++ b/lapce-data/src/document.rs @@ -1,5 +1,4 @@ use std::{ - borrow::Cow, cell::RefCell, collections::{HashMap, HashSet}, path::{Path, PathBuf}, @@ -11,7 +10,7 @@ piet::{ PietText, PietTextLayout, Text, TextAttribute, TextLayout, TextLayoutBuilder, }, - Color, ExtEventSink, Point, Size, Target, Vec2, WidgetId, + Color, ExtEventSink, FontFamily, Point, Size, Target, Vec2, WidgetId, }; use itertools::Itertools; use lapce_core::{ @@ -225,22 +224,35 @@ pub fn file_name(&self) -> &str { } } -#[derive(Default)] -pub struct PhantomTextLine<'hint, 'diag> { - // TODO: This could be made more general - /// These are entries that have an order within the text - ordered_text: SmallVec<[(usize, &'hint InlayHint); 6]>, - // TODO: This could be made more general (ex: for things like showing the commit information - // for that line) - /// These are entries that are always at the end of the text - end_text: SmallVec<[&'diag EditorDiagnostic; 3]>, +pub struct PhantomText { + kind: PhantomTextKind, + col: usize, + text: String, + font_size: Option, + font_family: Option, + fg: Option, + bg: Option, + under_line: Option, } -impl<'hint, 'diag> PhantomTextLine<'hint, 'diag> { +#[derive(Ord, Eq, PartialEq, PartialOrd)] +pub enum PhantomTextKind { + Ime, + InlayHint, + Diagnostic, +} + +#[derive(Default)] +pub struct PhantomTextLine { + text: SmallVec<[PhantomText; 6]>, + max_severity: Option, +} + +impl PhantomTextLine { /// Translate a column position into the text into what it would be after combining pub fn col_at(&self, pre_col: usize) -> usize { let mut last = pre_col; - for (col_shift, size, _, col) in self.offset_size_iter() { + for (col_shift, size, col, _) in self.offset_size_iter() { if pre_col >= col { last = pre_col + col_shift + size; } @@ -253,7 +265,7 @@ pub fn col_at(&self, pre_col: usize) -> usize { /// If before_cursor is false and the cursor is right at the start then it will stay there pub fn col_after(&self, pre_col: usize, before_cursor: bool) -> usize { let mut last = pre_col; - for (col_shift, size, _, col) in self.offset_size_iter() { + for (col_shift, size, col, _) in self.offset_size_iter() { if pre_col > col || (pre_col == col && before_cursor) { last = pre_col + col_shift + size; } @@ -265,7 +277,7 @@ pub fn col_after(&self, pre_col: usize, before_cursor: bool) -> usize { /// Translate a column position into the position it would be before combining pub fn before_col(&self, col: usize) -> usize { let mut last = col; - for (col_shift, size, _, hint_col) in self.offset_size_iter() { + for (col_shift, size, hint_col, _) in self.offset_size_iter() { let shifted_start = hint_col + col_shift; let shifted_end = shifted_start + size; if col >= shifted_start { @@ -280,62 +292,23 @@ pub fn before_col(&self, col: usize) -> usize { } /// Insert the hints at their positions in the text - pub fn combine_with_text<'b>(&self, mut text: Cow<'b, str>) -> Cow<'b, str> { + pub fn combine_with_text(&self, text: String) -> String { + let mut text = text; let mut col_shift = 0; - for (col, hint) in self.ordered_text.iter() { - let mut otext = text.into_owned(); - let location = col + col_shift; + for phantom in self.text.iter() { + let location = phantom.col + col_shift; // Stop iterating if the location is bad - if otext.get(location..).is_none() { - return Cow::Owned(otext); + if text.get(location..).is_none() { + return text; } - // Insert the right padding. This will be shifted to the right - // after we insert the text at location - if hint.padding_right == Some(true) { - otext.insert(location, ' '); - col_shift += 1; - } - - match &hint.label { - InlayHintLabel::String(label) => { - otext.insert_str(location, label.as_str()); - col_shift += label.len(); - } - InlayHintLabel::LabelParts(parts) => { - for part in parts.iter().rev() { - otext.insert_str(location, part.value.as_str()); - col_shift += part.value.len(); - } - } - }; - - if hint.padding_left == Some(true) { - otext.insert(location, ' '); - col_shift += 1; - } - - text = Cow::Owned(otext); + text.insert_str(location, &phantom.text); + col_shift += phantom.text.len(); } - // If there are end text entries then trim any whitespace at the end - if !self.end_text.is_empty() { - text = Cow::Owned(text.into_owned().trim_end().to_string()); - } - - let mut otext = text.into_owned(); - for entry in self.end_text.iter() { - // TODO: allow customization of padding. Remember to update end_offset_size_iter - otext.push_str(" "); - otext.extend(itertools::intersperse( - entry.diagnostic.message.lines(), - " ", - )); - } - - Cow::Owned(otext) + text } /// Iterator over (col_shift, size, hint, pre_column) @@ -343,58 +316,18 @@ pub fn combine_with_text<'b>(&self, mut text: Cow<'b, str>) -> Cow<'b, str> { /// they'll be positioned pub fn offset_size_iter( &self, - ) -> impl Iterator + '_ { + ) -> impl Iterator + '_ { let mut col_shift = 0; - self.ordered_text.iter().map(move |(col, hint)| { + + self.text.iter().map(move |phantom| { let pre_col_shift = col_shift; - match &hint.label { - InlayHintLabel::String(label) => { - col_shift += label.len(); - } - InlayHintLabel::LabelParts(parts) => { - for part in parts { - col_shift += part.value.len(); - } - } - } - - if hint.padding_right == Some(true) { - col_shift += 1; - } - - if hint.padding_left == Some(true) { - col_shift += 1; - } - - (pre_col_shift, col_shift - pre_col_shift, *hint, *col) - }) - } - - /// Iterator over (column, size, diagnostic) - pub fn end_offset_size_iter( - &self, - pre_text: &str, - ) -> impl Iterator + '_ { - const PADDING: usize = 4; - - // Trim because the text would be trimmed for any end text that existed - let column = pre_text.trim_end().len(); - // Move the column to be after the shifts by any of the ordered texts - let mut column = self.col_at(column); - - self.end_text.iter().map(move |entry| { - let text_size = itertools::intersperse( - entry.diagnostic.message.lines().map(str::len), - 1, + col_shift += phantom.text.len(); + ( + pre_col_shift, + col_shift - pre_col_shift, + phantom.col, + phantom, ) - .sum::(); - let size = PADDING + text_size; - - let column_pre = column; - - column += size; - - (column_pre, size, *entry) }) } } @@ -418,6 +351,8 @@ pub struct Document { pub code_actions: im::HashMap, pub inlay_hints: Option>, pub diagnostics: Option>>, + ime_text: Option>, + ime_pos: (usize, usize, usize), pub syntax_selection_range: Option, pub find: Rc>, find_progress: Rc>, @@ -461,6 +396,8 @@ pub fn new( code_actions: im::HashMap::new(), inlay_hints: None, diagnostics: None, + ime_text: None, + ime_pos: (0, 0, 0), find: Rc::new(RefCell::new(Find::new(0))), find_progress: Rc::new(RefCell::new(FindProgress::Ready)), event_sink, @@ -1003,6 +940,30 @@ fn update_inlay_hints(&mut self, delta: &RopeDelta) { } } + pub fn set_ime_pos(&mut self, line: usize, col: usize, shift: usize) { + self.ime_pos = (line, col, shift); + } + + pub fn ime_text(&self) -> Option<&Arc> { + self.ime_text.as_ref() + } + + pub fn ime_pos(&self) -> (usize, usize, usize) { + self.ime_pos + } + + pub fn set_ime_text(&mut self, text: String) { + self.ime_text = Some(Arc::new(text)); + self.clear_text_layout_cache(); + } + + pub fn clear_ime_text(&mut self) { + if self.ime_text.is_some() { + self.ime_text = None; + self.clear_text_layout_cache(); + } + } + pub fn line_phantom_text( &self, config: &LapceConfig, @@ -1010,44 +971,157 @@ pub fn line_phantom_text( ) -> PhantomTextLine { let start_offset = self.buffer.offset_of_line(line); let end_offset = self.buffer.offset_of_line(line + 1); - let hints = if config.editor.enable_inlay_hints { - self.inlay_hints.as_ref().map(|hints| { - hints.iter_chunks(start_offset..end_offset).filter_map( - |(interval, inlay_hint)| { - if interval.start >= start_offset - && interval.start < end_offset - { + + let hints = config + .editor + .enable_inlay_hints + .then_some(()) + .and_then(|_| { + self.inlay_hints.as_ref().map(|hints| { + let chunks = hints.iter_chunks(start_offset..end_offset); + chunks.filter_map(|(interval, inlay_hint)| { + let on_line = interval.start >= start_offset + && interval.start < end_offset; + on_line.then(|| { let (_, col) = self.buffer.offset_to_line_col(interval.start); - Some((col, inlay_hint)) - } else { - None - } - }, - ) - }) - } else { - None - }; - - let diagnostics = if config.editor.enable_error_lens { - // Is end line a good place to use? - self.diagnostics.as_ref().map(|diags| { - diags.iter().filter(|diag| { - diag.diagnostic.range.end.line as usize == line - && diag.diagnostic.severity < Some(DiagnosticSeverity::HINT) + let text = match &inlay_hint.label { + InlayHintLabel::String(label) => label.to_string(), + InlayHintLabel::LabelParts(parts) => { + parts.iter().map(|p| &p.value).join("") + } + }; + PhantomText { + kind: PhantomTextKind::InlayHint, + col, + text, + fg: Some( + config + .get_color_unchecked( + LapceTheme::INLAY_HINT_FOREGROUND, + ) + .clone(), + ), + font_family: Some( + config.editor.inlay_hint_font_family(), + ), + font_size: Some( + config.editor.inlay_hint_font_size(), + ), + bg: Some( + config + .get_color_unchecked( + LapceTheme::INLAY_HINT_BACKGROUND, + ) + .clone(), + ), + under_line: None, + } + }) + }) }) - }) - } else { - None - }; + }); + let mut text: SmallVec<[PhantomText; 6]> = + hints.into_iter().flatten().collect(); - let ordered_text = hints.into_iter().flatten().collect(); - let end_text = diagnostics.into_iter().flatten().collect(); - PhantomTextLine { - ordered_text, - end_text, + let mut max_severity = None; + let diag_text = + config.editor.enable_error_lens.then_some(()).and_then(|_| { + self.diagnostics.as_ref().map(|diags| { + diags + .iter() + .filter(|diag| { + diag.diagnostic.range.end.line as usize == line + && diag.diagnostic.severity + < Some(DiagnosticSeverity::HINT) + }) + .map(|diag| { + match (diag.diagnostic.severity, max_severity) { + (Some(severity), Some(max)) => { + if severity < max { + max_severity = Some(severity); + } + } + (Some(severity), None) => { + max_severity = Some(severity); + } + _ => {} + } + + let col = self.buffer.line_end_col(line, true); + let fg = { + let severity = diag + .diagnostic + .severity + .unwrap_or(DiagnosticSeverity::WARNING); + let theme_prop = if severity + == DiagnosticSeverity::ERROR + { + LapceTheme::ERROR_LENS_ERROR_FOREGROUND + } else if severity == DiagnosticSeverity::WARNING { + LapceTheme::ERROR_LENS_WARNING_FOREGROUND + } else { + // information + hint (if we keep that) + things without a severity + LapceTheme::ERROR_LENS_OTHER_FOREGROUND + }; + + config.get_color_unchecked(theme_prop).clone() + }; + let text = format!( + " {}", + diag.diagnostic.message.lines().join(" ") + ); + PhantomText { + kind: PhantomTextKind::Diagnostic, + col, + text, + fg: Some(fg), + font_size: Some( + config.editor.error_lens_font_size(), + ), + font_family: Some( + config.editor.error_lens_font_family(), + ), + bg: None, + under_line: None, + } + }) + }) + }); + let mut diag_text: SmallVec<[PhantomText; 6]> = + diag_text.into_iter().flatten().collect(); + + text.append(&mut diag_text); + + if let Some(ime_text) = self.ime_text.as_ref() { + let (ime_line, col, _) = self.ime_pos; + if line == ime_line { + text.push(PhantomText { + kind: PhantomTextKind::Ime, + text: ime_text.to_string(), + col, + font_size: None, + font_family: None, + fg: None, + bg: None, + under_line: Some( + config + .get_color_unchecked(LapceTheme::EDITOR_FOREGROUND) + .clone(), + ), + }); + } } + + text.sort_by(|a, b| { + if a.col == b.col { + a.kind.cmp(&b.kind) + } else { + a.col.cmp(&b.col) + } + }); + + PhantomTextLine { text, max_severity } } fn apply_deltas(&mut self, deltas: &[(RopeDelta, InvalLines)]) { @@ -1617,7 +1691,7 @@ pub fn points_of_line_col( let line = line.min(self.buffer.last_line()); let phantom_text = self.line_phantom_text(config, line); - let col = phantom_text.col_after(col, true); + let col = phantom_text.col_after(col, false); let mut x_shift = 0.0; if font_size < config.editor.font_size { @@ -1783,7 +1857,7 @@ fn new_text_layout( let phantom_text = self.line_phantom_text(config, line); let line_content = - phantom_text.combine_with_text(line_content_original.clone()); + phantom_text.combine_with_text(line_content_original.to_string()); let tab_width = config.tab_width(text, config.editor.font_family(), font_size); @@ -1824,117 +1898,50 @@ fn new_text_layout( } // Give the inlay hints their styling - for (offset, size, _, col) in phantom_text.offset_size_iter() { + for (offset, size, col, phantom) in phantom_text.offset_size_iter() { let start = col + offset; let end = start + size; - layout_builder = layout_builder.range_attribute( - start..end, - TextAttribute::FontSize( - config.editor.inlay_hint_font_size().min(font_size) as f64, - ), - ); - layout_builder = layout_builder.range_attribute( - start..end, - TextAttribute::FontFamily(config.editor.inlay_hint_font_family()), - ); - layout_builder = layout_builder.range_attribute( - start..end, - TextAttribute::TextColor( - config - .get_color_unchecked(LapceTheme::INLAY_HINT_FOREGROUND) - .clone(), - ), - ); + if let Some(fg) = phantom.fg.clone() { + layout_builder = layout_builder + .range_attribute(start..end, TextAttribute::TextColor(fg)); + } + if let Some(font_size) = phantom.font_size { + layout_builder = layout_builder.range_attribute( + start..end, + TextAttribute::FontSize( + config.editor.inlay_hint_font_size().min(font_size) as f64, + ), + ); + } + if let Some(font_family) = phantom.font_family.clone() { + layout_builder = layout_builder.range_attribute( + start..end, + TextAttribute::FontFamily(font_family), + ); + } } - // Add styling to all the diagnostics that appear at the end of the line - for (column, size, entry) in - phantom_text.end_offset_size_iter(&line_content_original) - { - let end = column + size; - - let text_color = { - let severity = entry - .diagnostic - .severity - .unwrap_or(DiagnosticSeverity::WARNING); - let theme_prop = if severity == DiagnosticSeverity::ERROR { - LapceTheme::ERROR_LENS_ERROR_FOREGROUND - } else if severity == DiagnosticSeverity::WARNING { - LapceTheme::ERROR_LENS_WARNING_FOREGROUND - } else { - // information + hint (if we keep that) + things without a severity - LapceTheme::ERROR_LENS_OTHER_FOREGROUND - }; - - config.get_color_unchecked(theme_prop).clone() - }; - - layout_builder = layout_builder.range_attribute( - column..end, - TextAttribute::FontSize( - config.editor.error_lens_font_size().min(font_size) as f64, - ), - ); - layout_builder = layout_builder.range_attribute( - column..end, - TextAttribute::FontFamily(config.editor.error_lens_font_family()), - ); - layout_builder = layout_builder - .range_attribute(column..end, TextAttribute::TextColor(text_color)); - } - - // TODO: error lens background colors - // We could provide an option for whether they should just be around the diagnostic or over the entire line? - let layout_text = layout_builder.build().unwrap(); let mut extra_style = Vec::new(); - for (offset, size, _, col) in phantom_text.offset_size_iter() { - let start = col + offset; - let end = start + size; - let x0 = layout_text.hit_test_text_position(start).point.x; - let x1 = layout_text.hit_test_text_position(end).point.x; - extra_style.push(( - x0, - Some(x1), - LineExtraStyle { - bg_color: Some( - config - .get_color_unchecked(LapceTheme::INLAY_HINT_BACKGROUND) - .clone(), - ), - under_line: None, - }, - )); - } - - let is_error_lens_to_eol = config.editor.error_lens_end_of_line; - - let mut max_severity = None; - let mut end_column = None; - for (column, size, entry) in - phantom_text.end_offset_size_iter(&line_content_original) - { - match (entry.diagnostic.severity, max_severity) { - (Some(severity), Some(max)) => { - if severity < max { - max_severity = Some(severity); - } - } - (Some(severity), None) => { - max_severity = Some(severity); - } - _ => {} - } - - if !is_error_lens_to_eol { - end_column = Some(column + size); + for (offset, size, col, phantom) in phantom_text.offset_size_iter() { + if phantom.bg.is_some() || phantom.under_line.is_some() { + let start = col + offset; + let end = start + size; + let x0 = layout_text.hit_test_text_position(start).point.x; + let x1 = layout_text.hit_test_text_position(end).point.x; + extra_style.push(( + x0, + Some(x1), + LineExtraStyle { + bg_color: phantom.bg.clone(), + under_line: phantom.under_line.clone(), + }, + )); } } - if !phantom_text.end_text.is_empty() { - let max_severity = max_severity.unwrap_or(DiagnosticSeverity::WARNING); + if let Some(max_severity) = phantom_text.max_severity { let theme_prop = if max_severity == DiagnosticSeverity::ERROR { LapceTheme::ERROR_LENS_ERROR_BACKGROUND } else if max_severity == DiagnosticSeverity::WARNING { @@ -1943,10 +1950,12 @@ fn new_text_layout( LapceTheme::ERROR_LENS_OTHER_BACKGROUND }; - // Use the end of the diagnostics if end column exists (which it only will if the config setting is false) - // otherwise None, which is the end of the line in the view - let x1 = end_column - .map(|col| layout_text.hit_test_text_position(col).point.x); + let x1 = config.editor.error_lens_end_of_line.then(|| { + layout_text + .hit_test_text_position(line_content.len()) + .point + .x + }); extra_style.push(( 0.0, diff --git a/lapce-ui/Cargo.toml b/lapce-ui/Cargo.toml index f0c33e42..e6e4ea3d 100644 --- a/lapce-ui/Cargo.toml +++ b/lapce-ui/Cargo.toml @@ -29,11 +29,7 @@ toml_edit = { version = "0.14.4", features = ["easy"] } open = "3.0.2" # lapce deps -druid = { git = "https://github.com/lapce/druid", branch = "shell_opengl", features = [ - "svg", - "im", - "serde", -] } +druid = { git = "https://github.com/lapce/druid", branch = "shell_opengl", features = ["svg", "im", "serde"] } # druid = { path = "../../druid/druid", features = ["svg", "im" , "serde"] } lapce-data = { path = "../lapce-data" } lapce-rpc = { path = "../lapce-rpc" } diff --git a/lapce-ui/src/editor.rs b/lapce-ui/src/editor.rs index f8b5491a..3cbb70f2 100644 --- a/lapce-ui/src/editor.rs +++ b/lapce-ui/src/editor.rs @@ -885,6 +885,14 @@ fn paint_text( bg, ); } + if let Some(under_line) = &style.under_line { + let x1 = x1.unwrap_or(self_size.width); + let line = Line::new( + Point::new(*x0, y + height), + Point::new(x1, y + height), + ); + ctx.stroke(line, under_line, 1.0); + } } if let Some(whitespace) = &text_layout.whitespace { @@ -917,6 +925,19 @@ fn paint_cursor_caret( phantom_text.col_after(col, false) }; + let col = data + .doc + .ime_text() + .map(|_| { + let (ime_line, _, shift) = data.doc.ime_pos(); + if ime_line == line { + col + shift + } else { + col + } + }) + .unwrap_or(col); + let x0 = data .doc .line_point_of_line_col(ctx.text(), line, col, font_size, &data.config) diff --git a/lapce-ui/src/editor/view.rs b/lapce-ui/src/editor/view.rs index d161f99c..cd4cb6e0 100644 --- a/lapce-ui/src/editor/view.rs +++ b/lapce-ui/src/editor/view.rs @@ -32,6 +32,7 @@ container::LapceEditorContainer, header::LapceEditorHeader, LapceEditor, }, find::FindBox, + ime::ImeComponent, plugin::PluginInfo, settings::LapceSettingsPanel, }; @@ -46,6 +47,7 @@ pub struct LapceEditorView { last_idle_timer: TimerToken, display_border: bool, background_color_name: &'static str, + ime: ImeComponent, } pub fn editor_tab_child_widget( @@ -99,6 +101,7 @@ pub fn new( last_idle_timer: TimerToken::INVALID, display_border: true, background_color_name: LapceTheme::EDITOR_BACKGROUND, + ime: ImeComponent::default(), } } @@ -680,28 +683,52 @@ fn event( match event { Event::KeyDown(key_event) => { ctx.set_handled(); - let mut keypress = data.keypress.clone(); - if Arc::make_mut(&mut keypress).key_down( - ctx, - key_event, - &mut editor_data, - env, - ) { - self.ensure_cursor_visible( + if key_event.is_composing { + if data.config.editor.blink_interval > 0 { + self.cursor_blink_timer = ctx.request_timer( + Duration::from_millis(data.config.editor.blink_interval), + None, + ); + *editor_data.editor.last_cursor_instant.borrow_mut() = + Instant::now(); + } + if let Some(text) = self.ime.get_input_text() { + Arc::make_mut(&mut editor_data.doc).clear_ime_text(); + editor_data.receive_char(ctx, &text); + } else if !self.ime.borrow().text().is_empty() { + let offset = editor_data.editor.cursor.offset(); + let (line, col) = + editor_data.doc.buffer().offset_to_line_col(offset); + let doc = Arc::make_mut(&mut editor_data.doc); + doc.set_ime_pos(line, col, self.ime.get_shift()); + doc.set_ime_text(self.ime.borrow().text().to_string()); + } else { + Arc::make_mut(&mut editor_data.doc).clear_ime_text(); + } + } else { + Arc::make_mut(&mut editor_data.doc).clear_ime_text(); + let mut keypress = data.keypress.clone(); + if Arc::make_mut(&mut keypress).key_down( ctx, - &editor_data, - &data.panel, - None, + key_event, + &mut editor_data, env, + ) { + self.ensure_cursor_visible( + ctx, + &editor_data, + &data.panel, + None, + env, + ); + } + editor_data.sync_buffer_position( + self.editor.widget().editor.widget().inner().offset(), ); - } - editor_data.sync_buffer_position( - self.editor.widget().editor.widget().inner().offset(), - ); - editor_data.get_code_actions(ctx); + editor_data.get_code_actions(ctx); - data.keypress = keypress.clone(); - ctx.set_handled(); + data.keypress = keypress.clone(); + } } Event::Command(cmd) if cmd.is(LAPCE_COMMAND) => { let command = cmd.get_unchecked(LAPCE_COMMAND); @@ -771,6 +798,13 @@ fn lifecycle( Target::Widget(editor.view_id), )); } + ctx.register_text_input(self.ime.ime_handler()); + let editor = data.main_split.editors.get(&self.view_id).unwrap(); + if editor.cursor.is_insert() { + self.ime.set_active(true); + } else { + self.ime.set_active(false); + } } LifeCycle::FocusChanged(is_focus) => { let editor = data.main_split.editors.get(&self.view_id).unwrap(); @@ -811,22 +845,37 @@ fn lifecycle( } _ => {} } - } else if editor.content.is_palette() - && data.palette.status == PaletteStatus::Inactive - { - let cmd = if data.workspace.path.is_none() { - LapceWorkbenchCommand::PaletteWorkspace - } else { - LapceWorkbenchCommand::Palette - }; - ctx.submit_command(Command::new( - LAPCE_COMMAND, - LapceCommand { - kind: CommandKind::Workbench(cmd), - data: None, - }, - Target::Auto, - )); + } else { + let editor_data = data.editor_view_content(self.view_id); + let offset = editor_data.editor.cursor.offset(); + let (_, origin) = editor_data.doc.points_of_offset( + ctx.text(), + offset, + &editor_data.editor.view, + &editor_data.config, + ); + self.ime.set_origin( + *editor_data.editor.window_origin.borrow() + + (origin.x, origin.y), + ); + + if editor.content.is_palette() + && data.palette.status == PaletteStatus::Inactive + { + let cmd = if data.workspace.path.is_none() { + LapceWorkbenchCommand::PaletteWorkspace + } else { + LapceWorkbenchCommand::Palette + }; + ctx.submit_command(Command::new( + LAPCE_COMMAND, + LapceCommand { + kind: CommandKind::Workbench(cmd), + data: None, + }, + Target::Auto, + )); + } } } LifeCycle::HotChanged(is_hot) => { @@ -871,14 +920,15 @@ fn update( } } + let offset = editor_data.editor.cursor.offset(); + let old_offset = old_editor_data.editor.cursor.offset(); + if data.config.editor.blink_interval > 0 && *data.focus == self.view_id { let reset = if *old_data.focus != self.view_id { true } else { let mode = editor_data.editor.cursor.get_mode(); let old_mode = old_editor_data.editor.cursor.get_mode(); - let offset = editor_data.editor.cursor.offset(); - let old_offset = old_editor_data.editor.cursor.offset(); let (line, col) = editor_data.doc.buffer().offset_to_line_col(offset); let (old_line, old_col) = @@ -954,6 +1004,40 @@ fn update( } } + let mut update_ime_origin = false; + match ( + old_editor_data.editor.cursor.is_insert(), + editor_data.editor.cursor.is_insert(), + ) { + (true, false) => { + self.ime.set_active(false); + } + (false, true) => { + self.ime.set_active(true); + update_ime_origin = true; + } + (false, false) | (true, true) => {} + } + + if offset != old_offset + || editor_data.editor.scroll_offset + != old_editor_data.editor.scroll_offset + { + update_ime_origin = true; + } + + if update_ime_origin { + let (_, origin) = editor_data.doc.points_of_offset( + ctx.text(), + offset, + &editor_data.editor.view, + &editor_data.config, + ); + self.ime.set_origin( + *editor_data.editor.window_origin.borrow() + (origin.x, origin.y), + ); + } + if editor_data.editor.content != old_editor_data.editor.content { ctx.request_layout(); } diff --git a/lapce-ui/src/ime.rs b/lapce-ui/src/ime.rs new file mode 100644 index 00000000..aa753909 --- /dev/null +++ b/lapce-ui/src/ime.rs @@ -0,0 +1,223 @@ +use std::{ + cell::{Cell, Ref, RefCell, RefMut}, + ops::Range, + sync::{Arc, Weak}, +}; + +use druid::{ + piet::HitTestPoint, + text::{EditableText, ImeHandlerRef, InputHandler, Selection}, + Point, Rect, +}; + +#[derive(Debug, Clone, Copy, PartialEq)] +enum ImeLock { + None, + ReadWrite, + Read, +} + +pub struct ImeComponent { + ime_session: Arc>, + lock: Arc>, +} + +impl Default for ImeComponent { + fn default() -> Self { + let session = ImeSession { + is_active: false, + composition_range: None, + text: "".to_string(), + input_text: None, + orgin: Point::ZERO, + shift: 0, + }; + ImeComponent { + ime_session: Arc::new(RefCell::new(session)), + lock: Arc::new(Cell::new(ImeLock::None)), + } + } +} + +impl ImeComponent { + pub fn ime_handler(&self) -> impl ImeHandlerRef { + ImeSessionRef { + inner: Arc::downgrade(&self.ime_session), + lock: self.lock.clone(), + } + } + + /// Returns `true` if the inner [`ImeSession`] can be read. + pub fn can_read(&self) -> bool { + self.lock.get() != ImeLock::ReadWrite + } + + pub fn borrow(&self) -> Ref<'_, ImeSession> { + self.ime_session.borrow() + } + + pub fn borrow_mut(&self) -> RefMut<'_, ImeSession> { + self.ime_session.borrow_mut() + } + + pub fn set_origin(&self, origin: Point) { + self.ime_session.borrow_mut().orgin = origin; + } + + pub fn set_active(&mut self, active: bool) { + self.ime_session.borrow_mut().is_active = active; + } + + pub fn clear_text(&self) { + self.ime_session.borrow_mut().text.clear(); + } + + pub fn get_input_text(&self) -> Option { + self.ime_session.borrow_mut().input_text.take() + } + + pub fn get_shift(&self) -> usize { + self.ime_session.borrow().shift + } + + /// Returns `true` if the IME is actively composing (or the text is locked.) + pub fn is_composing(&self) -> bool { + self.can_read() && self.borrow().composition_range.is_some() + } +} + +impl ImeHandlerRef for ImeSessionRef { + fn is_alive(&self) -> bool { + Weak::strong_count(&self.inner) > 0 + } + + fn acquire( + &self, + mutable: bool, + ) -> Option> { + let lock = if mutable { + ImeLock::ReadWrite + } else { + ImeLock::Read + }; + self.lock.replace(lock); + Weak::upgrade(&self.inner) + .map(ImeSessionHandle::new) + .map(|doc| Box::new(doc) as Box) + } + + fn release(&self) -> bool { + self.lock.replace(ImeLock::None) == ImeLock::ReadWrite + } +} + +struct ImeSessionRef { + inner: Weak>, + lock: Arc>, +} + +pub struct ImeSession { + is_active: bool, + /// The portion of the text that is currently marked by the IME. + composition_range: Option>, + text: String, + input_text: Option, + shift: usize, + orgin: Point, +} + +impl ImeSession { + pub fn text(&self) -> &str { + &self.text + } +} + +struct ImeSessionHandle { + inner: Arc>, + selection: Selection, + text: String, +} + +impl ImeSessionHandle { + fn new(inner: Arc>) -> Self { + let text = inner.borrow().text.clone(); + ImeSessionHandle { + inner, + text, + selection: Selection::default(), + } + } +} + +impl InputHandler for ImeSessionHandle { + fn selection(&self) -> Selection { + self.selection + } + + fn set_selection(&mut self, selection: Selection) { + self.selection = selection; + self.inner.borrow_mut().shift = selection.active; + } + + fn composition_range(&self) -> Option> { + self.inner.borrow().composition_range.clone() + } + + fn set_composition_range(&mut self, range: Option>) { + if range.is_none() { + self.inner.borrow_mut().text.clear(); + self.text.clear(); + } + self.inner.borrow_mut().composition_range = range; + } + + fn is_char_boundary(&self, i: usize) -> bool { + self.text.cursor(i).is_some() + } + + fn len(&self) -> usize { + self.text.len() + } + + fn slice(&self, range: std::ops::Range) -> std::borrow::Cow { + self.text.slice(range).unwrap() + } + + fn insert_text(&mut self, text: &str) { + if self.composition_range().is_some() { + self.inner.borrow_mut().input_text = Some(text.to_string()); + } + self.replace_range(0..0, ""); + } + + fn is_active(&self) -> bool { + self.inner.borrow().is_active + } + + fn replace_range(&mut self, _range: Range, text: &str) { + self.inner.borrow_mut().text = text.to_string(); + self.text = self.inner.borrow().text.clone(); + } + + fn hit_test_point(&self, _point: Point) -> HitTestPoint { + HitTestPoint::default() + } + + fn line_range( + &self, + _index: usize, + _affinity: druid::text::Affinity, + ) -> std::ops::Range { + 0..self.len() + } + + fn bounding_box(&self) -> Option { + None + } + + fn slice_bounding_box(&self, _range: std::ops::Range) -> Option { + Some(Rect::ZERO.with_origin(self.inner.borrow().orgin)) + } + + fn handle_action(&mut self, _action: druid::text::TextAction) {} +} diff --git a/lapce-ui/src/lib.rs b/lapce-ui/src/lib.rs index 37eba77b..1dbc39e7 100644 --- a/lapce-ui/src/lib.rs +++ b/lapce-ui/src/lib.rs @@ -7,6 +7,7 @@ pub mod explorer; pub mod find; pub mod hover; +pub mod ime; pub mod keymap; pub mod list; mod logging;