mirror of https://github.com/lapce/lapce.git
IME Support for macOS (#1440)
This commit is contained in:
parent
278618b191
commit
7c993f9435
|
@ -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]
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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) {}
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
pub mod explorer;
|
||||
pub mod find;
|
||||
pub mod hover;
|
||||
pub mod ime;
|
||||
pub mod keymap;
|
||||
pub mod list;
|
||||
mod logging;
|
||||
|
|
Loading…
Reference in New Issue