IME Support for macOS (#1440)

This commit is contained in:
Dongdong Zhou 2022-10-05 22:34:32 +01:00 committed by GitHub
parent 278618b191
commit 7c993f9435
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 652 additions and 296 deletions

22
CHANGELOG.md Normal file
View File

@ -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]

6
Cargo.lock generated
View File

@ -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",

View File

@ -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<usize>,
font_family: Option<FontFamily>,
fg: Option<Color>,
bg: Option<Color>,
under_line: Option<Color>,
}
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<DiagnosticSeverity>,
}
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<Item = (usize, usize, &'hint InlayHint, usize)> + '_ {
) -> impl Iterator<Item = (usize, usize, usize, &PhantomText)> + '_ {
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<Item = (usize, usize, &'diag EditorDiagnostic)> + '_ {
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::<usize>();
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<usize, (PluginId, CodeActionResponse)>,
pub inlay_hints: Option<Spans<InlayHint>>,
pub diagnostics: Option<Arc<Vec<EditorDiagnostic>>>,
ime_text: Option<Arc<String>>,
ime_pos: (usize, usize, usize),
pub syntax_selection_range: Option<SyntaxSelectionRanges>,
pub find: Rc<RefCell<Find>>,
find_progress: Rc<RefCell<FindProgress>>,
@ -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<String>> {
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,

View File

@ -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" }

View File

@ -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)

View File

@ -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();
}

223
lapce-ui/src/ime.rs Normal file
View File

@ -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<RefCell<ImeSession>>,
lock: Arc<Cell<ImeLock>>,
}
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<String> {
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<Box<dyn druid::text::InputHandler + 'static>> {
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<dyn InputHandler>)
}
fn release(&self) -> bool {
self.lock.replace(ImeLock::None) == ImeLock::ReadWrite
}
}
struct ImeSessionRef {
inner: Weak<RefCell<ImeSession>>,
lock: Arc<Cell<ImeLock>>,
}
pub struct ImeSession {
is_active: bool,
/// The portion of the text that is currently marked by the IME.
composition_range: Option<Range<usize>>,
text: String,
input_text: Option<String>,
shift: usize,
orgin: Point,
}
impl ImeSession {
pub fn text(&self) -> &str {
&self.text
}
}
struct ImeSessionHandle {
inner: Arc<RefCell<ImeSession>>,
selection: Selection,
text: String,
}
impl ImeSessionHandle {
fn new(inner: Arc<RefCell<ImeSession>>) -> 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<std::ops::Range<usize>> {
self.inner.borrow().composition_range.clone()
}
fn set_composition_range(&mut self, range: Option<std::ops::Range<usize>>) {
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<usize>) -> std::borrow::Cow<str> {
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<usize>, 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<usize> {
0..self.len()
}
fn bounding_box(&self) -> Option<Rect> {
None
}
fn slice_bounding_box(&self, _range: std::ops::Range<usize>) -> Option<Rect> {
Some(Rect::ZERO.with_origin(self.inner.borrow().orgin))
}
fn handle_action(&mut self, _action: druid::text::TextAction) {}
}

View File

@ -7,6 +7,7 @@
pub mod explorer;
pub mod find;
pub mod hover;
pub mod ime;
pub mod keymap;
pub mod list;
mod logging;