use core::indent::IndentStyle;
use std::{
cell::{Cell, RefCell},
cmp::Ordering,
collections::{hash_map::DefaultHasher, HashMap},
hash::{Hash, Hasher},
rc::Rc,
sync::Arc,
time::Duration,
};
use crate::{
action::{exec_after, TimerToken},
keyboard::Modifiers,
kurbo::{Point, Rect, Vec2},
peniko::Color,
pointer::{PointerButton, PointerInputEvent, PointerMoveEvent},
prop, prop_extractor,
reactive::{batch, untrack, ReadSignal, RwSignal, Scope},
style::{CursorColor, StylePropValue, TextColor},
text::{Attrs, AttrsList, LineHeightValue, TextLayout, Wrap},
view::{IntoView, View},
views::text,
};
use floem_editor_core::{
buffer::rope_text::{RopeText, RopeTextVal},
command::MoveCommand,
cursor::{ColPosition, Cursor, CursorAffinity, CursorMode},
mode::Mode,
movement::Movement,
register::Register,
selection::Selection,
soft_tab::{snap_to_soft_tab_line_col, SnapDirection},
};
use floem_reactive::{SignalGet, SignalTrack, SignalUpdate, SignalWith, Trigger};
use lapce_xi_rope::Rope;
pub mod actions;
pub mod color;
pub mod command;
pub mod gutter;
pub mod id;
pub mod keypress;
pub mod layout;
pub mod listener;
pub mod movement;
pub mod phantom_text;
pub mod text;
pub mod text_document;
pub mod view;
pub mod visual_line;
pub use floem_editor_core as core;
use peniko::Brush;
use self::{
command::Command,
id::EditorId,
layout::TextLayoutLine,
phantom_text::PhantomTextLine,
text::{Document, Preedit, PreeditData, RenderWhitespace, Styling, WrapMethod},
view::{LineInfo, ScreenLines, ScreenLinesBase},
visual_line::{
hit_position_aff, ConfigId, FontSizeCacheId, LayoutEvent, LineFontSizeProvider, Lines,
RVLine, ResolvedWrap, TextLayoutProvider, VLine, VLineInfo,
},
};
prop!(pub WrapProp: WrapMethod {} = WrapMethod::EditorWidth);
impl StylePropValue for WrapMethod {
fn debug_view(&self) -> Option<Box<dyn View>> {
Some(crate::views::text(self).into_any())
}
}
prop!(pub CursorSurroundingLines: usize {} = 1);
prop!(pub ScrollBeyondLastLine: bool {} = false);
prop!(pub ShowIndentGuide: bool {} = false);
prop!(pub Modal: bool {} = false);
prop!(pub ModalRelativeLine: bool {} = false);
prop!(pub SmartTab: bool {} = false);
prop!(pub PhantomColor: Color {} = Color::DIM_GRAY);
prop!(pub PlaceholderColor: Color {} = Color::DIM_GRAY);
prop!(pub PreeditUnderlineColor: Color {} = Color::WHITE);
prop!(pub RenderWhitespaceProp: RenderWhitespace {} = RenderWhitespace::None);
impl StylePropValue for RenderWhitespace {
fn debug_view(&self) -> Option<Box<dyn View>> {
Some(crate::views::text(self).into_any())
}
}
prop!(pub IndentStyleProp: IndentStyle {} = IndentStyle::Spaces(4));
impl StylePropValue for IndentStyle {
fn debug_view(&self) -> Option<Box<dyn View>> {
Some(text(self).into_any())
}
}
prop!(pub DropdownShadow: Option<Color> {} = None);
prop!(pub Foreground: Color { inherited } = Color::rgb8(0x38, 0x3A, 0x42));
prop!(pub Focus: Option<Color> {} = None);
prop!(pub SelectionColor: Color {} = Color::BLACK.multiply_alpha(0.5));
prop!(pub CurrentLineColor: Option<Color> { } = None);
prop!(pub Link: Option<Color> {} = None);
prop!(pub VisibleWhitespaceColor: Color {} = Color::TRANSPARENT);
prop!(pub IndentGuideColor: Color {} = Color::TRANSPARENT);
prop!(pub StickyHeaderBackground: Option<Color> {} = None);
prop_extractor! {
pub EditorStyle {
pub text_color: TextColor,
pub phantom_color: PhantomColor,
pub placeholder_color: PlaceholderColor,
pub preedit_underline_color: PreeditUnderlineColor,
pub show_indent_guide: ShowIndentGuide,
pub modal: Modal,
pub modal_relative_line: ModalRelativeLine,
pub smart_tab: SmartTab,
pub wrap_method: WrapProp,
pub cursor_surrounding_lines: CursorSurroundingLines,
pub render_whitespace: RenderWhitespaceProp,
pub indent_style: IndentStyleProp,
pub caret: CursorColor,
pub selection: SelectionColor,
pub current_line: CurrentLineColor,
pub visible_whitespace: VisibleWhitespaceColor,
pub indent_guide: IndentGuideColor,
pub scroll_beyond_last_line: ScrollBeyondLastLine,
}
}
impl EditorStyle {
fn ed_text_color(&self) -> Color {
self.text_color().unwrap_or(Color::BLACK)
}
}
impl EditorStyle {
pub fn ed_caret(&self) -> Brush {
self.caret()
}
}
pub(crate) const CHAR_WIDTH: f64 = 7.5;
#[derive(Clone)]
pub struct Editor {
pub cx: Cell<Scope>,
effects_cx: Cell<Scope>,
id: EditorId,
pub active: RwSignal<bool>,
pub read_only: RwSignal<bool>,
pub(crate) doc: RwSignal<Rc<dyn Document>>,
pub(crate) style: RwSignal<Rc<dyn Styling>>,
pub cursor: RwSignal<Cursor>,
pub window_origin: RwSignal<Point>,
pub viewport: RwSignal<Rect>,
pub parent_size: RwSignal<Rect>,
pub editor_view_focused: Trigger,
pub editor_view_focus_lost: Trigger,
pub editor_view_id: RwSignal<Option<crate::id::ViewId>>,
pub scroll_delta: RwSignal<Vec2>,
pub scroll_to: RwSignal<Option<Vec2>>,
lines: Rc<Lines>,
pub screen_lines: RwSignal<ScreenLines>,
pub register: RwSignal<Register>,
pub cursor_info: CursorInfo,
pub last_movement: RwSignal<Movement>,
pub ime_allowed: RwSignal<bool>,
pub es: RwSignal<EditorStyle>,
pub floem_style_id: RwSignal<u64>,
}
impl Editor {
pub fn new(cx: Scope, doc: Rc<dyn Document>, style: Rc<dyn Styling>, modal: bool) -> Editor {
let id = EditorId::next();
Editor::new_id(cx, id, doc, style, modal)
}
pub fn new_id(
cx: Scope,
id: EditorId,
doc: Rc<dyn Document>,
style: Rc<dyn Styling>,
modal: bool,
) -> Editor {
let editor = Editor::new_direct(cx, id, doc, style, modal);
editor.recreate_view_effects();
editor
}
pub fn new_direct(
cx: Scope,
id: EditorId,
doc: Rc<dyn Document>,
style: Rc<dyn Styling>,
modal: bool,
) -> Editor {
let cx = cx.create_child();
let viewport = cx.create_rw_signal(Rect::ZERO);
let cursor_mode = if modal {
CursorMode::Normal(0)
} else {
CursorMode::Insert(Selection::caret(0))
};
let cursor = Cursor::new(cursor_mode, None, None);
let cursor = cx.create_rw_signal(cursor);
let doc = cx.create_rw_signal(doc);
let style = cx.create_rw_signal(style);
let font_sizes = RefCell::new(Rc::new(EditorFontSizes {
id,
style: style.read_only(),
doc: doc.read_only(),
}));
let lines = Rc::new(Lines::new(cx, font_sizes));
let screen_lines = cx.create_rw_signal(ScreenLines::new(cx, viewport.get_untracked()));
let editor_style = cx.create_rw_signal(EditorStyle::default());
let ed = Editor {
cx: Cell::new(cx),
effects_cx: Cell::new(cx.create_child()),
id,
active: cx.create_rw_signal(false),
read_only: cx.create_rw_signal(false),
doc,
style,
cursor,
window_origin: cx.create_rw_signal(Point::ZERO),
viewport,
parent_size: cx.create_rw_signal(Rect::ZERO),
scroll_delta: cx.create_rw_signal(Vec2::ZERO),
scroll_to: cx.create_rw_signal(None),
editor_view_focused: cx.create_trigger(),
editor_view_focus_lost: cx.create_trigger(),
editor_view_id: cx.create_rw_signal(None),
lines,
screen_lines,
register: cx.create_rw_signal(Register::default()),
cursor_info: CursorInfo::new(cx),
last_movement: cx.create_rw_signal(Movement::Left),
ime_allowed: cx.create_rw_signal(false),
es: editor_style,
floem_style_id: cx.create_rw_signal(0),
};
create_view_effects(ed.effects_cx.get(), &ed);
ed
}
pub fn id(&self) -> EditorId {
self.id
}
pub fn doc(&self) -> Rc<dyn Document> {
self.doc.get_untracked()
}
pub fn doc_track(&self) -> Rc<dyn Document> {
self.doc.get()
}
pub fn doc_signal(&self) -> RwSignal<Rc<dyn Document>> {
self.doc
}
pub fn config_id(&self) -> ConfigId {
let style_id = self.style.with(|s| s.id());
let floem_style_id = self.floem_style_id;
ConfigId::new(style_id, floem_style_id.get_untracked())
}
pub fn recreate_view_effects(&self) {
batch(|| {
self.effects_cx.get().dispose();
self.effects_cx.set(self.cx.get().create_child());
create_view_effects(self.effects_cx.get(), self);
});
}
pub fn update_doc(&self, doc: Rc<dyn Document>, styling: Option<Rc<dyn Styling>>) {
batch(|| {
self.effects_cx.get().dispose();
*self.lines.font_sizes.borrow_mut() = Rc::new(EditorFontSizes {
id: self.id(),
style: self.style.read_only(),
doc: self.doc.read_only(),
});
self.lines.clear(0, None);
self.doc.set(doc);
if let Some(styling) = styling {
self.style.set(styling);
}
self.screen_lines.update(|screen_lines| {
screen_lines.clear(self.viewport.get_untracked());
});
self.effects_cx.set(self.cx.get().create_child());
create_view_effects(self.effects_cx.get(), self);
});
}
pub fn update_styling(&self, styling: Rc<dyn Styling>) {
batch(|| {
self.effects_cx.get().dispose();
*self.lines.font_sizes.borrow_mut() = Rc::new(EditorFontSizes {
id: self.id(),
style: self.style.read_only(),
doc: self.doc.read_only(),
});
self.lines.clear(0, None);
self.style.set(styling);
self.screen_lines.update(|screen_lines| {
screen_lines.clear(self.viewport.get_untracked());
});
self.effects_cx.set(self.cx.get().create_child());
create_view_effects(self.effects_cx.get(), self);
});
}
pub fn duplicate(&self, editor_id: Option<EditorId>) -> Editor {
let doc = self.doc();
let style = self.style();
let mut editor = Editor::new_direct(
self.cx.get(),
editor_id.unwrap_or_else(EditorId::next),
doc,
style,
false,
);
batch(|| {
editor.read_only.set(self.read_only.get_untracked());
editor.es.set(self.es.get_untracked());
editor
.floem_style_id
.set(self.floem_style_id.get_untracked());
editor.cursor.set(self.cursor.get_untracked());
editor.scroll_delta.set(self.scroll_delta.get_untracked());
editor.scroll_to.set(self.scroll_to.get_untracked());
editor.window_origin.set(self.window_origin.get_untracked());
editor.viewport.set(self.viewport.get_untracked());
editor.parent_size.set(self.parent_size.get_untracked());
editor.register.set(self.register.get_untracked());
editor.cursor_info = self.cursor_info.clone();
editor.last_movement.set(self.last_movement.get_untracked());
});
editor.recreate_view_effects();
editor
}
pub fn style(&self) -> Rc<dyn Styling> {
self.style.get_untracked()
}
pub fn text(&self) -> Rope {
self.doc().text()
}
pub fn rope_text(&self) -> RopeTextVal {
self.doc().rope_text()
}
pub fn lines(&self) -> &Lines {
&self.lines
}
pub fn text_prov(&self) -> &Self {
self
}
fn preedit(&self) -> PreeditData {
self.doc.with_untracked(|doc| doc.preedit())
}
pub fn set_preedit(&self, text: String, cursor: Option<(usize, usize)>, offset: usize) {
batch(|| {
self.preedit().preedit.set(Some(Preedit {
text,
cursor,
offset,
}));
self.doc().cache_rev().update(|cache_rev| {
*cache_rev += 1;
});
});
}
pub fn clear_preedit(&self) {
let preedit = self.preedit();
if preedit.preedit.with_untracked(|preedit| preedit.is_none()) {
return;
}
batch(|| {
preedit.preedit.set(None);
self.doc().cache_rev().update(|cache_rev| {
*cache_rev += 1;
});
});
}
pub fn receive_char(&self, c: &str) {
self.doc().receive_char(self, c)
}
fn compute_screen_lines(&self, base: RwSignal<ScreenLinesBase>) -> ScreenLines {
self.doc().compute_screen_lines(self, base)
}
pub fn pointer_down(&self, pointer_event: &PointerInputEvent) {
match pointer_event.button {
PointerButton::Primary => {
self.active.set(true);
self.left_click(pointer_event);
}
PointerButton::Secondary => {
self.right_click(pointer_event);
}
_ => {}
}
}
pub fn left_click(&self, pointer_event: &PointerInputEvent) {
match pointer_event.count {
1 => {
self.single_click(pointer_event);
}
2 => {
self.double_click(pointer_event);
}
3 => {
self.triple_click(pointer_event);
}
_ => {}
}
}
pub fn single_click(&self, pointer_event: &PointerInputEvent) {
let mode = self.cursor.with_untracked(|c| c.get_mode());
let (new_offset, _) = self.offset_of_point(mode, pointer_event.pos);
self.cursor.update(|cursor| {
cursor.set_offset(
new_offset,
pointer_event.modifiers.shift(),
pointer_event.modifiers.alt(),
)
});
}
pub fn double_click(&self, pointer_event: &PointerInputEvent) {
let mode = self.cursor.with_untracked(|c| c.get_mode());
let (mouse_offset, _) = self.offset_of_point(mode, pointer_event.pos);
let (start, end) = self.select_word(mouse_offset);
self.cursor.update(|cursor| {
cursor.add_region(
start,
end,
pointer_event.modifiers.shift(),
pointer_event.modifiers.alt(),
)
});
}
pub fn triple_click(&self, pointer_event: &PointerInputEvent) {
let mode = self.cursor.with_untracked(|c| c.get_mode());
let (mouse_offset, _) = self.offset_of_point(mode, pointer_event.pos);
let line = self.line_of_offset(mouse_offset);
let start = self.offset_of_line(line);
let end = self.offset_of_line(line + 1);
self.cursor.update(|cursor| {
cursor.add_region(
start,
end,
pointer_event.modifiers.shift(),
pointer_event.modifiers.alt(),
)
});
}
pub fn pointer_move(&self, pointer_event: &PointerMoveEvent) {
let mode = self.cursor.with_untracked(|c| c.get_mode());
let (offset, _is_inside) = self.offset_of_point(mode, pointer_event.pos);
if self.active.get_untracked() && self.cursor.with_untracked(|c| c.offset()) != offset {
self.cursor
.update(|cursor| cursor.set_offset(offset, true, pointer_event.modifiers.alt()));
}
}
pub fn pointer_up(&self, _pointer_event: &PointerInputEvent) {
self.active.set(false);
}
fn right_click(&self, pointer_event: &PointerInputEvent) {
let mode = self.cursor.with_untracked(|c| c.get_mode());
let (offset, _) = self.offset_of_point(mode, pointer_event.pos);
let doc = self.doc();
let pointer_inside_selection = self
.cursor
.with_untracked(|c| c.edit_selection(&doc.rope_text()).contains(offset));
if !pointer_inside_selection {
self.single_click(pointer_event);
}
}
pub fn page_move(&self, down: bool, mods: Modifiers) {
let viewport = self.viewport.get_untracked();
let line_height = f64::from(self.line_height(0));
let lines = (viewport.height() / line_height / 2.0).round() as usize;
let distance = (lines as f64) * line_height;
self.scroll_delta
.set(Vec2::new(0.0, if down { distance } else { -distance }));
let cmd = if down {
MoveCommand::Down
} else {
MoveCommand::Up
};
let cmd = Command::Move(cmd);
self.doc().run_command(self, &cmd, Some(lines), mods);
}
pub fn center_window(&self) {
let viewport = self.viewport.get_untracked();
let line_height = f64::from(self.line_height(0));
let offset = self.cursor.with_untracked(|cursor| cursor.offset());
let (line, _col) = self.offset_to_line_col(offset);
let viewport_center = viewport.height() / 2.0;
let current_line_position = line as f64 * line_height;
let desired_top = current_line_position - viewport_center + (line_height / 2.0);
let scroll_delta = desired_top - viewport.y0;
self.scroll_delta.set(Vec2::new(0.0, scroll_delta));
}
pub fn top_of_window(&self, scroll_off: usize) {
let viewport = self.viewport.get_untracked();
let line_height = f64::from(self.line_height(0));
let offset = self.cursor.with_untracked(|cursor| cursor.offset());
let (line, _col) = self.offset_to_line_col(offset);
let desired_top = (line.saturating_sub(scroll_off)) as f64 * line_height;
let scroll_delta = desired_top - viewport.y0;
self.scroll_delta.set(Vec2::new(0.0, scroll_delta));
}
pub fn bottom_of_window(&self, scroll_off: usize) {
let viewport = self.viewport.get_untracked();
let line_height = f64::from(self.line_height(0));
let offset = self.cursor.with_untracked(|cursor| cursor.offset());
let (line, _col) = self.offset_to_line_col(offset);
let desired_bottom = (line + scroll_off + 1) as f64 * line_height - viewport.height();
let scroll_delta = desired_bottom - viewport.y0;
self.scroll_delta.set(Vec2::new(0.0, scroll_delta));
}
pub fn scroll(&self, top_shift: f64, down: bool, count: usize, mods: Modifiers) {
let viewport = self.viewport.get_untracked();
let line_height = f64::from(self.line_height(0));
let diff = line_height * count as f64;
let diff = if down { diff } else { -diff };
let offset = self.cursor.with_untracked(|cursor| cursor.offset());
let (line, _col) = self.offset_to_line_col(offset);
let top = viewport.y0 + diff + top_shift;
let bottom = viewport.y0 + diff + viewport.height();
let new_line = if (line + 1) as f64 * line_height + line_height > bottom {
let line = (bottom / line_height).floor() as usize;
if line > 2 {
line - 2
} else {
0
}
} else if line as f64 * line_height - line_height < top {
let line = (top / line_height).ceil() as usize;
line + 1
} else {
line
};
self.scroll_delta.set(Vec2::new(0.0, diff));
let res = match new_line.cmp(&line) {
Ordering::Greater => Some((MoveCommand::Down, new_line - line)),
Ordering::Less => Some((MoveCommand::Up, line - new_line)),
_ => None,
};
if let Some((cmd, count)) = res {
let cmd = Command::Move(cmd);
self.doc().run_command(self, &cmd, Some(count), mods);
}
}
pub fn phantom_text(&self, line: usize) -> PhantomTextLine {
self.doc()
.phantom_text(self.id(), &self.es.get_untracked(), line)
}
pub fn line_height(&self, line: usize) -> f32 {
self.style().line_height(self.id(), line)
}
pub fn iter_vlines(
&self,
backwards: bool,
start: VLine,
) -> impl Iterator<Item = VLineInfo> + '_ {
self.lines.iter_vlines(self.text_prov(), backwards, start)
}
pub fn iter_vlines_over(
&self,
backwards: bool,
start: VLine,
end: VLine,
) -> impl Iterator<Item = VLineInfo> + '_ {
self.lines
.iter_vlines_over(self.text_prov(), backwards, start, end)
}
pub fn iter_rvlines(
&self,
backwards: bool,
start: RVLine,
) -> impl Iterator<Item = VLineInfo<()>> + '_ {
self.lines.iter_rvlines(self.text_prov(), backwards, start)
}
pub fn iter_rvlines_over(
&self,
backwards: bool,
start: RVLine,
end_line: usize,
) -> impl Iterator<Item = VLineInfo<()>> + '_ {
self.lines
.iter_rvlines_over(self.text_prov(), backwards, start, end_line)
}
pub fn first_rvline_info(&self) -> VLineInfo<()> {
self.rvline_info(RVLine::default())
}
pub fn num_lines(&self) -> usize {
self.rope_text().num_lines()
}
pub fn last_line(&self) -> usize {
self.rope_text().last_line()
}
pub fn last_vline(&self) -> VLine {
self.lines.last_vline(self.text_prov())
}
pub fn last_rvline(&self) -> RVLine {
self.lines.last_rvline(self.text_prov())
}
pub fn last_rvline_info(&self) -> VLineInfo<()> {
self.rvline_info(self.last_rvline())
}
pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) {
self.rope_text().offset_to_line_col(offset)
}
pub fn offset_of_line(&self, offset: usize) -> usize {
self.rope_text().offset_of_line(offset)
}
pub fn offset_of_line_col(&self, line: usize, col: usize) -> usize {
self.rope_text().offset_of_line_col(line, col)
}
pub fn line_of_offset(&self, offset: usize) -> usize {
self.rope_text().line_of_offset(offset)
}
pub fn first_non_blank_character_on_line(&self, line: usize) -> usize {
self.rope_text().first_non_blank_character_on_line(line)
}
pub fn line_end_col(&self, line: usize, caret: bool) -> usize {
self.rope_text().line_end_col(line, caret)
}
pub fn select_word(&self, offset: usize) -> (usize, usize) {
self.rope_text().select_word(offset)
}
pub fn vline_of_offset(&self, offset: usize, affinity: CursorAffinity) -> VLine {
self.lines
.vline_of_offset(&self.text_prov(), offset, affinity)
}
pub fn vline_of_line(&self, line: usize) -> VLine {
self.lines.vline_of_line(&self.text_prov(), line)
}
pub fn rvline_of_line(&self, line: usize) -> RVLine {
self.lines.rvline_of_line(&self.text_prov(), line)
}
pub fn vline_of_rvline(&self, rvline: RVLine) -> VLine {
self.lines.vline_of_rvline(&self.text_prov(), rvline)
}
pub fn offset_of_vline(&self, vline: VLine) -> usize {
self.lines.offset_of_vline(&self.text_prov(), vline)
}
pub fn vline_col_of_offset(&self, offset: usize, affinity: CursorAffinity) -> (VLine, usize) {
self.lines
.vline_col_of_offset(&self.text_prov(), offset, affinity)
}
pub fn rvline_of_offset(&self, offset: usize, affinity: CursorAffinity) -> RVLine {
self.lines
.rvline_of_offset(&self.text_prov(), offset, affinity)
}
pub fn rvline_col_of_offset(&self, offset: usize, affinity: CursorAffinity) -> (RVLine, usize) {
self.lines
.rvline_col_of_offset(&self.text_prov(), offset, affinity)
}
pub fn offset_of_rvline(&self, rvline: RVLine) -> usize {
self.lines.offset_of_rvline(&self.text_prov(), rvline)
}
pub fn vline_info(&self, vline: VLine) -> VLineInfo {
let vline = vline.min(self.last_vline());
self.iter_vlines(false, vline).next().unwrap()
}
pub fn screen_rvline_info_of_offset(
&self,
offset: usize,
affinity: CursorAffinity,
) -> Option<VLineInfo<()>> {
let rvline = self.rvline_of_offset(offset, affinity);
self.screen_lines.with_untracked(|screen_lines| {
screen_lines
.iter_vline_info()
.find(|vline_info| vline_info.rvline == rvline)
})
}
pub fn rvline_info(&self, rvline: RVLine) -> VLineInfo<()> {
let rvline = rvline.min(self.last_rvline());
self.iter_rvlines(false, rvline).next().unwrap()
}
pub fn rvline_info_of_offset(&self, offset: usize, affinity: CursorAffinity) -> VLineInfo<()> {
let rvline = self.rvline_of_offset(offset, affinity);
self.rvline_info(rvline)
}
pub fn first_col<T: std::fmt::Debug>(&self, info: VLineInfo<T>) -> usize {
info.first_col(&self.text_prov())
}
pub fn last_col<T: std::fmt::Debug>(&self, info: VLineInfo<T>, caret: bool) -> usize {
info.last_col(&self.text_prov(), caret)
}
pub fn max_line_width(&self) -> f64 {
self.lines.max_width()
}
pub fn line_point_of_offset(&self, offset: usize, affinity: CursorAffinity) -> Point {
let (line, col) = self.offset_to_line_col(offset);
self.line_point_of_line_col(line, col, affinity, false)
}
pub fn line_point_of_line_col(
&self,
line: usize,
col: usize,
affinity: CursorAffinity,
force_affinity: bool,
) -> Point {
let text_layout = self.text_layout(line);
let index = if force_affinity {
text_layout
.phantom_text
.col_after_force(col, affinity == CursorAffinity::Forward)
} else {
text_layout
.phantom_text
.col_after(col, affinity == CursorAffinity::Forward)
};
hit_position_aff(
&text_layout.text,
index,
affinity == CursorAffinity::Backward,
)
.point
}
pub fn points_of_offset(&self, offset: usize, affinity: CursorAffinity) -> (Point, Point) {
let line = self.line_of_offset(offset);
let line_height = f64::from(self.style().line_height(self.id(), line));
let info = self.screen_lines.with_untracked(|sl| {
sl.iter_line_info().find(|info| {
info.vline_info.interval.start <= offset && offset <= info.vline_info.interval.end
})
});
let Some(info) = info else {
return (Point::new(0.0, 0.0), Point::new(0.0, 0.0));
};
let y = info.vline_y;
let x = self.line_point_of_offset(offset, affinity).x;
(Point::new(x, y), Point::new(x, y + line_height))
}
pub fn offset_of_point(&self, mode: Mode, point: Point) -> (usize, bool) {
let ((line, col), is_inside) = self.line_col_of_point(mode, point);
(self.offset_of_line_col(line, col), is_inside)
}
pub fn line_col_of_point_with_phantom(&self, point: Point) -> (usize, usize) {
let line_height = f64::from(self.style().line_height(self.id(), 0));
let info = if point.y <= 0.0 {
Some(self.first_rvline_info())
} else {
self.screen_lines
.with_untracked(|sl| {
sl.iter_line_info().find(|info| {
info.vline_y <= point.y && info.vline_y + line_height >= point.y
})
})
.map(|info| info.vline_info)
};
let info = info.unwrap_or_else(|| {
for (y_idx, info) in self.iter_rvlines(false, RVLine::default()).enumerate() {
let vline_y = y_idx as f64 * line_height;
if vline_y <= point.y && vline_y + line_height >= point.y {
return info;
}
}
self.last_rvline_info()
});
let rvline = info.rvline;
let line = rvline.line;
let text_layout = self.text_layout(line);
let y = text_layout.get_layout_y(rvline.line_index).unwrap_or(0.0);
let hit_point = text_layout.text.hit_point(Point::new(point.x, y as f64));
(line, hit_point.index)
}
pub fn line_col_of_point(&self, mode: Mode, point: Point) -> ((usize, usize), bool) {
let line_height = f64::from(self.style().line_height(self.id(), 0));
let info = if point.y <= 0.0 {
Some(self.first_rvline_info())
} else {
self.screen_lines
.with_untracked(|sl| {
sl.iter_line_info().find(|info| {
info.vline_y <= point.y && info.vline_y + line_height >= point.y
})
})
.map(|info| info.vline_info)
};
let info = info.unwrap_or_else(|| {
for (y_idx, info) in self.iter_rvlines(false, RVLine::default()).enumerate() {
let vline_y = y_idx as f64 * line_height;
if vline_y <= point.y && vline_y + line_height >= point.y {
return info;
}
}
self.last_rvline_info()
});
let rvline = info.rvline;
let line = rvline.line;
let text_layout = self.text_layout(line);
let y = text_layout.get_layout_y(rvline.line_index).unwrap_or(0.0);
let hit_point = text_layout.text.hit_point(Point::new(point.x, y as f64));
let col = text_layout.phantom_text.before_col(hit_point.index);
let max_col = self.line_end_col(line, mode != Mode::Normal);
let mut col = col.min(max_col);
if !hit_point.is_inside {
col = info.last_col(&self.text_prov(), true);
}
let tab_width = self.style().tab_width(self.id(), line);
if self.style().atomic_soft_tabs(self.id(), line) && tab_width > 1 {
col = snap_to_soft_tab_line_col(
&self.text(),
line,
col,
SnapDirection::Nearest,
tab_width,
);
}
((line, col), hit_point.is_inside)
}
pub fn line_horiz_col(&self, line: usize, horiz: &ColPosition, caret: bool) -> usize {
match *horiz {
ColPosition::Col(x) => {
let text_layout = self.text_layout(line);
let hit_point = text_layout.text.hit_point(Point::new(x, 0.0));
let n = hit_point.index;
let col = text_layout.phantom_text.before_col(n);
col.min(self.line_end_col(line, caret))
}
ColPosition::End => self.line_end_col(line, caret),
ColPosition::Start => 0,
ColPosition::FirstNonBlank => self.first_non_blank_character_on_line(line),
}
}
pub fn rvline_horiz_col(
&self,
RVLine { line, line_index }: RVLine,
horiz: &ColPosition,
caret: bool,
) -> usize {
match *horiz {
ColPosition::Col(x) => {
let text_layout = self.text_layout(line);
let y_pos = text_layout
.text
.layout_runs()
.nth(line_index)
.map(|run| run.line_y)
.or_else(|| text_layout.text.layout_runs().last().map(|run| run.line_y))
.unwrap_or(0.0);
let hit_point = text_layout.text.hit_point(Point::new(x, y_pos as f64));
let n = hit_point.index;
let col = text_layout.phantom_text.before_col(n);
col.min(self.line_end_col(line, caret))
}
_ => self.line_horiz_col(line, horiz, caret),
}
}
pub fn move_right(&self, offset: usize, mode: Mode, count: usize) -> usize {
self.rope_text().move_right(offset, mode, count)
}
pub fn move_left(&self, offset: usize, mode: Mode, count: usize) -> usize {
self.rope_text().move_left(offset, mode, count)
}
}
impl std::fmt::Debug for Editor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Editor").field(&self.id).finish()
}
}
impl Editor {
pub fn text_layout(&self, line: usize) -> Arc<TextLayoutLine> {
self.text_layout_trigger(line, true)
}
pub fn text_layout_trigger(&self, line: usize, trigger: bool) -> Arc<TextLayoutLine> {
let cache_rev = self.doc().cache_rev().get_untracked();
self.lines
.get_init_text_layout(cache_rev, self.config_id(), self, line, trigger)
}
fn try_get_text_layout(&self, line: usize) -> Option<Arc<TextLayoutLine>> {
let cache_rev = self.doc().cache_rev().get_untracked();
self.lines
.try_get_text_layout(cache_rev, self.config_id(), line)
}
fn new_whitespace_layout(
line_content: &str,
text_layout: &TextLayout,
phantom: &PhantomTextLine,
render_whitespace: RenderWhitespace,
) -> Option<Vec<(char, (f64, f64))>> {
let mut render_leading = false;
let mut render_boundary = false;
let mut render_between = false;
match render_whitespace {
RenderWhitespace::All => {
render_leading = true;
render_boundary = true;
render_between = true;
}
RenderWhitespace::Boundary => {
render_leading = true;
render_boundary = true;
}
RenderWhitespace::Trailing => {} RenderWhitespace::None => return None,
}
let mut whitespace_buffer = Vec::new();
let mut rendered_whitespaces: Vec<(char, (f64, f64))> = Vec::new();
let mut char_found = false;
let mut col = 0;
for c in line_content.chars() {
match c {
'\t' => {
let col_left = phantom.col_after(col, true);
let col_right = phantom.col_after(col + 1, false);
let x0 = text_layout.hit_position(col_left).point.x;
let x1 = text_layout.hit_position(col_right).point.x;
whitespace_buffer.push(('\t', (x0, x1)));
}
' ' => {
let col_left = phantom.col_after(col, true);
let col_right = phantom.col_after(col + 1, false);
let x0 = text_layout.hit_position(col_left).point.x;
let x1 = text_layout.hit_position(col_right).point.x;
whitespace_buffer.push((' ', (x0, x1)));
}
_ => {
if (char_found && render_between)
|| (char_found && render_boundary && whitespace_buffer.len() > 1)
|| (!char_found && render_leading)
{
rendered_whitespaces.extend(whitespace_buffer.iter());
}
char_found = true;
whitespace_buffer.clear();
}
}
col += c.len_utf8();
}
rendered_whitespaces.extend(whitespace_buffer.iter());
Some(rendered_whitespaces)
}
}
impl TextLayoutProvider for Editor {
fn text(&self) -> Rope {
Editor::text(self)
}
fn new_text_layout(
&self,
line: usize,
_font_size: usize,
_wrap: ResolvedWrap,
) -> Arc<TextLayoutLine> {
let edid = self.id();
let text = self.rope_text();
let style = self.style();
let doc = self.doc();
let line_content_original = text.line_content(line);
let font_size = style.font_size(edid, line);
let line_content = if let Some(s) = line_content_original.strip_suffix("\r\n") {
format!("{s} ")
} else if let Some(s) = line_content_original.strip_suffix('\n') {
format!("{s} ",)
} else {
line_content_original.to_string()
};
let phantom_text = doc.phantom_text(edid, &self.es.get_untracked(), line);
let line_content = phantom_text.combine_with_text(&line_content);
let family = style.font_family(edid, line);
let attrs = Attrs::new()
.color(self.es.with(|s| s.ed_text_color()))
.family(&family)
.font_size(font_size as f32)
.line_height(LineHeightValue::Px(style.line_height(edid, line)));
let mut attrs_list = AttrsList::new(attrs);
self.es.with_untracked(|es| {
style.apply_attr_styles(edid, es, line, attrs, &mut attrs_list);
});
for (offset, size, col, phantom) in phantom_text.offset_size_iter() {
let start = col + offset;
let end = start + size;
let mut attrs = attrs;
if let Some(fg) = phantom.fg {
attrs = attrs.color(fg);
} else {
attrs = attrs.color(self.es.with(|es| es.phantom_color()))
}
if let Some(phantom_font_size) = phantom.font_size {
attrs = attrs.font_size(phantom_font_size.min(font_size) as f32);
}
attrs_list.add_span(start..end, attrs);
}
let mut text_layout = TextLayout::new();
text_layout.set_tab_width(style.tab_width(edid, line));
text_layout.set_text(&line_content, attrs_list);
match self.es.with(|s| s.wrap_method()) {
WrapMethod::None => {}
WrapMethod::EditorWidth => {
let width = self.viewport.get_untracked().width();
text_layout.set_wrap(Wrap::WordOrGlyph);
text_layout.set_size(width as f32, f32::MAX);
}
WrapMethod::WrapWidth { width } => {
text_layout.set_wrap(Wrap::WordOrGlyph);
text_layout.set_size(width, f32::MAX);
}
WrapMethod::WrapColumn { .. } => {}
}
let whitespaces = Self::new_whitespace_layout(
&line_content_original,
&text_layout,
&phantom_text,
self.es.with(|s| s.render_whitespace()),
);
let indent_line = style.indent_line(edid, line, &line_content_original);
let indent = if indent_line != line {
let layout = self.try_get_text_layout(indent_line).unwrap_or_else(|| {
self.new_text_layout(
indent_line,
style.font_size(edid, indent_line),
self.lines.wrap(),
)
});
layout.indent + 1.0
} else {
let offset = text.first_non_blank_character_on_line(indent_line);
let (_, col) = text.offset_to_line_col(offset);
text_layout.hit_position(col).point.x
};
let mut layout_line = TextLayoutLine {
text: text_layout,
extra_style: Vec::new(),
whitespaces,
indent,
phantom_text,
};
self.es.with_untracked(|es| {
style.apply_layout_styles(edid, es, line, &mut layout_line);
});
Arc::new(layout_line)
}
fn before_phantom_col(&self, line: usize, col: usize) -> usize {
self.doc()
.before_phantom_col(self.id(), &self.es.get_untracked(), line, col)
}
fn has_multiline_phantom(&self) -> bool {
self.doc()
.has_multiline_phantom(self.id(), &self.es.get_untracked())
}
}
struct EditorFontSizes {
id: EditorId,
style: ReadSignal<Rc<dyn Styling>>,
doc: ReadSignal<Rc<dyn Document>>,
}
impl LineFontSizeProvider for EditorFontSizes {
fn font_size(&self, line: usize) -> usize {
self.style
.with_untracked(|style| style.font_size(self.id, line))
}
fn cache_id(&self) -> FontSizeCacheId {
let mut hasher = DefaultHasher::new();
self.style
.with_untracked(|style| style.id().hash(&mut hasher));
self.doc
.with_untracked(|doc| doc.cache_rev().get_untracked().hash(&mut hasher));
hasher.finish()
}
}
const MIN_WRAPPED_WIDTH: f32 = 100.0;
fn create_view_effects(cx: Scope, ed: &Editor) {
let ed2 = ed.clone();
let ed3 = ed.clone();
let ed4 = ed.clone();
{
let cursor_info = ed.cursor_info.clone();
let cursor = ed.cursor;
cx.create_effect(move |_| {
cursor.track();
cursor_info.reset();
});
}
let update_screen_lines = |ed: &Editor| {
ed.screen_lines.update(|screen_lines| {
let new_screen_lines = ed.compute_screen_lines(screen_lines.base);
*screen_lines = new_screen_lines;
});
};
ed3.lines.layout_event.listen_with(cx, move |val| {
let ed = &ed2;
match val {
LayoutEvent::CreatedLayout { line, .. } => {
let sl = ed.screen_lines.get_untracked();
let should_update = sl.on_created_layout(ed, line);
if should_update {
untrack(|| {
update_screen_lines(ed);
});
ed2.text_layout_trigger(line, true);
}
}
}
});
let viewport_changed_trigger = cx.create_trigger();
cx.create_effect(move |_| {
let ed = &ed3;
let viewport = ed.viewport.get();
let wrap = match ed.es.with(|s| s.wrap_method()) {
WrapMethod::None => ResolvedWrap::None,
WrapMethod::EditorWidth => {
ResolvedWrap::Width((viewport.width() as f32).max(MIN_WRAPPED_WIDTH))
}
WrapMethod::WrapColumn { .. } => todo!(),
WrapMethod::WrapWidth { width } => ResolvedWrap::Width(width),
};
ed.lines.set_wrap(wrap);
let base = ed.screen_lines.with_untracked(|sl| sl.base);
if viewport != base.with_untracked(|base| base.active_viewport) {
batch(|| {
base.update(|base| {
base.active_viewport = viewport;
});
viewport_changed_trigger.notify();
});
}
});
cx.create_effect(move |_| {
viewport_changed_trigger.track();
update_screen_lines(&ed4);
});
}
pub fn normal_compute_screen_lines(
editor: &Editor,
base: RwSignal<ScreenLinesBase>,
) -> ScreenLines {
let lines = &editor.lines;
let style = editor.style.get();
let line_height = style.line_height(editor.id(), 0);
let (y0, y1) = base.with_untracked(|base| (base.active_viewport.y0, base.active_viewport.y1));
let min_vline = VLine((y0 / line_height as f64).floor() as usize);
let max_vline = VLine((y1 / line_height as f64).ceil() as usize);
let cache_rev = editor.doc.get().cache_rev().get();
editor.lines.check_cache_rev(cache_rev);
let min_info = editor.iter_vlines(false, min_vline).next();
let mut rvlines = Vec::new();
let mut info = HashMap::new();
let Some(min_info) = min_info else {
return ScreenLines {
lines: Rc::new(rvlines),
info: Rc::new(info),
diff_sections: None,
base,
};
};
let count = max_vline.get() - min_vline.get();
let iter = lines
.iter_rvlines_init(
editor.text_prov(),
cache_rev,
editor.config_id(),
min_info.rvline,
false,
)
.take(count);
for (i, vline_info) in iter.enumerate() {
rvlines.push(vline_info.rvline);
let line_height = f64::from(style.line_height(editor.id(), vline_info.rvline.line));
let y_idx = min_vline.get() + i;
let vline_y = y_idx as f64 * line_height;
let line_y = vline_y - vline_info.rvline.line_index as f64 * line_height;
info.insert(
vline_info.rvline,
LineInfo {
y: line_y - y0,
vline_y: vline_y - y0,
vline_info,
},
);
}
ScreenLines {
lines: Rc::new(rvlines),
info: Rc::new(info),
diff_sections: None,
base,
}
}
#[derive(Clone)]
pub struct CursorInfo {
pub hidden: RwSignal<bool>,
pub blink_timer: RwSignal<TimerToken>,
pub should_blink: Rc<dyn Fn() -> bool + 'static>,
pub blink_interval: Rc<dyn Fn() -> u64 + 'static>,
}
impl CursorInfo {
pub fn new(cx: Scope) -> CursorInfo {
CursorInfo {
hidden: cx.create_rw_signal(false),
blink_timer: cx.create_rw_signal(TimerToken::INVALID),
should_blink: Rc::new(|| true),
blink_interval: Rc::new(|| 500),
}
}
pub fn blink(&self) {
let info = self.clone();
let blink_interval = (info.blink_interval)();
if blink_interval > 0 && (info.should_blink)() {
let blink_timer = info.blink_timer;
let timer_token =
exec_after(Duration::from_millis(blink_interval), move |timer_token| {
if info.blink_timer.try_get_untracked() == Some(timer_token) {
info.hidden.update(|hide| {
*hide = !*hide;
});
info.blink();
}
});
blink_timer.set(timer_token);
}
}
pub fn reset(&self) {
if self.hidden.get_untracked() {
self.hidden.set(false);
}
self.blink_timer.set(TimerToken::INVALID);
self.blink();
}
}