use std::ops::Range;
use std::sync::Arc;
use gpui::{
App, AvailableSpace, BorderStyle, Bounds, ClipboardItem, Context, Corners, CursorStyle, Edges,
Element, ElementId, ElementInputHandler, Entity, EntityInputHandler, EventEmitter, FocusHandle,
Focusable, Font, FontWeight, GlobalElementId, HighlightStyle, Hitbox, HitboxBehavior, Hsla,
InspectorElementId, InteractiveElement, IntoElement, KeyBinding, LayoutId, MouseButton,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, PathBuilder, Pixels,
Point, Render, RenderImage, ScrollHandle, SharedString, StatefulInteractiveElement, Style,
Styled, TextRun, UTF16Selection, Window, WrappedLine, actions, div, fill, hsla, point, px,
relative, rgb, rgba, size,
};
use unicode_segmentation::UnicodeSegmentation;
mod markdown_syntax;
pub use markdown_syntax::{AlertIcons, MathAlign, PropertyIconFn, SyntaxStyle};
mod tables;
use tables::*;
mod element;
use element::*;
const CONTEXT: &str = "Editor";
actions!(
zorite_editor,
[
Backspace,
Delete,
Left,
Right,
Up,
Down,
Home,
End,
SelectLeft,
SelectRight,
SelectUp,
SelectDown,
SelectAll,
Newline,
Paste,
Copy,
Cut,
ShowCharacterPalette,
Undo,
Redo,
WordLeft,
WordRight,
SelectWordLeft,
SelectWordRight,
Indent,
Outdent,
Bold,
Italic,
Underline,
Strike,
Code,
Dismiss,
]
);
pub fn bind_keys(cx: &mut App) {
let ctx = Some(CONTEXT);
cx.bind_keys([
KeyBinding::new("backspace", Backspace, ctx),
KeyBinding::new("delete", Delete, ctx),
KeyBinding::new("left", Left, ctx),
KeyBinding::new("right", Right, ctx),
KeyBinding::new("up", Up, ctx),
KeyBinding::new("down", Down, ctx),
KeyBinding::new("home", Home, ctx),
KeyBinding::new("end", End, ctx),
KeyBinding::new("shift-left", SelectLeft, ctx),
KeyBinding::new("shift-right", SelectRight, ctx),
KeyBinding::new("shift-up", SelectUp, ctx),
KeyBinding::new("shift-down", SelectDown, ctx),
KeyBinding::new("enter", Newline, ctx),
KeyBinding::new("tab", Indent, ctx),
KeyBinding::new("shift-tab", Outdent, ctx),
KeyBinding::new("cmd-a", SelectAll, ctx),
KeyBinding::new("ctrl-a", SelectAll, ctx),
KeyBinding::new("cmd-c", Copy, ctx),
KeyBinding::new("ctrl-c", Copy, ctx),
KeyBinding::new("cmd-v", Paste, ctx),
KeyBinding::new("ctrl-v", Paste, ctx),
KeyBinding::new("cmd-x", Cut, ctx),
KeyBinding::new("ctrl-x", Cut, ctx),
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, ctx),
KeyBinding::new("cmd-z", Undo, ctx),
KeyBinding::new("ctrl-z", Undo, ctx),
KeyBinding::new("cmd-shift-z", Redo, ctx),
KeyBinding::new("ctrl-shift-z", Redo, ctx),
KeyBinding::new("ctrl-y", Redo, ctx),
KeyBinding::new("alt-left", WordLeft, ctx),
KeyBinding::new("alt-right", WordRight, ctx),
KeyBinding::new("alt-shift-left", SelectWordLeft, ctx),
KeyBinding::new("alt-shift-right", SelectWordRight, ctx),
KeyBinding::new("cmd-b", Bold, ctx),
KeyBinding::new("ctrl-b", Bold, ctx),
KeyBinding::new("cmd-i", Italic, ctx),
KeyBinding::new("ctrl-i", Italic, ctx),
KeyBinding::new("cmd-e", Code, ctx),
KeyBinding::new("ctrl-e", Code, ctx),
KeyBinding::new("cmd-shift-x", Strike, ctx),
KeyBinding::new("ctrl-shift-x", Strike, ctx),
KeyBinding::new("cmd-u", Underline, ctx),
KeyBinding::new("ctrl-u", Underline, ctx),
KeyBinding::new("escape", Dismiss, ctx),
]);
}
const UNDO_LIMIT: usize = 256;
pub const LINE_HEIGHT_RATIO: f32 = 1.45;
const LIST_ROW_GAP: f32 = 4.;
const LIST_TEXT_GAP: f32 = 8.;
const LIST_LEVEL_PER_SPACE: f32 = 4.5;
const CARET_WIDTH: f32 = 1.0;
const CODE_INSET: f32 = 12.;
const CODE_PAD: f32 = 8.;
const QUOTE_INSET: f32 = 14.;
const CHIP_PAD: f32 = 5.;
const IMG_ROW_PAD: f32 = 12.;
const INLINE_MATH_ROW_PAD: f32 = 6.;
const IMG_GRIP: f32 = 14.;
const IMG_MIN_W: f32 = 40.;
#[derive(Clone, Copy)]
struct ImageResize {
line: usize,
start_width: f32,
start_x: Pixels,
width: f32,
}
#[derive(Clone)]
struct Snapshot {
content: String,
caret: usize,
}
#[derive(Clone, Copy, PartialEq)]
enum EditKind {
Insert(usize),
Delete,
Other,
}
#[derive(Clone)]
pub struct Diagnostic {
pub range: Range<usize>,
}
#[derive(Clone)]
struct DiagMenu {
anchor: Point<Pixels>,
range: Range<usize>,
suggestions: Vec<SharedString>,
scroll: ScrollHandle,
turn_into: bool,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum TurnKind {
Text,
H1,
H2,
H3,
Bullet,
Numbered,
Todo,
Quote,
Callout,
Code,
Math,
}
impl TurnKind {
const ALL: [TurnKind; 11] = [
TurnKind::Text,
TurnKind::H1,
TurnKind::H2,
TurnKind::H3,
TurnKind::Bullet,
TurnKind::Numbered,
TurnKind::Todo,
TurnKind::Quote,
TurnKind::Callout,
TurnKind::Code,
TurnKind::Math,
];
fn label(self, labels: &Labels) -> SharedString {
match self {
TurnKind::Text => labels.text.clone(),
TurnKind::H1 => labels.heading_1.clone(),
TurnKind::H2 => labels.heading_2.clone(),
TurnKind::H3 => labels.heading_3.clone(),
TurnKind::Bullet => labels.bulleted_list.clone(),
TurnKind::Numbered => labels.numbered_list.clone(),
TurnKind::Todo => labels.todo.clone(),
TurnKind::Quote => labels.quote.clone(),
TurnKind::Callout => labels.callout.clone(),
TurnKind::Code => labels.code_block.clone(),
TurnKind::Math => labels.math_block.clone(),
}
}
}
fn normalize_loaded(content: String) -> String {
if !content.contains("$$") {
return content;
}
match zorite_markdown::syntax::normalize_math_fences(&content) {
std::borrow::Cow::Owned(n) => n,
std::borrow::Cow::Borrowed(_) => content,
}
}
fn caret_off_marker_line(content: &str, offset: usize) -> usize {
let row = content[..offset.min(content.len())].matches('\n').count();
let line_start = |r: usize| {
let mut off = 0;
for (i, l) in content.split('\n').enumerate() {
if i == r {
return off;
}
off += l.len() + 1;
}
content.len()
};
if let Some(t) = markdown_syntax::table_regions(content)
.iter()
.find(|t| t.marker_line == Some(row))
{
return line_start(t.lines.start);
}
if let Some(m) = markdown_syntax::math_regions(content)
.iter()
.find(|m| m.marker_line == Some(row))
{
return line_start(m.range.end);
}
offset
}
fn strip_block_prefix(line: &str) -> &str {
if let Some((p, ..)) = markdown_syntax::task_prefix(line) {
return &line[p..];
}
if let Some((p, ..)) = markdown_syntax::list_prefix(line) {
return &line[p..];
}
if let Some(n) = markdown_syntax::heading_level(line) {
let after = &line[n as usize..];
return after.strip_prefix(' ').unwrap_or(after);
}
if let Some(p) = markdown_syntax::blockquote_prefix(line) {
return &line[p..];
}
line.trim_start()
}
fn prefix_lines(body: &[String], p: impl Fn(usize) -> String) -> String {
body.iter()
.enumerate()
.map(|(i, l)| format!("{}{l}", p(i)))
.collect::<Vec<_>>()
.join("\n")
}
#[derive(Clone, Copy)]
enum ColEdit {
Insert(usize),
Delete(usize),
}
#[derive(Clone, Copy)]
enum TableMenuAction {
InsertRowAbove,
InsertRowBelow,
DuplicateRow,
InsertColLeft,
InsertColRight,
DeleteRow,
DeleteColumn,
AlignLeft,
AlignCenter,
AlignRight,
SetStyle(Option<&'static str>),
CopyTable,
DeleteTable,
}
impl TableMenuAction {
fn apply(self, editor: &mut EditorState, cx: &mut Context<EditorState>) {
match self {
TableMenuAction::InsertRowAbove => editor.insert_table_row(false, cx),
TableMenuAction::InsertRowBelow => editor.insert_table_row(true, cx),
TableMenuAction::DuplicateRow => editor.duplicate_table_row(cx),
TableMenuAction::InsertColLeft => editor.insert_table_column(false, cx),
TableMenuAction::InsertColRight => editor.insert_table_column(true, cx),
TableMenuAction::DeleteRow => editor.delete_table_row(cx),
TableMenuAction::DeleteColumn => editor.delete_table_column(cx),
TableMenuAction::AlignLeft => editor.set_caret_table_align(CellAlign::Left, cx),
TableMenuAction::AlignCenter => editor.set_caret_table_align(CellAlign::Center, cx),
TableMenuAction::AlignRight => editor.set_caret_table_align(CellAlign::Right, cx),
TableMenuAction::SetStyle(name) => editor.set_table_style(name, cx),
TableMenuAction::CopyTable => editor.copy_table(cx),
TableMenuAction::DeleteTable => editor.delete_table(cx),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EditorEvent {
Changed,
OpenLink(SharedString),
OpenWikiLink(SharedString),
SelectionChanged,
EditMath {
range: Range<usize>,
source: SharedString,
at_end: bool,
inline: bool,
},
MathMenu {
source: SharedString,
position: Point<Pixels>,
},
EditProperties {
range: Range<usize>,
source: SharedString,
at_end: bool,
row: Option<usize>,
},
PreviewImage(SharedString),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum CellAlign {
Left,
Center,
Right,
}
type SuggestFn = Box<dyn Fn(&str) -> Vec<String>>;
type BlockImageFn = Box<dyn Fn(&str) -> Option<Arc<RenderImage>>>;
type BlockChipFn = Box<dyn Fn(&str) -> Option<SharedString>>;
pub type ClipboardWriter = std::rc::Rc<dyn Fn(&str, &mut App)>;
type EmbedViewFn = Box<dyn Fn(&str) -> Option<(gpui::AnyView, Pixels)>>;
type BlockMermaidFn = Box<dyn Fn(&str) -> Option<(Arc<RenderImage>, f32, f32)>>;
type BlockMathFn = Box<dyn Fn(&str) -> Option<(Arc<RenderImage>, f32, f32)>>;
type CodeHighlightFn = Box<dyn Fn(&str, &str) -> Vec<(Range<usize>, HighlightStyle)>>;
type AutoReplaceFn = Box<dyn Fn(&str) -> Option<(Range<usize>, String)>>;
pub fn mermaid_sources(content: &str) -> Vec<SharedString> {
markdown_syntax::mermaid_blocks(content)
.into_iter()
.map(|(_, source)| source.into())
.collect()
}
pub fn math_sources(content: &str) -> Vec<SharedString> {
markdown_syntax::math_blocks(content)
.into_iter()
.map(|(_, source)| source.into())
.collect()
}
pub fn inline_math_sources(content: &str) -> Vec<SharedString> {
let mut out = Vec::new();
let mut in_fence = false;
for line in content.split('\n') {
if line.trim_start().starts_with("```") {
in_fence = !in_fence;
continue;
}
if in_fence {
continue;
}
for span in markdown_syntax::inline_math_spans(line) {
out.push(markdown_syntax::inline_math_latex(line, &span).into());
}
}
out
}
#[derive(Clone)]
pub struct Labels {
pub cut: SharedString,
pub copy: SharedString,
pub copy_as_markdown: SharedString,
pub paste: SharedString,
pub code_copy: SharedString,
pub math_copy: SharedString,
pub turn_into: SharedString,
pub text: SharedString,
pub heading_1: SharedString,
pub heading_2: SharedString,
pub heading_3: SharedString,
pub bulleted_list: SharedString,
pub numbered_list: SharedString,
pub todo: SharedString,
pub quote: SharedString,
pub callout: SharedString,
pub code_block: SharedString,
pub math_block: SharedString,
pub insert_row_above: SharedString,
pub insert_row_below: SharedString,
pub duplicate_row: SharedString,
pub insert_column_left: SharedString,
pub insert_column_right: SharedString,
pub align_left: SharedString,
pub align_center: SharedString,
pub align_right: SharedString,
pub grid_style: SharedString,
pub striped_style: SharedString,
pub header_style: SharedString,
pub minimal_style: SharedString,
pub delete_row: SharedString,
pub delete_column: SharedString,
pub delete_table: SharedString,
pub edit_properties: SharedString,
pub delete_property: SharedString,
pub delete_image: SharedString,
}
impl Default for Labels {
fn default() -> Self {
Self {
cut: "Cut".into(),
copy: "Copy".into(),
copy_as_markdown: "Copy as Markdown".into(),
paste: "Paste".into(),
code_copy: "Copy".into(),
math_copy: "Copy".into(),
turn_into: "Turn into".into(),
text: "Text".into(),
heading_1: "Heading 1".into(),
heading_2: "Heading 2".into(),
heading_3: "Heading 3".into(),
bulleted_list: "Bulleted list".into(),
numbered_list: "Numbered list".into(),
todo: "To-do".into(),
quote: "Quote".into(),
callout: "Callout".into(),
code_block: "Code block".into(),
math_block: "Math block".into(),
insert_row_above: "Insert row above".into(),
insert_row_below: "Insert row below".into(),
duplicate_row: "Duplicate row".into(),
insert_column_left: "Insert column left".into(),
insert_column_right: "Insert column right".into(),
align_left: "Align left".into(),
align_center: "Align center".into(),
align_right: "Align right".into(),
grid_style: "Grid style".into(),
striped_style: "Striped style".into(),
header_style: "Header style".into(),
minimal_style: "Minimal style".into(),
delete_row: "Delete row".into(),
delete_column: "Delete column".into(),
delete_table: "Delete table".into(),
edit_properties: "Edit properties".into(),
delete_property: "Delete property".into(),
delete_image: "Delete image".into(),
}
}
}
pub struct EditorState {
focus_handle: FocusHandle,
content: String,
placeholder: SharedString,
selected_range: Range<usize>,
selection_reversed: bool,
marked_range: Option<Range<usize>>,
clipboard_writer: Option<ClipboardWriter>,
search: Option<(Vec<Range<usize>>, Option<usize>)>,
wrapped: Vec<WrappedLine>,
line_tops: Vec<Pixels>,
wrap_rows: Vec<usize>,
line_heights: Vec<Pixels>,
table_rows: Vec<Option<TableRow>>,
table_row_add_rects: Vec<(Bounds<Pixels>, usize)>,
table_col_add_rects: Vec<(Bounds<Pixels>, usize, usize)>,
table_hover_zones: Vec<(Bounds<Pixels>, usize)>,
table_hover_region: Option<(usize, u8)>,
table_row_del: Option<(Bounds<Pixels>, usize)>,
table_col_del: Option<(Bounds<Pixels>, usize, usize)>,
table_hover_cell: Option<(usize, usize)>,
widget_rows: Vec<bool>,
offset_maps: Vec<Option<std::rc::Rc<Vec<usize>>>>,
line_insets: Vec<Pixels>,
rtl_rows: Vec<Option<RtlRow>>,
last_bounds: Option<Bounds<Pixels>>,
line_height: Pixels,
font_size: Pixels,
paint_font: Option<Font>,
is_selecting: bool,
undo_stack: Vec<Snapshot>,
redo_stack: Vec<Snapshot>,
last_edit: EditKind,
last_edit_keystroke: bool,
tab_indent: usize,
goal_x: Option<Pixels>,
diagnostics: Vec<Diagnostic>,
markdown_style: Option<SyntaxStyle>,
labels: Labels,
menu: Option<DiagMenu>,
table_menu: Option<Point<Pixels>>,
table_menu_scroll: ScrollHandle,
image_menu: Option<(usize, Point<Pixels>)>,
prop_menu: Option<(usize, Point<Pixels>)>,
suggest: Option<SuggestFn>,
block_image: Option<BlockImageFn>,
block_chip: Option<BlockChipFn>,
embed_view: Option<EmbedViewFn>,
block_mermaid: Option<BlockMermaidFn>,
block_math: Option<BlockMathFn>,
code_highlight: Option<CodeHighlightFn>,
auto_replace: Option<AutoReplaceFn>,
last_replaced: Option<String>,
block_math_em: Option<f32>,
chip_rows: Vec<Option<(SharedString, bool)>>,
image_rects: Vec<(usize, Bounds<Pixels>)>,
checkbox_rects: Vec<(usize, Bounds<Pixels>)>,
code_chip_rects: Vec<CodeChipHit>,
code_card_rects: Vec<(usize, Bounds<Pixels>)>,
code_chip_hover: Option<usize>,
code_lang_menu: Option<(usize, Point<Pixels>)>,
code_lang_scroll: ScrollHandle,
code_langs: Vec<SharedString>,
alert_fold_rects: Vec<(usize, Bounds<Pixels>)>,
image_resize: Option<ImageResize>,
table_col_resize: Option<TableColResize>,
table_col_resize_rects: Vec<(Bounds<Pixels>, usize, usize, f32)>,
table_resize_hover: Option<usize>,
shape_memo: std::cell::RefCell<Option<ShapeMemo>>,
scan_cache: std::cell::RefCell<Option<(u64, std::rc::Rc<ScanData>)>>,
scroll_compensator: Option<ScrollCompensatorFn>,
shape_caches: ShapeCaches,
shape_band: std::cell::Cell<Option<(f32, f32)>>,
compensated: std::cell::Cell<bool>,
line_drag: Option<(usize, usize, usize)>,
grip_hover_row: Option<usize>,
table_scroll_x: std::collections::HashMap<usize, f32>,
table_thumbs: Vec<TableThumb>,
table_thumb_drag: Option<(usize, Pixels, f32)>,
grip_inset: Pixels,
last_paint_gen: u64,
content_gen: u64,
utf16_anchor: std::cell::Cell<(u64, usize, usize)>,
editing_block: Option<EditingBlock>,
inline_math_rects: Vec<(Range<usize>, SharedString, Bounds<Pixels>)>,
editing_inline: Option<EditingInline>,
prop_pill_rects: Vec<(Bounds<Pixels>, zorite_markdown::syntax::LinkHit)>,
prop_row_rects: Vec<(Bounds<Pixels>, usize)>,
prop_hover_row: Option<usize>,
folded_headings: std::collections::HashSet<String>,
heading_fold_rects: Vec<(usize, Bounds<Pixels>)>,
heading_row_rects: Vec<(usize, Bounds<Pixels>)>,
heading_hover_row: Option<usize>,
}
struct EditingBlock {
range: Range<usize>,
view: gpui::AnyView,
height: Pixels,
}
struct EditingInline {
range: Range<usize>,
view: gpui::AnyView,
offset: Point<Pixels>,
}
impl EditorState {
pub fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
content: String::new(),
placeholder: SharedString::default(),
selected_range: 0..0,
selection_reversed: false,
marked_range: None,
clipboard_writer: None,
search: None,
wrapped: Vec::new(),
line_tops: Vec::new(),
line_heights: Vec::new(),
wrap_rows: Vec::new(),
widget_rows: Vec::new(),
offset_maps: Vec::new(),
line_insets: Vec::new(),
rtl_rows: Vec::new(),
table_rows: Vec::new(),
table_row_add_rects: Vec::new(),
table_col_add_rects: Vec::new(),
table_hover_zones: Vec::new(),
table_hover_region: None,
table_row_del: None,
table_col_del: None,
table_hover_cell: None,
last_bounds: None,
line_height: px(20.),
font_size: px(16.),
paint_font: None,
is_selecting: false,
undo_stack: Vec::new(),
redo_stack: Vec::new(),
last_edit: EditKind::Other,
last_edit_keystroke: false,
tab_indent: 4,
goal_x: None,
diagnostics: Vec::new(),
markdown_style: None,
labels: Labels::default(),
menu: None,
table_menu: None,
table_menu_scroll: ScrollHandle::new(),
image_menu: None,
prop_menu: None,
suggest: None,
block_image: None,
block_chip: None,
embed_view: None,
block_mermaid: None,
block_math: None,
block_math_em: None,
code_highlight: None,
auto_replace: None,
last_replaced: None,
chip_rows: Vec::new(),
image_rects: Vec::new(),
checkbox_rects: Vec::new(),
code_chip_rects: Vec::new(),
code_card_rects: Vec::new(),
code_chip_hover: None,
code_lang_menu: None,
code_lang_scroll: ScrollHandle::new(),
code_langs: Vec::new(),
alert_fold_rects: Vec::new(),
image_resize: None,
table_col_resize: None,
table_col_resize_rects: Vec::new(),
table_resize_hover: None,
shape_memo: std::cell::RefCell::new(None),
scan_cache: std::cell::RefCell::new(None),
scroll_compensator: None,
last_paint_gen: 0,
shape_caches: ShapeCaches::default(),
shape_band: std::cell::Cell::new(None),
compensated: std::cell::Cell::new(false),
line_drag: None,
grip_hover_row: None,
table_scroll_x: std::collections::HashMap::new(),
table_thumbs: Vec::new(),
table_thumb_drag: None,
grip_inset: px(0.),
content_gen: 0,
utf16_anchor: std::cell::Cell::new((0, 0, 0)),
editing_block: None,
inline_math_rects: Vec::new(),
editing_inline: None,
prop_pill_rects: Vec::new(),
prop_row_rects: Vec::new(),
prop_hover_row: None,
folded_headings: std::collections::HashSet::new(),
heading_fold_rects: Vec::new(),
heading_row_rects: Vec::new(),
heading_hover_row: None,
}
}
pub fn with_text(mut self, text: impl Into<String>) -> Self {
self.content = normalize_loaded(text.into());
let caret = caret_off_marker_line(&self.content, 0);
self.selected_range = caret..caret;
self
}
pub fn with_placeholder(mut self, text: impl Into<SharedString>) -> Self {
self.placeholder = text.into();
self
}
pub fn text(&self) -> &str {
&self.content
}
pub fn replace_range(&mut self, range: Range<usize>, text: &str, cx: &mut Context<Self>) {
let len = self.content.len();
let mut start = range.start.min(len);
while start > 0 && !self.content.is_char_boundary(start) {
start -= 1;
}
let mut end = range.end.clamp(start, len);
while end < len && !self.content.is_char_boundary(end) {
end += 1;
}
let range = start..end;
self.record_edit(&range, text);
self.content.replace_range(range.clone(), text);
self.remap_diagnostics(&range, text.len());
let caret = range.start + text.len();
self.selected_range = caret..caret;
self.selection_reversed = false;
self.marked_range = None;
self.last_edit = EditKind::Other;
cx.notify();
}
pub fn set_text(&mut self, text: impl Into<String>, cx: &mut Context<Self>) {
self.content_gen += 1;
self.content = normalize_loaded(text.into());
let caret = caret_off_marker_line(&self.content, 0);
self.selected_range = caret..caret;
self.selection_reversed = false;
self.marked_range = None;
self.undo_stack.clear();
self.redo_stack.clear();
self.last_edit = EditKind::Other;
cx.notify();
}
pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>, cx: &mut Context<Self>) {
self.diagnostics = diagnostics;
*self.shape_caches.row_keys.borrow_mut() = (None, Vec::new());
cx.notify();
}
pub fn set_code_languages(&mut self, langs: Vec<SharedString>) {
self.code_langs = langs;
}
pub fn set_scroll_compensator(&mut self, f: impl Fn(Pixels, &mut Window, &mut App) + 'static) {
self.scroll_compensator = Some(std::rc::Rc::new(f));
}
pub fn set_markdown_style(&mut self, style: SyntaxStyle, cx: &mut Context<Self>) {
self.markdown_style = Some(style);
cx.notify();
}
pub fn set_labels(&mut self, labels: Labels, cx: &mut Context<Self>) {
self.labels = labels;
cx.notify();
}
pub fn clear_markdown_style(&mut self, cx: &mut Context<Self>) {
if self.markdown_style.take().is_some() {
cx.notify();
}
}
pub fn on_suggest(&mut self, provider: impl Fn(&str) -> Vec<String> + 'static) {
self.suggest = Some(Box::new(provider));
}
pub fn set_block_image_provider(
&mut self,
provider: impl Fn(&str) -> Option<Arc<RenderImage>> + 'static,
) {
self.block_image = Some(Box::new(provider));
}
pub fn set_block_chip_provider(
&mut self,
provider: impl Fn(&str) -> Option<SharedString> + 'static,
) {
self.block_chip = Some(Box::new(provider));
}
pub fn set_embed_provider(
&mut self,
provider: impl Fn(&str) -> Option<(gpui::AnyView, Pixels)> + 'static,
) {
self.embed_view = Some(Box::new(provider));
}
pub fn set_block_mermaid_provider(
&mut self,
provider: impl Fn(&str) -> Option<(Arc<RenderImage>, f32, f32)> + 'static,
) {
self.block_mermaid = Some(Box::new(provider));
}
pub fn set_clipboard_writer(&mut self, writer: ClipboardWriter) {
self.clipboard_writer = Some(writer);
}
pub fn set_search(
&mut self,
matches: Vec<Range<usize>>,
active: Option<usize>,
cx: &mut Context<Self>,
) {
self.search = (!matches.is_empty()).then_some((matches, active));
cx.notify();
}
pub fn offset_screen_top(&self, offset: usize) -> Option<Pixels> {
let bounds = self.last_bounds?;
let (row, _) = self.row_col(offset.min(self.content.len()));
Some(bounds.top() + self.line_tops.get(row).copied()?)
}
pub fn set_block_math_provider(
&mut self,
provider: impl Fn(&str) -> Option<(Arc<RenderImage>, f32, f32)> + 'static,
) {
self.block_math = Some(Box::new(provider));
}
pub fn set_block_math_em(&mut self, em: f32) {
self.block_math_em = (em > 0.).then_some(em);
}
pub fn set_code_highlighter(
&mut self,
f: impl Fn(&str, &str) -> Vec<(Range<usize>, HighlightStyle)> + 'static,
) {
self.code_highlight = Some(Box::new(f));
}
pub fn take_replaced_selection(&mut self) -> Option<String> {
self.last_replaced.take()
}
pub fn set_auto_replace(
&mut self,
f: impl Fn(&str) -> Option<(Range<usize>, String)> + 'static,
) {
self.auto_replace = Some(Box::new(f));
}
fn apply_auto_replace(&mut self, boundary: usize) {
let Some(f) = self.auto_replace.as_ref() else {
return;
};
let line_start = self.content[..boundary].rfind('\n').map_or(0, |p| p + 1);
let line = &self.content[line_start..boundary];
if line.is_empty() {
return;
}
let (row, _) = self.row_col(boundary);
if *self.scan_data().fence_odd.get(row).unwrap_or(&false)
|| line.trim_start().starts_with("```")
{
return;
}
let Some((r, replacement)) = f(line) else {
self.normalize_math_at(row);
return;
};
if r.start >= r.end || r.end > line.len() {
return;
}
let abs = line_start + r.start..line_start + r.end;
let delta = replacement.len() as isize - abs.len() as isize;
self.record_edit(&abs, &replacement);
self.content =
self.content[..abs.start].to_owned() + &replacement + &self.content[abs.end..];
self.remap_diagnostics(&abs, replacement.len());
let caret = (self.selected_range.start as isize + delta) as usize;
self.selected_range = caret..caret;
}
pub fn set_editing_block(
&mut self,
range: Range<usize>,
view: gpui::AnyView,
height: Pixels,
cx: &mut Context<Self>,
) {
self.editing_block = Some(EditingBlock {
range,
view,
height,
});
cx.notify();
}
pub fn editing_block_range(&self) -> Option<Range<usize>> {
self.editing_block.as_ref().map(|eb| eb.range.clone())
}
pub fn end_editing_block(&mut self, cx: &mut Context<Self>) -> Option<Range<usize>> {
let range = self.editing_block.take().map(|eb| eb.range);
cx.notify();
range
}
pub fn set_editing_inline(
&mut self,
range: Range<usize>,
view: gpui::AnyView,
offset: Point<Pixels>,
cx: &mut Context<Self>,
) {
self.editing_inline = Some(EditingInline {
range,
view,
offset,
});
cx.notify();
}
pub fn end_editing_inline(&mut self, cx: &mut Context<Self>) -> Option<Range<usize>> {
let range = self.editing_inline.take().map(|e| e.range);
cx.notify();
range
}
pub fn is_inline_math_range(&self, range: &Range<usize>) -> bool {
range.start < range.end
&& range.end <= self.content.len()
&& self.content.is_char_boundary(range.start)
&& self.content.is_char_boundary(range.end)
&& {
let s = &self.content[range.clone()];
s.len() >= 3
&& s.starts_with('$')
&& s.ends_with('$')
&& !s.starts_with("$$")
&& !s.contains('\n')
}
}
pub fn math_align(&self, block_start: usize) -> MathAlign {
let row = self.row_col(block_start).0;
markdown_syntax::math_regions(&self.content)
.into_iter()
.find(|r| r.range.start == row)
.map_or(MathAlign::default(), |r| r.align)
}
pub fn math_marker_edit(
&self,
block: Range<usize>,
align: MathAlign,
) -> (Range<usize>, String) {
let row = self.row_col(block.start).0;
let prefix = align.marker().map_or(String::new(), |m| format!("{m}\n"));
let has_marker =
row > 0 && markdown_syntax::math_align_marker(self.line_str(row - 1)).is_some();
let start = if has_marker {
self.line_starts()[row - 1]
} else {
block.start
};
(start..block.end, prefix)
}
pub fn find_math_block(&self, source: &str, approx: usize) -> Option<Range<usize>> {
let starts = self.line_starts();
markdown_syntax::math_blocks(&self.content)
.into_iter()
.filter(|(_, s)| s == source)
.map(|(r, _)| starts[r.start]..self.line_end(r.end - 1))
.min_by_key(|r| r.start.abs_diff(approx))
}
pub fn find_inline_math(&self, latex: &str, approx: usize) -> Option<Range<usize>> {
let mut line_start = 0;
let mut best: Option<Range<usize>> = None;
for line in self.content.split('\n') {
for span in markdown_syntax::inline_math_spans(line) {
if markdown_syntax::inline_math_latex(line, &span) == latex {
let abs = line_start + span.start..line_start + span.end;
if best
.as_ref()
.is_none_or(|b| abs.start.abs_diff(approx) < b.start.abs_diff(approx))
{
best = Some(abs);
}
}
}
line_start += line.len() + 1;
}
best
}
pub fn is_math_block_range(&self, range: &Range<usize>) -> bool {
range.end <= self.content.len()
&& range.start <= range.end
&& self.content.is_char_boundary(range.start)
&& self.content[range.start..range.end]
.trim_start()
.starts_with("$$")
}
fn line_str(&self, row: usize) -> &str {
let starts = self.line_starts();
match starts.get(row) {
Some(&s) => &self.content[s..self.line_end(row)],
None => "",
}
}
fn embed_overlays(&self, window: &Window) -> Vec<gpui::Div> {
let Some(provider) = &self.embed_view else {
return Vec::new();
};
if self.markdown_style.is_none() {
return Vec::new();
}
let caret_row = self
.focus_handle
.is_focused(window)
.then(|| self.row_col(self.cursor_offset()).0);
let mut out = Vec::new();
for (row, line) in self.content.split('\n').enumerate() {
if caret_row == Some(row) {
continue;
}
let Some(inner) = zorite_markdown::syntax::embed_line(line) else {
continue;
};
let (Some(top), Some((view, height))) = (self.line_tops.get(row), provider(inner))
else {
continue;
};
out.push(
div()
.absolute()
.top(*top)
.left(px(0.))
.w_full()
.h(height)
.occlude()
.child(view),
);
}
out
}
fn editing_block_overlay(&self) -> Option<gpui::Div> {
let eb = self.editing_block.as_ref()?;
let row = self.row_col(eb.range.start).0;
let top = *self.line_tops.get(row)?;
let height = *self.line_heights.get(row)?;
Some(
div()
.absolute()
.top(top)
.left(px(0.))
.w_full()
.h(height)
.occlude()
.child(eb.view.clone()),
)
}
fn editing_inline_overlay(&self) -> Option<gpui::Div> {
let ei = self.editing_inline.as_ref()?;
let (_, _, rect) = self
.inline_math_rects
.iter()
.find(|(r, _, _)| *r == ei.range)?;
let origin = self.last_bounds.map_or(Point::default(), |b| b.origin);
Some(
div()
.absolute()
.top(rect.origin.y - origin.y + ei.offset.y)
.left(rect.origin.x - origin.x + ei.offset.x)
.occlude()
.child(ei.view.clone()),
)
}
pub fn set_tab_indent(&mut self, spaces: usize) {
self.tab_indent = spaces.max(1);
}
pub fn cursor(&self) -> usize {
self.cursor_offset()
}
pub fn last_edit_was_keystroke(&self) -> bool {
self.last_edit_keystroke
}
pub fn set_cursor(&mut self, offset: usize, cx: &mut Context<Self>) {
let mut offset = offset.min(self.content.len());
while !self.content.is_char_boundary(offset) {
offset -= 1;
}
self.move_to(offset, cx);
}
pub fn row_layout(&self) -> Vec<(Pixels, Pixels)> {
self.line_tops
.iter()
.enumerate()
.map(|(row, &top)| (top, self.line_h(row)))
.collect()
}
fn scan_data(&self) -> std::rc::Rc<ScanData> {
if let Some((generation, data)) = self.scan_cache.borrow().as_ref()
&& *generation == self.content_gen
{
return data.clone();
}
let lines: Vec<&str> = self.content.split('\n').collect();
let mut fence_odd = Vec::with_capacity(lines.len());
let mut odd = false;
for l in &lines {
fence_odd.push(odd);
if l.trim_start().starts_with("```") {
odd = !odd;
}
}
let data = std::rc::Rc::new(ScanData {
generation: self.content_gen,
ordered: markdown_syntax::ordered_numbers(&lines),
tables: markdown_syntax::table_regions(&self.content),
mermaid: markdown_syntax::mermaid_blocks(&self.content),
math: markdown_syntax::math_regions(&self.content),
props: markdown_syntax::property_regions(&self.content),
alert_folds: markdown_syntax::alert_fold_regions(&self.content),
fence_odd,
});
*self.scan_cache.borrow_mut() = Some((self.content_gen, data.clone()));
data
}
fn row_span(&self, row: usize) -> usize {
self.wrap_rows.get(row).copied().unwrap_or(1).max(1)
}
fn line_h(&self, row: usize) -> Pixels {
self.line_heights
.get(row)
.copied()
.unwrap_or(self.line_height)
}
fn line_inset(&self, row: usize) -> Pixels {
self.line_insets.get(row).copied().unwrap_or(px(0.))
}
fn rtl_shift(&self, row: usize) -> Pixels {
self.rtl_rows
.get(row)
.and_then(Option::as_ref)
.and_then(|r| r.shifts.first().copied())
.unwrap_or(px(0.))
}
fn row_origin_x(&self, row: usize) -> Pixels {
self.line_inset(row) + self.rtl_shift(row)
}
fn caret_table_is_rtl(&self) -> bool {
let (row, _) = self.row_col(self.cursor_offset());
self.table_rows
.get(row)
.and_then(Option::as_ref)
.is_some_and(|t| t.rtl)
}
fn caret_row_is_rtl(&self) -> bool {
let (row, _) = self.row_col(self.cursor_offset());
self.rtl_rows
.get(row)
.and_then(Option::as_ref)
.is_some_and(|r| r.base_rtl)
}
fn horizontal_step(&self, visual_right: bool) -> usize {
let off = self.cursor_offset();
let (row, col) = self.row_col(off);
if let Some(r) = self.bidi_map(row) {
let dcol = self.display_col(row, col);
let (k, local) = r.row_of(dcol);
if let Some(rr) = r.rows.get(k)
&& let Some(next) = rr.map.step_visual(local, visual_right)
{
let target = self.line_starts()[row] + self.source_col(row, rr.start + next);
if target != off {
let line_start = self.line_starts()[row];
let here = off.saturating_sub(line_start);
let lands_on_a_formula = markdown_syntax::inline_math_spans(self.line_str(row))
.into_iter()
.any(|s| {
line_start + s.start == target && !(s.start < here && here < s.end)
});
return if lands_on_a_formula {
target + 1
} else {
target
};
}
}
return if visual_right != self.caret_row_is_rtl() {
self.next_visible_boundary(off)
} else {
self.prev_visible_boundary(off)
};
}
if visual_right {
self.next_visible_boundary(off)
} else {
let target = self.prev_visible_boundary(off);
let (trow, _) = self.row_col(target);
let line_start = self.line_starts()[trow];
let here = off.saturating_sub(line_start);
let lands_on_a_formula = markdown_syntax::inline_math_spans(self.line_str(trow))
.into_iter()
.any(|s| line_start + s.start == target && !(s.start < here && here < s.end));
if lands_on_a_formula {
target + 1
} else {
target
}
}
}
fn bidi_map(&self, row: usize) -> Option<&RtlRow> {
self.rtl_rows.get(row).and_then(Option::as_ref)
}
pub fn bounds_for_offset(&self, offset: usize) -> Option<Bounds<Pixels>> {
let bounds = self.last_bounds?;
let (row, col) = self.row_col(offset);
let lh = self.line_h(row);
let line = self.wrapped.get(row)?;
let p = line_pos(line, self.bidi_map(row), self.display_col(row, col), lh)?;
let top = bounds.top() + self.line_tops.get(row).copied().unwrap_or(px(0.)) + p.y;
let x = bounds.left() + p.x + self.row_origin_x(row);
Some(Bounds::from_corners(point(x, top), point(x, top + lh)))
}
pub fn value(&self) -> SharedString {
self.content.clone().into()
}
pub fn focus(&self, window: &mut Window, cx: &mut Context<Self>) {
self.focus_handle.focus(window, cx);
}
fn remap_diagnostics(&mut self, edited: &Range<usize>, new_len: usize) {
let delta = new_len as isize - (edited.end - edited.start) as isize;
self.diagnostics.retain_mut(|d| {
if d.range.end <= edited.start {
true
} else if d.range.start >= edited.end {
d.range.start = (d.range.start as isize + delta) as usize;
d.range.end = (d.range.end as isize + delta) as usize;
true
} else {
false
}
});
}
fn left(&mut self, _: &Left, _: &mut Window, cx: &mut Context<Self>) {
if !self.selected_range.is_empty() {
let to = if self.caret_row_is_rtl() {
self.selected_range.end
} else {
self.selected_range.start
};
self.move_to(to, cx);
return;
}
if self.caret_in_table()
&& let Some(off) =
self.table_move_horizontal(if self.caret_table_is_rtl() { 1 } else { -1 })
{
self.move_to(off, cx);
return;
}
let off = self.horizontal_step(false);
if let Some((range, source)) = self.inline_math_span_at(off) {
cx.emit(EditorEvent::EditMath {
range,
source,
at_end: true,
inline: true,
});
return;
}
if let Some((range, source)) = self.math_block_at(self.row_col(off).0) {
cx.emit(EditorEvent::EditMath {
range,
source,
at_end: true,
inline: false,
});
return;
}
if let Some((range, source)) = self.property_block_at(self.row_col(off).0) {
cx.emit(EditorEvent::EditProperties {
range,
source,
at_end: true,
row: None,
});
return;
}
self.move_to(off, cx);
}
fn right(&mut self, _: &Right, _: &mut Window, cx: &mut Context<Self>) {
if !self.selected_range.is_empty() {
let to = if self.caret_row_is_rtl() {
self.selected_range.start
} else {
self.selected_range.end
};
self.move_to(to, cx);
return;
}
if self.caret_in_table()
&& let Some(off) =
self.table_move_horizontal(if self.caret_table_is_rtl() { -1 } else { 1 })
{
self.move_to(off, cx);
return;
}
let off = self.horizontal_step(true);
if let Some((range, source)) = self.inline_math_span_at(off) {
cx.emit(EditorEvent::EditMath {
range,
source,
at_end: false,
inline: true,
});
return;
}
if let Some((range, source)) = self.math_block_at(self.row_col(off).0) {
cx.emit(EditorEvent::EditMath {
range,
source,
at_end: false,
inline: false,
});
return;
}
if let Some((range, source)) = self.property_block_at(self.row_col(off).0) {
cx.emit(EditorEvent::EditProperties {
range,
source,
at_end: false,
row: None,
});
return;
}
self.move_to(off, cx);
}
fn up(&mut self, _: &Up, _: &mut Window, cx: &mut Context<Self>) {
if self.caret_in_table()
&& let Some(off) = self.table_move_vertical(-1)
{
self.move_to(off, cx);
return;
}
let off = self.move_vertical(-1);
if let Some((range, source)) = self.inline_math_span_at(off) {
cx.emit(EditorEvent::EditMath {
range,
source,
at_end: true,
inline: true,
});
return;
}
if let Some((range, source)) = self.math_block_at(self.row_col(off).0) {
cx.emit(EditorEvent::EditMath {
range,
source,
at_end: true,
inline: false,
});
return;
}
if let Some((range, source)) = self.property_block_at(self.row_col(off).0) {
cx.emit(EditorEvent::EditProperties {
range,
source,
at_end: true,
row: None,
});
return;
}
self.selected_range = off..off;
self.last_edit = EditKind::Other;
cx.emit(EditorEvent::SelectionChanged);
cx.notify();
}
fn down(&mut self, _: &Down, _: &mut Window, cx: &mut Context<Self>) {
if self.caret_in_table()
&& let Some(off) = self.table_move_vertical(1)
{
self.move_to(off, cx);
return;
}
let off = self.move_vertical(1);
if let Some((range, source)) = self.inline_math_span_at(off) {
cx.emit(EditorEvent::EditMath {
range,
source,
at_end: false,
inline: true,
});
return;
}
if let Some((range, source)) = self.math_block_at(self.row_col(off).0) {
cx.emit(EditorEvent::EditMath {
range,
source,
at_end: false,
inline: false,
});
return;
}
if let Some((range, source)) = self.property_block_at(self.row_col(off).0) {
cx.emit(EditorEvent::EditProperties {
range,
source,
at_end: false,
row: None,
});
return;
}
self.selected_range = off..off;
self.last_edit = EditKind::Other;
cx.emit(EditorEvent::SelectionChanged);
cx.notify();
}
fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
self.goal_x = None;
let off = self.horizontal_step(false);
self.select_to(off, cx);
}
fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
self.goal_x = None;
let off = self.horizontal_step(true);
self.select_to(off, cx);
}
fn select_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
let off = self.move_vertical(-1);
self.select_to(off, cx);
}
fn select_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context<Self>) {
let off = self.move_vertical(1);
self.select_to(off, cx);
}
fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
self.move_to(0, cx);
self.select_to(self.content.len(), cx);
}
fn home(&mut self, _: &Home, _: &mut Window, cx: &mut Context<Self>) {
let (row, col) = self.row_col(self.cursor_offset());
let starts = self.line_starts();
let plen = self.hidden_prefix_len(row);
let target = if plen > 0 && col > plen {
starts[row] + plen
} else {
starts[row]
};
self.move_to(target, cx);
}
fn hidden_prefix_len(&self, row: usize) -> usize {
if self.markdown_style.is_none() {
return 0;
}
let Some(&start) = self.line_starts().get(row) else {
return 0;
};
let line = &self.content[start..self.line_end(row)];
markdown_syntax::task_prefix(line)
.map(|(l, ..)| l)
.or_else(|| markdown_syntax::list_prefix(line).map(|(l, ..)| l))
.or_else(|| markdown_syntax::blockquote_prefix(line))
.unwrap_or(0)
}
fn end(&mut self, _: &End, _: &mut Window, cx: &mut Context<Self>) {
let (row, _) = self.row_col(self.cursor_offset());
self.move_to(self.line_end(row), cx);
}
fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
if self.selected_range.is_empty() {
let off = self.cursor_offset();
let (row, col) = self.row_col(off);
if let Some(range) = self.image_row_range(row).or_else(|| {
(col == 0 && row > 0)
.then(|| self.image_row_range(row - 1))
.flatten()
}) {
self.replace_range(range, "", cx);
cx.emit(EditorEvent::Changed);
return;
}
if let Some((range, _)) = self
.inline_math_span_at(self.previous_boundary(off))
.filter(|(r, _)| off == r.end)
{
self.replace_range(range, "", cx);
cx.emit(EditorEvent::Changed);
return;
}
if col == 0
&& row > 0
&& self.math_block_at(row).is_none() && let Some((range, _)) = self.math_block_at(row - 1)
{
let range = self.math_delete_range(range);
self.replace_range(range, "", cx);
cx.emit(EditorEvent::Changed);
return;
}
let join_into_props = col == 0
&& row > 0
&& self.property_block_at(row).is_none()
&& self.property_block_at(row - 1).is_some();
if let Some(range) = self.fmt_delete_range(off, true) {
self.replace_range(range, "", cx);
cx.emit(EditorEvent::Changed);
return;
}
let prev = self.previous_boundary(off);
if off == prev {
return;
}
self.select_to(prev, cx);
self.replace_text_in_range(None, "", window, cx);
if join_into_props {
self.edit_properties_at_caret(true, cx);
}
return;
}
self.replace_text_in_range(None, "", window, cx);
}
fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
if self.selected_range.is_empty() {
let off = self.cursor_offset();
let (row, _) = self.row_col(off);
if let Some(range) = self.image_row_range(row).or_else(|| {
(off == self.line_end(row))
.then(|| self.image_row_range(row + 1))
.flatten()
}) {
self.replace_range(range, "", cx);
cx.emit(EditorEvent::Changed);
return;
}
if let Some((range, _)) = self
.inline_math_span_at(self.next_boundary(off))
.filter(|(r, _)| off == r.start)
{
self.replace_range(range, "", cx);
cx.emit(EditorEvent::Changed);
return;
}
if off == self.line_end(row)
&& self.math_block_at(row).is_none() && let Some((range, _)) = self.math_block_at(row + 1)
{
let range = self.math_delete_range(range);
self.replace_range(range, "", cx);
cx.emit(EditorEvent::Changed);
return;
}
let join_into_props = off == self.line_end(row)
&& self.property_block_at(row).is_none()
&& self.property_block_at(row + 1).is_some();
if let Some(range) = self.fmt_delete_range(off, false) {
self.replace_range(range, "", cx);
cx.emit(EditorEvent::Changed);
return;
}
let next = self.next_boundary(off);
if off == next {
return;
}
self.select_to(next, cx);
self.replace_text_in_range(None, "", window, cx);
if join_into_props {
self.edit_properties_at_caret(false, cx);
}
return;
}
self.replace_text_in_range(None, "", window, cx);
}
fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context<Self>) {
if self.caret_in_table() {
if let Some(off) = self.table_move_vertical(1)
&& self
.table_rows
.get(self.row_col(off).0)
.and_then(Option::as_ref)
.is_some_and(|t| !t.is_separator)
{
self.move_to(off, cx);
return;
}
let (row, _) = self.row_col(self.cursor_offset());
let mut last = row;
while self
.table_rows
.get(last + 1)
.and_then(Option::as_ref)
.is_some()
{
last += 1;
}
let starts = self.line_starts();
let end = starts.get(last + 1).map_or(self.content.len(), |&s| s - 1);
self.selected_range = end..end;
self.replace_text_in_range(None, "\n", window, cx);
return;
}
if self.selected_range.is_empty() {
let (row, _) = self.row_col(self.cursor_offset());
if self.property_block_at(row).is_some() {
self.edit_properties_at_caret(false, cx);
return;
}
}
if self.selected_range.is_empty() {
let cursor = self.cursor_offset();
let line_start = self.content[..cursor].rfind('\n').map_or(0, |i| i + 1);
let line_end = self.content[line_start..]
.find('\n')
.map_or(self.content.len(), |i| line_start + i);
let line = &self.content[line_start..line_end];
if let Some((prefix_len, indent, ordered, num)) = markdown_syntax::list_prefix(line) {
let task = markdown_syntax::task_prefix(line);
let content_start = task.map_or(prefix_len, |(l, ..)| l);
let empty = line.get(content_start..).unwrap_or("").trim().is_empty();
let cont = if empty {
None
} else {
let ws = &line[..indent];
let bullet = line.as_bytes()[indent] as char;
Some(if task.is_some() {
format!("\n{ws}{bullet} [ ] ")
} else if ordered {
format!("\n{ws}{}. ", num + 1)
} else {
format!("\n{ws}{bullet} ")
})
};
match cont {
None => {
self.selected_range = line_start..line_end;
self.replace_text_in_range(None, "", window, cx);
}
Some(text) => self.replace_text_in_range(None, &text, window, cx),
}
return;
}
}
self.replace_text_in_range(None, "\n", window, cx);
}
fn toggle_wrap(&mut self, marker: &str, cx: &mut Context<Self>) {
self.toggle_wrap_pair(marker, marker, cx);
}
fn toggle_wrap_pair(&mut self, open: &str, close: &str, cx: &mut Context<Self>) {
let sel = self.selected_range.clone();
if sel.start >= sel.end {
return;
}
let (ol, cl) = (open.len(), close.len());
let sel_text = &self.content[sel.clone()];
let (range, new, new_sel) =
if sel_text.len() >= ol + cl && sel_text.starts_with(open) && sel_text.ends_with(close)
{
let inner = self.content[sel.start + ol..sel.end - cl].to_string();
(sel.clone(), inner, sel.start..sel.end - ol - cl)
} else if self.content[..sel.start].ends_with(open)
&& self.content[sel.end..].starts_with(close)
{
(
sel.start - ol..sel.end + cl,
sel_text.to_string(),
sel.start - ol..sel.end - ol,
)
} else {
(
sel.clone(),
format!("{open}{sel_text}{close}"),
sel.start + ol..sel.end + ol,
)
};
self.record_edit(&range, &new);
self.content.replace_range(range.clone(), &new);
self.selected_range = new_sel;
self.selection_reversed = false;
self.goal_x = None;
self.remap_diagnostics(&range, new.len());
cx.emit(EditorEvent::Changed);
cx.notify();
}
fn bold(&mut self, _: &Bold, _: &mut Window, cx: &mut Context<Self>) {
self.toggle_wrap("**", cx);
}
fn italic(&mut self, _: &Italic, _: &mut Window, cx: &mut Context<Self>) {
self.toggle_wrap("*", cx);
}
fn code(&mut self, _: &Code, _: &mut Window, cx: &mut Context<Self>) {
self.toggle_wrap("`", cx);
}
fn strike(&mut self, _: &Strike, _: &mut Window, cx: &mut Context<Self>) {
self.toggle_wrap("~~", cx);
}
fn underline(&mut self, _: &Underline, _: &mut Window, cx: &mut Context<Self>) {
self.toggle_wrap_pair("<u>", "</u>", cx);
}
fn indent(&mut self, _: &Indent, window: &mut Window, cx: &mut Context<Self>) {
if self.caret_in_table() {
if let Some(offset) = self.table_cell_nav(true) {
self.move_to(offset, cx);
}
return;
}
let cursor = self.cursor_offset();
let line_start = self.content[..cursor].rfind('\n').map_or(0, |i| i + 1);
let line_end = self.content[line_start..]
.find('\n')
.map_or(self.content.len(), |i| line_start + i);
let line = &self.content[line_start..line_end];
let item = markdown_syntax::list_prefix(line);
let is_item = item.is_some() || markdown_syntax::blockquote_prefix(line).is_some();
let indent = " ".repeat(self.tab_indent);
if !is_item {
self.replace_text_in_range(None, &indent, window, cx);
return;
}
let (range, new_text) = match item {
Some((_, ws, true, _)) => {
let de = ws + line[ws..].bytes().take_while(u8::is_ascii_digit).count();
(
line_start..line_start + de,
format!("{indent}{}1", &line[..ws]),
)
}
_ => (line_start..line_start, indent),
};
self.record_edit(&range, &new_text);
let delta = new_text.len() as isize - (range.end - range.start) as isize;
self.content.replace_range(range.clone(), &new_text);
let caret = (cursor as isize + delta).max(line_start as isize) as usize;
self.selected_range = caret..caret;
self.selection_reversed = false;
self.goal_x = None;
self.remap_diagnostics(&range, new_text.len());
cx.emit(EditorEvent::Changed);
cx.notify();
}
fn outdent(&mut self, _: &Outdent, _: &mut Window, cx: &mut Context<Self>) {
if self.caret_in_table() {
if let Some(offset) = self.table_cell_nav(false) {
self.move_to(offset, cx);
}
return;
}
let cursor = self.cursor_offset();
let line_start = self.content[..cursor].rfind('\n').map_or(0, |i| i + 1);
let line = &self.content[line_start..];
let removed = if line.starts_with('\t') {
1
} else {
line.bytes()
.take(self.tab_indent)
.take_while(|b| *b == b' ')
.count()
};
if removed == 0 {
return;
}
let range = line_start..line_start + removed;
self.record_edit(&range, "");
self.content.replace_range(range.clone(), "");
let caret = cursor.saturating_sub(removed).max(line_start);
self.selected_range = caret..caret;
self.selection_reversed = false;
self.goal_x = None;
self.remap_diagnostics(&range, 0);
cx.emit(EditorEvent::Changed);
cx.notify();
}
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
let item = cx.read_from_clipboard();
let has_files = item.as_ref().is_some_and(|i| {
i.entries()
.iter()
.any(|e| matches!(e, gpui::ClipboardEntry::ExternalPaths(_)))
});
match item.and_then(|i| i.text()).filter(|_| !has_files) {
Some(text) => {
let mut text = if text.contains('\r') {
text.replace("\r\n", "\n").replace('\r', "\n")
} else {
text
};
if self.caret_in_table() && text.contains(['\n', '|']) {
text = text
.trim_end_matches('\n')
.replace('\n', " ")
.replace("\\|", "|")
.replace('|', "\\|");
} else if self.markdown_style.is_some() && text.contains("$$") {
if let std::borrow::Cow::Owned(n) =
zorite_markdown::syntax::normalize_math_fences(&text)
{
text = n;
}
}
self.replace_text_in_range(None, &text, window, cx);
}
None => cx.propagate(),
}
}
fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
if !self.selected_range.is_empty() {
let range = self.copy_range();
let text = if self.markdown_style.is_some() {
markdown_syntax::renumber_copy(&self.content, range)
} else {
self.content[range].to_string()
};
self.write_clipboard(text, cx);
}
}
pub fn copy_plain(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
if !self.selected_range.is_empty() {
let range = self.copy_range();
let text = if self.markdown_style.is_some() {
markdown_syntax::renumber_copy(&self.content, range)
} else {
self.content[range].to_string()
};
cx.write_to_clipboard(ClipboardItem::new_string(text));
}
}
fn write_clipboard(&self, text: String, cx: &mut Context<Self>) {
match &self.clipboard_writer {
Some(writer) => writer(&text, cx),
None => cx.write_to_clipboard(ClipboardItem::new_string(text)),
}
}
fn copy_range(&self) -> std::ops::Range<usize> {
let (start, end) = (
self.selected_range.start.min(self.selected_range.end),
self.selected_range.start.max(self.selected_range.end),
);
if self.markdown_style.is_none() || !self.content[start..end].contains('\n') {
return start..end;
}
let line_start = self.content[..start].rfind('\n').map_or(0, |i| i + 1);
let line_end = self.content[line_start..]
.find('\n')
.map_or(self.content.len(), |i| line_start + i);
let line = &self.content[line_start..line_end];
let prefix_len = markdown_syntax::task_prefix(line)
.map(|(l, ..)| l)
.or_else(|| markdown_syntax::list_prefix(line).map(|(l, ..)| l))
.or_else(|| markdown_syntax::blockquote_prefix(line));
match prefix_len {
Some(plen) if start == line_start + plen => line_start..end,
_ => start..end,
}
}
fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
if !self.selected_range.is_empty() {
self.write_clipboard(self.content[self.selected_range.clone()].to_string(), cx);
self.replace_text_in_range(None, "", window, cx);
}
}
fn show_character_palette(
&mut self,
_: &ShowCharacterPalette,
window: &mut Window,
_: &mut Context<Self>,
) {
window.show_character_palette();
}
fn undo(&mut self, _: &Undo, _: &mut Window, cx: &mut Context<Self>) {
if let Some(prev) = self.undo_stack.pop() {
self.redo_stack.push(self.snapshot());
self.restore(prev);
self.last_edit = EditKind::Other;
cx.notify();
}
}
fn redo(&mut self, _: &Redo, _: &mut Window, cx: &mut Context<Self>) {
if let Some(next) = self.redo_stack.pop() {
self.undo_stack.push(self.snapshot());
self.restore(next);
self.last_edit = EditKind::Other;
cx.notify();
}
}
fn snapshot(&self) -> Snapshot {
Snapshot {
content: self.content.clone(),
caret: self.selected_range.end,
}
}
fn restore(&mut self, s: Snapshot) {
self.content_gen += 1;
self.content = s.content;
let caret = s.caret.min(self.content.len());
self.selected_range = caret..caret;
self.selection_reversed = false;
self.marked_range = None;
}
fn record_edit(&mut self, range: &Range<usize>, new_text: &str) {
self.content_gen += 1;
let kind = if new_text.is_empty() {
EditKind::Delete
} else if range.start == range.end
&& new_text != "\n"
&& new_text.graphemes(true).count() == 1
{
EditKind::Insert(range.start + new_text.len())
} else {
EditKind::Other
};
let coalesce = match (self.last_edit, kind) {
(EditKind::Insert(end), EditKind::Insert(_)) => end == range.start,
(EditKind::Delete, EditKind::Delete) => true,
_ => false,
};
if !coalesce {
self.undo_stack.push(self.snapshot());
if self.undo_stack.len() > UNDO_LIMIT {
self.undo_stack.remove(0);
}
self.redo_stack.clear();
}
self.last_edit = kind;
self.last_edit_keystroke = (new_text != "\n" && new_text.graphemes(true).count() == 1)
|| (new_text.is_empty() && self.content[range.clone()].graphemes(true).count() == 1);
}
fn math_block_at(&self, row: usize) -> Option<(Range<usize>, SharedString)> {
self.markdown_style.as_ref()?;
let starts = self.line_starts();
let blocks = markdown_syntax::math_blocks(&self.content);
blocks
.iter()
.find(|(r, _)| r.contains(&row))
.or_else(|| {
markdown_syntax::math_align_marker(self.line_str(row))?;
blocks.iter().find(|(r, _)| r.start == row + 1)
})
.map(|(r, source)| {
(
starts[r.start]..self.line_end(r.end - 1),
source.clone().into(),
)
})
}
fn math_delete_range(&self, range: Range<usize>) -> Range<usize> {
let mut start = range.start;
let (row, _) = self.row_col(range.start);
if row > 0 {
let prev_start = self.line_starts()[row - 1];
let prev = &self.content[prev_start..self.line_end(row - 1)];
if markdown_syntax::math_align_marker(prev).is_some() {
start = prev_start;
}
}
start..(range.end + 1).min(self.content.len())
}
fn enter_construct_at(&mut self, off: usize, at_end: bool, cx: &mut Context<Self>) -> bool {
if let Some((range, source)) = self.inline_math_span_at(off) {
cx.emit(EditorEvent::EditMath {
range,
source,
at_end,
inline: true,
});
return true;
}
let (row, _) = self.row_col(off);
if let Some((range, source)) = self.math_block_at(row) {
cx.emit(EditorEvent::EditMath {
range,
source,
at_end,
inline: false,
});
return true;
}
if let Some((range, source)) = self.property_block_at(row) {
let block_row = row - self.row_col(range.start).0;
cx.emit(EditorEvent::EditProperties {
range,
source,
at_end,
row: Some(block_row),
});
return true;
}
false
}
fn edit_properties_at_caret(&mut self, at_end: bool, cx: &mut Context<Self>) {
let (row, _) = self.row_col(self.cursor_offset());
if let Some((range, source)) = self.property_block_at(row) {
let block_row = row - self.row_col(range.start).0;
cx.emit(EditorEvent::EditProperties {
range,
source,
at_end,
row: Some(block_row),
});
}
}
fn property_block_at(&self, row: usize) -> Option<(Range<usize>, SharedString)> {
self.markdown_style.as_ref()?;
let region = markdown_syntax::property_regions(&self.content)
.into_iter()
.find(|r| r.contains(&row))?;
let start = *self.line_starts().get(region.start)?;
let end = self.line_end(region.end - 1);
Some((start..end, self.content[start..end].to_string().into()))
}
fn inline_math_span_at(&self, off: usize) -> Option<(Range<usize>, SharedString)> {
self.markdown_style.as_ref()?;
let (row, _) = self.row_col(off);
let line_start = *self.line_starts().get(row)?;
let line = self.line_str(row);
let col = off.saturating_sub(line_start);
markdown_syntax::inline_math_spans(line)
.into_iter()
.find(|s| s.start < col && col < s.end)
.map(|s| {
(
line_start + s.start..line_start + s.end,
SharedString::from(markdown_syntax::inline_math_latex(line, &s).to_string()),
)
})
}
pub fn edit_math_at_caret(&mut self, cx: &mut Context<Self>) {
let (row, _) = self.row_col(self.cursor_offset());
if let Some((range, source)) = self.math_block_at(row) {
cx.emit(EditorEvent::EditMath {
range,
source,
at_end: false,
inline: false,
});
}
}
pub fn property_block_at_caret(&self) -> Option<(Range<usize>, SharedString)> {
let (row, _) = self.row_col(self.cursor_offset());
self.property_block_at(row)
}
fn on_mouse_down(
&mut self,
event: &MouseDownEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some((line, width)) = self.grip_at(event.position) {
self.image_resize = Some(ImageResize {
line,
start_width: width,
start_x: event.position.x,
width,
});
self.is_selecting = false;
self.menu = None;
self.table_menu = None;
self.image_menu = None;
self.prop_menu = None;
cx.notify();
return;
}
if let Some(row) = self.checkbox_at(event.position) {
let range = self.line_starts()[row]..self.line_end(row);
if let Some(new_line) =
markdown_syntax::toggle_task_checkbox(&self.content[range.clone()])
{
self.record_edit(&range, &new_line);
self.content =
self.content[..range.start].to_owned() + &new_line + &self.content[range.end..];
self.remap_diagnostics(&range, new_line.len());
cx.emit(EditorEvent::Changed);
cx.notify();
}
return;
}
if let Some((on_copy, fence_row)) = self.code_chip_at(event.position) {
if on_copy {
if let Some((_, body)) = self.code_block_at(fence_row) {
let text = self.content[body].to_string();
self.write_clipboard(text, cx);
}
} else if !self.code_langs.is_empty() {
self.code_lang_menu = Some((fence_row, event.position));
cx.notify();
}
return;
}
if let Some(row) = self.alert_fold_at(event.position) {
let start = self.line_starts()[row];
let line = &self.content[start..self.line_end(row)];
if let Some((at, folded)) = zorite_markdown::syntax::alert_fold_char(line) {
let range = start + at..start + at + 1;
let repl = if folded { "+" } else { "-" };
self.record_edit(&range, repl);
self.content.replace_range(range.clone(), repl);
self.remap_diagnostics(&range, 1);
cx.emit(EditorEvent::Changed);
cx.notify();
}
return;
}
if let Some(row) = self.heading_fold_at(event.position) {
let start = self.line_starts()[row];
let end = self.line_end(row);
let key = self.content[start..end].trim().to_string();
if !self.folded_headings.remove(&key) {
let single = std::collections::HashSet::from([key.clone()]);
let crow = self.row_col(self.cursor_offset()).0;
if markdown_syntax::heading_fold_regions(&self.content, &single)
.iter()
.any(|r| crow > r.start && crow < r.end)
{
self.move_to(end, cx);
}
self.folded_headings.insert(key);
}
cx.notify();
return;
}
if let Some((src, wiki)) = self.chip_at(event.position) {
cx.emit(if wiki {
EditorEvent::OpenWikiLink(src)
} else {
EditorEvent::OpenLink(src)
});
return;
}
if !event.modifiers.shift
&& !event.modifiers.control
&& let Some((range, source)) = self.inline_math_at(event.position)
{
cx.emit(EditorEvent::EditMath {
range,
source,
at_end: true,
inline: true,
});
return;
}
if event.click_count == 1
&& !event.modifiers.shift
&& !event.modifiers.control
&& let Some((_, hit)) = self
.prop_pill_rects
.iter()
.find(|(b, _)| b.contains(&event.position))
{
match hit {
zorite_markdown::syntax::LinkHit::Page(t) => {
cx.emit(EditorEvent::OpenWikiLink(t.clone().into()))
}
zorite_markdown::syntax::LinkHit::BlockRef(id) => {
cx.emit(EditorEvent::OpenWikiLink(format!("#^{id}").into()))
}
zorite_markdown::syntax::LinkHit::Url(u) => {
cx.emit(EditorEvent::OpenLink(u.clone().into()))
}
}
return;
}
if event.click_count == 1 && !event.modifiers.shift && !event.modifiers.control {
let offset = self.index_for_mouse_position(event.position);
let row = self.row_col(offset).0;
if let Some((range, source)) = self.property_block_at(row) {
let block_row = row - self.row_col(range.start).0;
cx.emit(EditorEvent::EditProperties {
range,
source,
at_end: false,
row: Some(block_row),
});
return;
}
}
if !event.modifiers.shift
&& !event.modifiers.control
&& let Some(src) = self.inline_image_at(event.position)
{
cx.emit(EditorEvent::PreviewImage(src));
return;
}
if event.click_count == 1
&& !event.modifiers.shift
&& !event.modifiers.control
&& self.markdown_style.is_some()
{
let offset = self.index_for_mouse_position(event.position);
let (row, _) = self.row_col(offset);
let start = self.line_starts()[row];
let line = &self.content[start..self.line_end(row)];
match markdown_syntax::link_at(line, offset - start) {
Some(markdown_syntax::LinkHit::Page(title)) => {
cx.emit(EditorEvent::OpenWikiLink(title.into()));
return;
}
Some(markdown_syntax::LinkHit::BlockRef(id)) => {
cx.emit(EditorEvent::OpenWikiLink(format!("#^{id}").into()));
return;
}
Some(markdown_syntax::LinkHit::Url(url)) => {
cx.emit(EditorEvent::OpenLink(url.into()));
return;
}
None => {}
}
if self.row_col(self.selected_range.start).0 != row
&& let Some((at, id)) = zorite_markdown::syntax::block_id(line)
&& offset - start >= at
&& self
.markdown_style
.as_ref()
.and_then(|st| st.block_ref_count.as_ref().map(|f| f(id)))
.unwrap_or(0)
> 0
{
cx.emit(EditorEvent::OpenWikiLink(format!("refs:^{id}").into()));
return;
}
}
if let Some(row) = self.table_add_row_at(event.position) {
let keep = self.caret_table_cell_pos();
if let Some(off) = self.cell_start_offset(row, 0) {
self.selected_range = off..off;
self.insert_table_row(true, cx);
}
if let Some((r, c, ic)) = keep {
let caret = self.caret_pos_for_cell(r, c, ic);
self.selected_range = caret..caret;
cx.notify();
}
return;
}
if let Some((row, col)) = self.table_add_col_at(event.position) {
let keep = self.caret_table_cell_pos();
if let Some(off) = self.cell_start_offset(row, col) {
self.selected_range = off..off;
self.insert_table_column(true, cx);
}
if let Some((r, c, ic)) = keep {
let caret = self.caret_pos_for_cell(r, c, ic);
self.selected_range = caret..caret;
cx.notify();
}
return;
}
if let Some((rect, row)) = self.table_row_del
&& rect.contains(&event.position)
{
if let Some(off) = self.cell_start_offset(row, 0) {
self.selected_range = off..off;
self.delete_table_row(cx);
}
return;
}
if let Some((rect, row, col)) = self.table_col_del
&& rect.contains(&event.position)
{
if let Some(off) = self.cell_start_offset(row, col) {
self.selected_range = off..off;
self.delete_table_column(cx);
}
return;
}
if let Some(&TableThumb { header, .. }) = self
.table_thumbs
.iter()
.find(|t| t.grab.contains(&event.position))
{
let sx = self.table_scroll_x.get(&header).copied().unwrap_or(0.);
self.table_thumb_drag = Some((header, event.position.x, sx));
self.is_selecting = false;
cx.notify();
return;
}
if let Some(&(_, header_row, col, width)) = self
.table_col_resize_rects
.iter()
.find(|(band, ..)| band.contains(&event.position))
{
if event.click_count == 2 {
self.autofit_table_col(header_row, col, window, cx);
return;
}
self.table_col_resize = Some(TableColResize {
header_row,
col,
start_x: event.position.x,
orig: width,
width,
});
self.is_selecting = false;
cx.notify();
return;
}
let offset = self
.table_offset_at(event.position, window)
.unwrap_or_else(|| self.index_for_mouse_position(event.position));
self.menu = None;
self.table_menu = None;
self.image_menu = None;
self.prop_menu = None;
self.code_lang_menu = None;
self.goal_x = None;
self.last_edit = EditKind::Other;
match event.click_count {
2 => {
let (row, _) = self.row_col(offset);
if self.math_block_at(row).is_some() || self.property_block_at(row).is_some() {
return;
}
self.is_selecting = false;
self.selected_range = self.word_range_at(offset).unwrap_or(offset..offset);
self.selection_reversed = false;
cx.notify();
}
n if n >= 3 => {
let (row, _) = self.row_col(offset);
if self.math_block_at(row).is_some() || self.property_block_at(row).is_some() {
return;
}
self.is_selecting = false;
let start = self.line_starts()[row];
self.selected_range = start..self.line_end(row);
self.selection_reversed = false;
cx.notify();
}
_ => {
if !event.modifiers.shift {
let (row, _) = self.row_col(offset);
if let Some((range, source)) = self.math_block_at(row) {
if event.modifiers.control {
self.focus(window, cx);
cx.emit(EditorEvent::MathMenu {
source,
position: event.position,
});
} else {
cx.emit(EditorEvent::EditMath {
range,
source,
at_end: true,
inline: false,
});
}
return;
}
}
self.is_selecting = true;
if event.modifiers.shift {
self.select_to(offset, cx);
} else {
self.move_to(offset, cx);
}
}
}
}
fn on_mouse_up(&mut self, _: &MouseUpEvent, _: &mut Window, cx: &mut Context<Self>) {
if let Some((bs, be, t)) = self.line_drag.take() {
self.apply_line_drag(bs, be, t, cx);
cx.notify();
return;
}
if let Some(resize) = self.image_resize.take() {
self.commit_image_resize(resize, cx);
cx.notify();
return;
}
if self.table_thumb_drag.take().is_some() {
cx.notify();
return;
}
if let Some(resize) = self.table_col_resize.take() {
self.commit_table_col_widths(resize, cx);
cx.notify();
return;
}
self.is_selecting = false;
}
fn on_mouse_move(
&mut self,
event: &MouseMoveEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some((bs, be, t)) = self.line_drag {
let b = self.snap_drop_boundary(self.drop_boundary_at(event.position));
if b != t {
self.line_drag = Some((bs, be, b));
cx.notify();
}
return;
}
if let Some((header, grab_x, start_sx)) = self.table_thumb_drag {
let factor = self
.table_thumbs
.iter()
.find(|t| t.header == header)
.map_or(0., |t| t.factor);
let sx = start_sx + f32::from(event.position.x - grab_x) * factor;
let sx = sx.max(0.);
if self.table_scroll_x.get(&header).copied().unwrap_or(0.) != sx {
self.table_scroll_x.insert(header, sx);
cx.notify();
}
return;
}
if let Some(resize) = self.table_col_resize {
let dx = f32::from(event.position.x - resize.start_x);
let width = (resize.orig + dx).max(24.);
if let Some(r) = self.table_col_resize.as_mut() {
r.width = width;
}
cx.notify();
return;
}
if let Some(resize) = self.image_resize {
let avail = self
.last_bounds
.map_or(f32::MAX, |b| f32::from(b.size.width))
- f32::from(self.line_inset(resize.line));
let max_w = avail.max(IMG_MIN_W);
let dx = f32::from(event.position.x - resize.start_x);
let width = (resize.start_width + dx).clamp(IMG_MIN_W, max_w);
if let Some(r) = self.image_resize.as_mut() {
r.width = width;
}
cx.notify();
return;
}
if self.is_selecting {
let offset = self
.table_offset_at(event.position, window)
.unwrap_or_else(|| self.index_for_mouse_position(event.position));
self.select_to(offset, cx);
return;
}
if self.table_menu.is_some() {
return;
}
let region = self.table_hover_region_at(event.position);
let cell = self.hovered_table_cell(event.position);
if region != self.table_hover_region || cell != self.table_hover_cell {
self.table_hover_region = region;
self.table_hover_cell = cell;
cx.notify();
}
let on_band = self
.table_col_resize_rects
.iter()
.position(|(b, ..)| b.contains(&event.position));
if on_band != self.table_resize_hover {
self.table_resize_hover = on_band;
cx.notify();
}
let prow = self
.prop_row_rects
.iter()
.position(|(b, _)| b.contains(&event.position));
if prow != self.prop_hover_row {
self.prop_hover_row = prow;
cx.notify();
}
let hrow = self
.heading_row_rects
.iter()
.find_map(|(row, b)| b.contains(&event.position).then_some(*row));
if hrow != self.heading_hover_row {
self.heading_hover_row = hrow;
cx.notify();
}
let ccard = self
.code_card_rects
.iter()
.find_map(|(row, b)| b.contains(&event.position).then_some(*row));
if ccard != self.code_chip_hover {
self.code_chip_hover = ccard;
cx.notify();
}
}
fn commit_image_resize(&mut self, resize: ImageResize, cx: &mut Context<Self>) {
let starts = self.line_starts();
let Some(&start) = starts.get(resize.line) else {
return;
};
let end = self.line_end(resize.line);
let line = &self.content[start..end];
let new_line = set_image_width(line, resize.width.round().max(IMG_MIN_W) as u32);
if new_line == line {
return;
}
let range = start..end;
let delta = new_line.len() as isize - (end - start) as isize;
self.record_edit(&range, &new_line);
self.content = self.content[..start].to_owned() + &new_line + &self.content[end..];
self.remap_diagnostics(&range, new_line.len());
let remap = |o: usize| {
if o >= end {
o.saturating_add_signed(delta)
} else {
o.min(start + new_line.len())
}
};
self.selected_range = remap(self.selected_range.start)..remap(self.selected_range.end);
cx.emit(EditorEvent::Changed);
cx.notify();
}
fn image_row_range(&self, row: usize) -> Option<Range<usize>> {
self.markdown_style.as_ref()?;
let start = *self.line_starts().get(row)?;
let end = self.line_end(row);
let (src, ..) = markdown_syntax::image_row(&self.content[start..end])?;
if let Some(chip) = &self.block_chip
&& chip(src).is_some()
{
return None; }
Some(start..(end + 1).min(self.content.len()))
}
fn delete_property_row(&mut self, row: usize, cx: &mut Context<Self>) {
let Some(&start) = self.line_starts().get(row) else {
return;
};
let end = (self.line_end(row) + 1).min(self.content.len());
self.replace_range(start..end, "", cx);
cx.emit(EditorEvent::Changed);
}
fn delete_image_row(&mut self, row: usize, cx: &mut Context<Self>) {
if let Some(range) = self.image_row_range(row) {
self.replace_range(range, "", cx);
cx.emit(EditorEvent::Changed);
}
}
fn on_right_mouse_down(
&mut self,
event: &MouseDownEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(&(line, _)) = self
.image_rects
.iter()
.find(|(_, rect)| rect.contains(&event.position))
.filter(|&&(line, _)| self.image_row_range(line).is_some())
{
self.menu = None;
self.table_menu = None;
self.focus(window, cx);
self.image_menu = Some((line, event.position));
cx.notify();
return;
}
if let Some(&(_, row)) = self
.prop_row_rects
.iter()
.find(|(rect, _)| rect.contains(&event.position))
{
self.menu = None;
self.table_menu = None;
self.image_menu = None;
self.focus(window, cx);
self.prop_menu = Some((row, event.position));
cx.notify();
return;
}
if self.chip_at(event.position).is_some() {
self.menu = None;
self.focus(window, cx);
let offset = self.index_for_mouse_position(event.position);
self.move_to(offset, cx);
return;
}
{
let offset = self.index_for_mouse_position(event.position);
let (row, _) = self.row_col(offset);
if let Some((_range, source)) = self.math_block_at(row) {
self.focus(window, cx);
cx.emit(EditorEvent::MathMenu {
source,
position: event.position,
});
return;
}
}
if let Some(offset) = self.table_offset_at(event.position, window) {
self.menu = None;
self.focus(window, cx);
let sel = self.selected_range.clone();
if !sel.is_empty() && offset >= sel.start && offset <= sel.end {
self.menu = Some(DiagMenu {
anchor: event.position,
range: offset..offset,
suggestions: Vec::new(),
scroll: ScrollHandle::new(),
turn_into: false,
});
} else {
self.move_to(offset, cx);
self.table_menu = Some(event.position);
}
cx.notify();
return;
}
let offset = self.index_for_mouse_position(event.position);
let anchor = event.position;
let sel = self.selected_range.clone();
let in_selection = !sel.is_empty() && offset >= sel.start && offset <= sel.end;
if !in_selection {
self.move_to(offset, cx);
}
self.focus(window, cx);
let (range, suggestions) = match self.diagnostic_at(offset).map(|d| d.range.clone()) {
Some(range) => {
let word = self.content[range.clone()].to_string();
let suggestions = self.suggest.as_ref().map(|f| f(&word)).unwrap_or_default();
(range, suggestions)
}
None => (offset..offset, Vec::new()),
};
self.menu = Some(DiagMenu {
anchor,
range,
suggestions: suggestions.into_iter().map(SharedString::from).collect(),
scroll: ScrollHandle::new(),
turn_into: false,
});
cx.notify();
}
fn diagnostic_at(&self, offset: usize) -> Option<&Diagnostic> {
self.diagnostics
.iter()
.find(|d| d.range.start <= offset && offset < d.range.end)
}
fn dismiss(&mut self, _: &Dismiss, _: &mut Window, cx: &mut Context<Self>) {
if self.menu.take().is_some()
|| self.table_menu.take().is_some()
|| self.image_menu.take().is_some()
|| self.prop_menu.take().is_some()
|| self.code_lang_menu.take().is_some()
{
cx.notify();
}
}
fn normalize_math_at(&mut self, row: usize) {
if self.markdown_style.is_none() {
return;
}
let starts = self.line_starts();
let last_row = starts.len().saturating_sub(1);
let blank = |r: usize| self.content[starts[r]..self.line_end(r)].trim().is_empty();
let mut first = row;
while first > 0 && !blank(first - 1) {
first -= 1;
}
let mut last = row;
while last < last_row && !blank(last + 1) {
last += 1;
}
let span = starts[first]..self.line_end(last);
let para = &self.content[span.clone()];
if !para.contains("$$") {
return;
}
let normalized = match zorite_markdown::syntax::normalize_math_fences(para) {
std::borrow::Cow::Borrowed(_) => return,
std::borrow::Cow::Owned(s) => s,
};
let old_caret = self.selected_range.start;
let delta = normalized.len() as isize - (span.end - span.start) as isize;
self.record_edit(&span, &normalized);
self.content =
self.content[..span.start].to_owned() + &normalized + &self.content[span.end..];
self.remap_diagnostics(&span, normalized.len());
let caret = if old_caret >= span.start {
(old_caret as isize + delta).max(0) as usize
} else {
old_caret
};
let caret = caret.min(self.content.len());
self.selected_range = caret..caret;
self.last_edit = EditKind::Other;
}
fn apply_suggestion(
&mut self,
range: Range<usize>,
text: &str,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.menu = None;
self.selected_range = range;
self.selection_reversed = false;
self.replace_text_in_range(None, text, window, cx);
}
fn fence_block_rows(&self, row: usize) -> (usize, Option<usize>) {
let scan = self.scan_data();
let starts = self.line_starts();
let last_row = starts.len().saturating_sub(1);
let open = (0..=row)
.rev()
.find(|&r| !scan.fence_odd.get(r).copied().unwrap_or(false))
.unwrap_or(0);
let close = ((open + 1)..=last_row).find(|&r| {
self.content[starts[r]..self.line_end(r)]
.trim_start()
.starts_with("```")
});
(open, close)
}
fn quote_run_rows(&self, row: usize) -> (usize, usize) {
let starts = self.line_starts();
let last_row = starts.len().saturating_sub(1);
let is_q = |r: usize| {
markdown_syntax::blockquote_prefix(&self.content[starts[r]..self.line_end(r)]).is_some()
};
let mut first = row;
while first > 0 && is_q(first - 1) {
first -= 1;
}
let mut last = row;
while last < last_row && is_q(last + 1) {
last += 1;
}
(first, last)
}
fn block_kind_at(&self, row: usize) -> TurnKind {
let scan = self.scan_data();
if scan.math.iter().any(|m| m.range.contains(&row)) {
return TurnKind::Math;
}
let starts = self.line_starts();
let Some(&start) = starts.get(row) else {
return TurnKind::Text;
};
let line = &self.content[start..self.line_end(row)];
if scan.fence_odd.get(row).copied().unwrap_or(false) || line.trim_start().starts_with("```")
{
return TurnKind::Code;
}
if markdown_syntax::task_prefix(line).is_some() {
return TurnKind::Todo;
}
match markdown_syntax::heading_level(line) {
Some(1) => return TurnKind::H1,
Some(2) => return TurnKind::H2,
Some(3) => return TurnKind::H3,
_ => {}
}
if let Some((_, _, ordered, _)) = markdown_syntax::list_prefix(line) {
return if ordered {
TurnKind::Numbered
} else {
TurnKind::Bullet
};
}
if markdown_syntax::blockquote_prefix(line).is_some() {
let (first, last) = self.quote_run_rows(row);
for (r, &start) in starts.iter().enumerate().take(last + 1).skip(first) {
let l = &self.content[start..self.line_end(r)];
let p = markdown_syntax::blockquote_prefix(l).unwrap_or(0);
if markdown_syntax::alert_kind(&l[p..]).is_some() {
return TurnKind::Callout;
}
}
return TurnKind::Quote;
}
TurnKind::Text
}
fn turn_into(&mut self, kind: TurnKind, window: &mut Window, cx: &mut Context<Self>) {
let row = self.row_col(self.selected_range.start).0;
let cur = self.block_kind_at(row);
if cur == kind {
return;
}
let scan = self.scan_data();
let starts = self.line_starts();
let last_row = starts.len().saturating_sub(1);
let line_at = |r: usize| self.content[starts[r]..self.line_end(r)].to_string();
let (first, last, mut body): (usize, usize, Vec<String>) = match cur {
TurnKind::Code => {
let (open, close) = self.fence_block_rows(row);
let last = close.unwrap_or(last_row);
let body_end = close.map(|c| c.saturating_sub(1)).unwrap_or(last_row);
let body = ((open + 1)..=body_end).map(line_at).collect();
(open, last, body)
}
TurnKind::Math => {
let Some(reg) = scan.math.iter().find(|m| m.range.contains(&row)) else {
return;
};
let first = reg.range.start;
let last = reg.range.end.saturating_sub(1).max(first);
let body = ((first + 1)..last).map(line_at).collect();
(first, last, body)
}
TurnKind::Quote | TurnKind::Callout => {
let (first, last) = self.quote_run_rows(row);
let body = (first..=last)
.filter_map(|r| {
let line = line_at(r);
let stripped = strip_block_prefix(&line);
if let Some(rest) = stripped.trim_start().strip_prefix("[!") {
let after = rest.split_once(']').map(|(_, a)| a).unwrap_or("");
let after = after.trim_start_matches(['-', '+']).trim();
return (!after.is_empty()).then(|| after.to_string());
}
Some(stripped.to_string())
})
.collect();
(first, last, body)
}
_ => (
row,
row,
vec![strip_block_prefix(&line_at(row)).to_string()],
),
};
if body.is_empty() {
body.push(String::new());
}
let text = match kind {
TurnKind::Text => body.join("\n"),
TurnKind::H1 => prefix_lines(&body, |_| "# ".into()),
TurnKind::H2 => prefix_lines(&body, |_| "## ".into()),
TurnKind::H3 => prefix_lines(&body, |_| "### ".into()),
TurnKind::Bullet => prefix_lines(&body, |_| "- ".into()),
TurnKind::Numbered => prefix_lines(&body, |i| format!("{}. ", i + 1)),
TurnKind::Todo => prefix_lines(&body, |_| "- [ ] ".into()),
TurnKind::Quote => prefix_lines(&body, |_| "> ".into()),
TurnKind::Callout => format!("> [!NOTE]\n{}", prefix_lines(&body, |_| "> ".into())),
TurnKind::Code => format!("```\n{}\n```", body.join("\n")),
TurnKind::Math => format!("$$\n{}\n$$", body.join("\n")),
};
let range = starts[first]..self.line_end(last);
let range_start = range.start;
self.selected_range = range;
self.selection_reversed = false;
self.replace_text_in_range(None, &text, window, cx);
if matches!(kind, TurnKind::Code | TurnKind::Math) {
let caret =
(range_start + text.find('\n').map_or(0, |i| i + 1)).min(self.content.len());
self.selected_range = caret..caret;
}
}
pub fn set_grip_inset(&mut self, inset: Pixels) {
self.grip_inset = inset;
}
fn drag_block_rows(&self, row: usize) -> (usize, usize) {
let scan = self.scan_data();
let starts = self.line_starts();
let last_row = starts.len().saturating_sub(1);
let line_at = |r: usize| &self.content[starts[r]..self.line_end(r)];
if let Some(m) = scan
.math
.iter()
.find(|m| m.range.contains(&row) || m.marker_line == Some(row))
{
let first = m.marker_line.unwrap_or(m.range.start).min(m.range.start);
return (first, m.range.end.saturating_sub(1).max(m.range.start));
}
if let Some((r, _)) = scan.mermaid.iter().find(|(r, _)| r.contains(&row)) {
return (r.start, r.end.saturating_sub(1).max(r.start));
}
if let Some(t) = scan
.tables
.iter()
.find(|t| t.lines.contains(&row) || t.marker_line == Some(row))
{
let first = t.marker_line.unwrap_or(t.lines.start).min(t.lines.start);
return (first, t.lines.end.saturating_sub(1).max(t.lines.start));
}
if let Some(p) = scan.props.iter().find(|r| r.contains(&row)) {
return (p.start, p.end.saturating_sub(1).max(p.start));
}
if scan.fence_odd.get(row).copied().unwrap_or(false)
|| line_at(row).trim_start().starts_with("```")
{
let (open, close) = self.fence_block_rows(row);
return (open, close.unwrap_or(last_row));
}
if markdown_syntax::blockquote_prefix(line_at(row)).is_some() {
return self.quote_run_rows(row);
}
if markdown_syntax::list_prefix(line_at(row)).is_some() {
let indent = |r: usize| {
let l = line_at(r);
l.len() - l.trim_start().len()
};
let base = indent(row);
let mut last = row;
while last < last_row && !line_at(last + 1).trim().is_empty() && indent(last + 1) > base
{
last += 1;
}
return (row, last);
}
(row, row)
}
fn grip_hover_row_at(&self, position: Point<Pixels>) -> Option<usize> {
let bounds = self.last_bounds?;
if self.markdown_style.is_none()
|| self.line_drag.is_some()
|| position.x < grip_left(bounds.origin.x, self.grip_inset) - px(4.)
|| position.x > bounds.origin.x + bounds.size.width
|| position.y < bounds.origin.y
|| position.y > bounds.origin.y + bounds.size.height
{
return None;
}
let y = position.y - bounds.origin.y;
(0..self.line_tops.len()).find(|&i| {
let h = self.line_h(i) * self.row_span(i) as f32;
h > px(0.5) && y >= self.line_tops[i] && y < self.line_tops[i] + h
})
}
fn drop_boundary_at(&self, position: Point<Pixels>) -> usize {
let Some(bounds) = self.last_bounds else {
return 0;
};
let y = position.y - bounds.origin.y;
for i in 0..self.line_tops.len() {
let mid = self.line_tops[i] + self.line_h(i) * self.row_span(i) as f32 / 2.;
if y < mid {
return i;
}
}
self.line_tops.len()
}
fn snap_drop_boundary(&self, mut b: usize) -> usize {
let scan = self.scan_data();
let snap = |b: usize, s: usize, e: usize| {
if b > s && b < e {
if b - s <= e - b { s } else { e }
} else {
b
}
};
for m in scan.math.iter() {
let s = m.marker_line.unwrap_or(m.range.start).min(m.range.start);
b = snap(b, s, m.range.end);
}
for (r, _) in scan.mermaid.iter() {
b = snap(b, r.start, r.end);
}
for t in scan.tables.iter() {
let s = t.marker_line.unwrap_or(t.lines.start).min(t.lines.start);
b = snap(b, s, t.lines.end);
}
for p in scan.props.iter() {
b = snap(b, p.start, p.end);
}
if scan.fence_odd.get(b).copied().unwrap_or(false) {
let (open, close) = self.fence_block_rows(b);
let close = close.map(|c| c + 1).unwrap_or(self.line_starts().len());
b = if b - open <= close - b { open } else { close };
}
b
}
fn apply_line_drag(&mut self, bs: usize, be: usize, t: usize, cx: &mut Context<Self>) {
let starts = self.line_starts();
let n = starts.len();
if bs >= n || be >= n || (t >= bs && t <= be + 1) {
return; }
let block_start = starts[bs];
let block_end = self.line_end(be);
let block_text = self.content[block_start..block_end].to_string();
if t > be {
let target_off = if t >= n {
self.content.len()
} else {
starts[t]
};
let after_block = (block_end + 1).min(self.content.len());
let mut new = self.content[after_block..target_off].to_string();
if !new.is_empty() && !new.ends_with('\n') {
new.push('\n');
}
let rest_len = new.len();
new.push_str(&block_text);
if t < n {
new.push('\n');
}
self.replace_range(block_start..target_off, &new, cx);
let caret = block_start + rest_len;
self.selected_range = caret..caret;
} else {
let target_off = starts[t];
let between = &self.content[target_off..block_start];
let mut new = block_text.clone();
new.push('\n');
new.push_str(&between[..between.len().saturating_sub(1)]);
self.replace_range(target_off..block_end, &new, cx);
self.selected_range = target_off..target_off;
}
cx.emit(EditorEvent::Changed);
}
fn move_to(&mut self, offset: usize, cx: &mut Context<Self>) {
self.selected_range = offset..offset;
self.last_edit = EditKind::Other;
self.goal_x = None;
cx.emit(EditorEvent::SelectionChanged);
cx.notify();
}
pub fn exit_math(
&mut self,
block: Range<usize>,
after: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.focus(window, cx);
let target = if after {
let (end_row, _) = self.row_col(block.end.saturating_sub(1));
match self.line_starts().get(end_row + 1).copied() {
Some(start) => start,
None => {
let end = self.content.len();
self.replace_range(end..end, "\n", cx);
cx.emit(EditorEvent::Changed);
self.content.len()
}
}
} else {
let (start_row, _) = self.row_col(block.start);
let mut row = start_row;
if row > 0 && markdown_syntax::math_align_marker(self.line_str(row - 1)).is_some() {
row -= 1;
}
if row > 0 { self.line_end(row - 1) } else { 0 }
};
self.move_to(target, cx);
}
pub fn caret_screen_bounds(&self) -> Option<Bounds<Pixels>> {
let bounds = self.last_bounds?;
let (row, col) = self.row_col(self.cursor_offset());
let lh = self.line_h(row);
let p = self
.wrapped
.get(row)?
.position_for_index(self.display_col(row, col), lh)?;
let top = bounds.top() + self.line_tops.get(row).copied().unwrap_or(px(0.)) + p.y;
Some(Bounds::from_corners(
point(bounds.left(), top),
point(bounds.left(), top + lh),
))
}
fn cursor_offset(&self) -> usize {
if self.selection_reversed {
self.selected_range.start
} else {
self.selected_range.end
}
}
fn select_to(&mut self, offset: usize, cx: &mut Context<Self>) {
if self.selection_reversed {
self.selected_range.start = offset;
} else {
self.selected_range.end = offset;
}
if self.selected_range.end < self.selected_range.start {
self.selection_reversed = !self.selection_reversed;
self.selected_range = self.selected_range.end..self.selected_range.start;
}
cx.emit(EditorEvent::SelectionChanged);
cx.notify();
}
fn line_starts(&self) -> Vec<usize> {
let mut starts = vec![0];
for (i, b) in self.content.bytes().enumerate() {
if b == b'\n' {
starts.push(i + 1);
}
}
starts
}
fn row_col(&self, offset: usize) -> (usize, usize) {
let starts = self.line_starts();
let row = starts.partition_point(|&s| s <= offset).saturating_sub(1);
(row, offset - starts[row])
}
fn line_end(&self, row: usize) -> usize {
let starts = self.line_starts();
starts
.get(row + 1)
.map(|&s| s - 1)
.unwrap_or(self.content.len())
}
fn vertical_offset(&self, dir: i32) -> usize {
let cursor = self.cursor_offset();
let starts = self.line_starts();
let (row, col) = self.row_col(cursor);
let target = row as i32 + dir;
if target < 0 {
return 0;
}
if target as usize >= starts.len() {
return self.content.len();
}
let target = target as usize;
let target_start = starts[target];
let target_len = self.line_end(target) - target_start;
let mut new_col = col.min(target_len);
while new_col > 0 && !self.content.is_char_boundary(target_start + new_col) {
new_col -= 1;
}
target_start + new_col
}
fn move_vertical(&mut self, dir: i32) -> usize {
if self.wrapped.is_empty() {
return self.vertical_offset(dir);
}
let (row, col) = self.row_col(self.cursor_offset());
let cur_lh = self.line_h(row);
if cur_lh <= px(0.) {
return self.vertical_offset(dir);
}
let Some(cur) = self
.wrapped
.get(row)
.and_then(|l| line_pos(l, self.bidi_map(row), self.display_col(row, col), cur_lh))
else {
return self.vertical_offset(dir);
};
let global_y = self.line_tops[row] + cur.y;
let goal = self.goal_x.unwrap_or(cur.x + self.rtl_shift(row));
self.goal_x = Some(goal);
let target_y = if dir >= 0 {
global_y + cur_lh
} else {
global_y - px(1.)
};
if target_y < px(0.) {
return 0;
}
let last = self.wrapped.len() - 1;
let total = self.line_tops[last] + self.line_h(last) * self.row_span(last) as f32;
if target_y >= total {
let mut r = last;
while r > 0 && self.line_h(r) <= px(0.) {
r -= 1;
}
return if r == last {
self.content.len()
} else {
self.line_end(r)
};
}
let mut trow = last;
for i in 0..self.wrapped.len() {
let h = self.line_h(i) * self.row_span(i) as f32;
if target_y < self.line_tops[i] + h {
trow = i;
break;
}
}
if dir < 0 && trow > 0 && target_y < self.line_tops[trow] {
trow -= 1;
}
if self
.table_rows
.get(trow)
.and_then(Option::as_ref)
.is_some_and(|t| t.is_separator)
{
let skip = if dir >= 0 {
trow + 1
} else {
trow.wrapping_sub(1)
};
if skip < self.wrapped.len() {
trow = skip;
}
}
if self.line_h(trow) <= px(0.) {
let step = |r: usize| {
if dir >= 0 {
(r + 1 < self.wrapped.len()).then_some(r + 1)
} else {
r.checked_sub(1)
}
};
let mut r = trow;
loop {
match step(r) {
Some(n) if self.line_h(n) <= px(0.) => r = n,
Some(n) => {
trow = n;
break;
}
None => return self.cursor_offset(),
}
}
}
let rel = point(
(goal - self.rtl_shift(trow)).max(px(0.)),
(target_y - self.line_tops[trow]).max(px(0.)),
);
let col = line_index_at(
&self.wrapped[trow],
self.bidi_map(trow),
rel,
self.line_h(trow),
);
self.line_starts()[trow] + self.source_col(trow, col)
}
fn next_word(&self, offset: usize) -> usize {
self.content
.unicode_word_indices()
.map(|(i, w)| i + w.len())
.find(|&end| end > offset)
.unwrap_or(self.content.len())
}
fn prev_word(&self, offset: usize) -> usize {
self.content
.unicode_word_indices()
.map(|(i, _)| i)
.rfind(|&start| start < offset)
.unwrap_or(0)
}
fn word_range_at(&self, offset: usize) -> Option<Range<usize>> {
let mut ends_at = None;
for (i, w) in self.content.unicode_word_indices() {
let range = i..i + w.len();
if range.start <= offset && offset < range.end {
return Some(range);
}
if range.end == offset {
ends_at = Some(range);
}
}
ends_at
}
fn word_left(&mut self, _: &WordLeft, _: &mut Window, cx: &mut Context<Self>) {
let off = self.prev_word(self.cursor_offset());
if self.enter_construct_at(off, true, cx) {
return;
}
self.move_to(off, cx);
}
fn word_right(&mut self, _: &WordRight, _: &mut Window, cx: &mut Context<Self>) {
let off = self.next_word(self.cursor_offset());
if self.enter_construct_at(off, false, cx) {
return;
}
self.move_to(off, cx);
}
fn select_word_left(&mut self, _: &SelectWordLeft, _: &mut Window, cx: &mut Context<Self>) {
self.goal_x = None;
self.select_to(self.prev_word(self.cursor_offset()), cx);
}
fn select_word_right(&mut self, _: &SelectWordRight, _: &mut Window, cx: &mut Context<Self>) {
self.goal_x = None;
self.select_to(self.next_word(self.cursor_offset()), cx);
}
fn chip_at(&self, position: Point<Pixels>) -> Option<(SharedString, bool)> {
if self.wrapped.is_empty() || self.chip_rows.iter().all(Option::is_none) {
return None;
}
let bounds = self.last_bounds.as_ref()?;
let rel_y = position.y - bounds.top();
let mut row = self.wrapped.len() - 1;
for i in 0..self.wrapped.len() {
let h = self.line_h(i) * self.row_span(i) as f32;
if rel_y < self.line_tops[i] + h {
row = i;
break;
}
}
self.chip_rows.get(row).and_then(Option::clone)
}
fn inline_math_at(&self, position: Point<Pixels>) -> Option<(Range<usize>, SharedString)> {
self.inline_math_rects
.iter()
.find(|(_, latex, rect)| !latex.is_empty() && rect.contains(&position))
.map(|(range, latex, _)| (range.clone(), latex.clone()))
}
fn inline_image_at(&self, position: Point<Pixels>) -> Option<SharedString> {
let (range, _, _) = self
.inline_math_rects
.iter()
.find(|(_, latex, rect)| latex.is_empty() && rect.contains(&position))?;
let text = self.content.get(range.clone())?;
let open = text.rfind('(')?;
let close = text.rfind(')')?;
(open < close).then(|| text[open + 1..close].to_string().into())
}
fn grip_at(&self, position: Point<Pixels>) -> Option<(usize, f32)> {
self.image_rects.iter().find_map(|&(line, rect)| {
Self::image_grip(rect)
.contains(&position)
.then_some((line, f32::from(rect.size.width)))
})
}
fn image_grip(rect: Bounds<Pixels>) -> Bounds<Pixels> {
let s = px(IMG_GRIP);
Bounds::new(
point(rect.right() - s / 2., rect.bottom() - s / 2.),
size(s, s),
)
}
fn code_block_at(&self, fence_row: usize) -> Option<(String, Range<usize>)> {
let starts = self.line_starts();
let &start = starts.get(fence_row)?;
let fence_line = &self.content[start..self.line_end(fence_row)];
let trimmed = fence_line.trim_start();
let lang = trimmed.strip_prefix("```")?.trim().to_string();
let body_start = (self.line_end(fence_row) + 1).min(self.content.len());
let mut body_end = self.content.len();
for (row, &row_start) in starts.iter().enumerate().skip(fence_row + 1) {
if self.content[row_start..self.line_end(row)]
.trim_start()
.starts_with("```")
{
body_end = row_start.saturating_sub(1).max(body_start);
break;
}
}
Some((lang, body_start..body_end))
}
fn code_chip_at(&self, position: Point<Pixels>) -> Option<(bool, usize)> {
self.code_chip_rects.iter().find_map(|c| {
if c.copy.contains(&position) {
Some((true, c.fence_row))
} else if c.lang.contains(&position) {
Some((false, c.fence_row))
} else {
None
}
})
}
fn set_code_lang(&mut self, fence_row: usize, lang: &str, cx: &mut Context<Self>) {
let starts = self.line_starts();
let Some(&start) = starts.get(fence_row) else {
return;
};
let end = self.line_end(fence_row);
let line = &self.content[start..end];
let trimmed = line.trim_start();
if !trimmed.starts_with("```") {
return;
}
let indent = &line[..line.len() - trimmed.len()];
let new_line = format!("{indent}```{}", if lang == "text" { "" } else { lang });
self.replace_range(start..end, &new_line, cx);
let caret = (start + new_line.len() + 1).min(self.content.len());
self.selected_range = caret..caret;
cx.emit(EditorEvent::Changed);
}
fn checkbox_at(&self, position: Point<Pixels>) -> Option<usize> {
let pad = px(4.);
self.checkbox_rects.iter().find_map(|&(line, rect)| {
Bounds::new(
point(rect.origin.x - pad, rect.origin.y - pad),
size(rect.size.width + pad * 2., rect.size.height + pad * 2.),
)
.contains(&position)
.then_some(line)
})
}
fn alert_fold_at(&self, position: Point<Pixels>) -> Option<usize> {
let pad = px(4.);
self.alert_fold_rects.iter().find_map(|&(line, rect)| {
Bounds::new(
point(rect.origin.x - pad, rect.origin.y - pad),
size(rect.size.width + pad * 2., rect.size.height + pad * 2.),
)
.contains(&position)
.then_some(line)
})
}
fn heading_fold_at(&self, position: Point<Pixels>) -> Option<usize> {
let pad = px(4.);
self.heading_fold_rects.iter().find_map(|&(line, rect)| {
Bounds::new(
point(rect.origin.x - pad, rect.origin.y - pad),
size(rect.size.width + pad * 2., rect.size.height + pad * 2.),
)
.contains(&position)
.then_some(line)
})
}
fn index_for_mouse_position(&self, position: Point<Pixels>) -> usize {
if self.content.is_empty() || self.wrapped.is_empty() {
return 0;
}
let Some(bounds) = self.last_bounds.as_ref() else {
return 0;
};
let rel = point(position.x - bounds.left(), position.y - bounds.top());
let mut row = self.wrapped.len() - 1;
for i in 0..self.wrapped.len() {
let height = self.line_h(i) * self.row_span(i) as f32;
if rel.y < self.line_tops[i] + height {
row = i;
break;
}
}
if self.widget_rows.get(row).copied().unwrap_or(false) {
return self.line_starts()[row];
}
let x = (rel.x - self.row_origin_x(row)).max(px(0.));
let line_rel = point(x, rel.y - self.line_tops[row]);
let col = line_index_at(
&self.wrapped[row],
self.bidi_map(row),
line_rel,
self.line_h(row),
);
self.line_starts()[row] + self.source_col(row, col)
}
fn source_col(&self, row: usize, display_col: usize) -> usize {
match self.offset_maps.get(row).and_then(Option::as_ref) {
Some(map) => map.get(display_col).copied().unwrap_or(display_col),
None => display_col,
}
}
fn display_col(&self, row: usize, source_col: usize) -> usize {
display_col_in(
self.offset_maps.get(row).and_then(Option::as_ref),
source_col,
)
}
fn offset_from_utf16(&self, offset: usize) -> usize {
let (mut utf8, mut utf16) = self.utf16_resume(offset, false);
for ch in self.content[utf8..].chars() {
if utf16 >= offset {
break;
}
utf16 += ch.len_utf16();
utf8 += ch.len_utf8();
}
self.utf16_anchor.set((self.content_gen, utf8, utf16));
utf8
}
fn offset_to_utf16(&self, offset: usize) -> usize {
let (mut utf8, mut utf16) = self.utf16_resume(offset, true);
for ch in self.content[utf8..].chars() {
if utf8 >= offset {
break;
}
utf8 += ch.len_utf8();
utf16 += ch.len_utf16();
}
self.utf16_anchor.set((self.content_gen, utf8, utf16));
utf16
}
fn utf16_resume(&self, target: usize, by_utf8: bool) -> (usize, usize) {
let (generation, utf8, utf16) = self.utf16_anchor.get();
let anchor_pos = if by_utf8 { utf8 } else { utf16 };
if generation == self.content_gen && anchor_pos <= target && utf8 <= self.content.len() {
(utf8, utf16)
} else {
(0, 0)
}
}
fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
}
fn range_from_utf16(&self, range: &Range<usize>) -> Range<usize> {
self.offset_from_utf16(range.start)..self.offset_from_utf16(range.end)
}
fn prev_visible_boundary(&self, offset: usize) -> usize {
let (row0, col0) = self.row_col(offset);
let d0 = self.display_col(row0, col0);
let mut off = self.previous_boundary(offset);
loop {
if off == 0 {
return off;
}
let (row, col) = self.row_col(off);
if row != row0
|| self.offset_maps.get(row).and_then(Option::as_ref).is_none()
|| self.display_col(row, col) != d0
{
return off;
}
off = self.previous_boundary(off);
}
}
fn next_visible_boundary(&self, offset: usize) -> usize {
let (row0, col0) = self.row_col(offset);
let d0 = self.display_col(row0, col0);
let mut off = self.next_boundary(offset);
loop {
if off >= self.content.len() {
return self.content.len();
}
let (row, col) = self.row_col(off);
if row != row0
|| self.offset_maps.get(row).and_then(Option::as_ref).is_none()
|| self.display_col(row, col) != d0
{
return off;
}
off = self.next_boundary(off);
}
}
fn fmt_delete_range(&self, off: usize, back: bool) -> Option<Range<usize>> {
let st = self.markdown_style.as_ref()?;
let (row, col) = self.row_col(off);
let line_start = self.line_starts()[row];
let line_end = self.line_end(row);
let line = &self.content[line_start..line_end];
if *self.scan_data().fence_odd.get(row).unwrap_or(&false)
|| line.trim_start().starts_with("```")
{
return None;
}
let pairs = markdown_syntax::fmt_marker_pairs(line, st);
if pairs.is_empty() {
return None;
}
let markers: Vec<&Range<usize>> = pairs.iter().flat_map(|(o, c)| [o, c]).collect();
let mut edge = col;
if back {
while let Some(sp) = markers.iter().find(|sp| sp.end == edge) {
edge = sp.start;
}
} else {
while let Some(sp) = markers.iter().find(|sp| sp.start == edge) {
edge = sp.end;
}
}
let (del_start, del_end) = if back {
if edge == 0 {
return None;
}
let p = self.previous_boundary(line_start + edge) - line_start;
(p, edge)
} else {
if edge >= line.len() {
return None;
}
let n = self.next_boundary(line_start + edge).min(line_end) - line_start;
(edge, n)
};
let emptied = pairs
.iter()
.find(|(o, c)| o.end == del_start && c.start == del_end);
let range = match emptied {
Some((o, c)) => o.start..c.end,
None if edge == col => return None, None => del_start..del_end,
};
Some(line_start + range.start..line_start + range.end)
}
fn previous_boundary(&self, offset: usize) -> usize {
self.content
.grapheme_indices(true)
.rev()
.find_map(|(idx, _)| (idx < offset).then_some(idx))
.unwrap_or(0)
}
fn next_boundary(&self, offset: usize) -> usize {
self.content
.grapheme_indices(true)
.find_map(|(idx, _)| (idx > offset).then_some(idx))
.unwrap_or(self.content.len())
}
}
impl EntityInputHandler for EditorState {
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
actual_range: &mut Option<Range<usize>>,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<String> {
let range = self.range_from_utf16(&range_utf16);
actual_range.replace(self.range_to_utf16(&range));
Some(self.content[range].to_string())
}
fn selected_text_range(
&mut self,
_: bool,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<UTF16Selection> {
Some(UTF16Selection {
range: self.range_to_utf16(&self.selected_range),
reversed: self.selection_reversed,
})
}
fn marked_text_range(&self, _: &mut Window, _: &mut Context<Self>) -> Option<Range<usize>> {
self.marked_range.as_ref().map(|r| self.range_to_utf16(r))
}
fn unmark_text(&mut self, _: &mut Window, _: &mut Context<Self>) {
self.marked_range = None;
}
fn replace_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
_: &mut Window,
cx: &mut Context<Self>,
) {
let range = range_utf16
.as_ref()
.map(|r| self.range_from_utf16(r))
.or(self.marked_range.clone())
.unwrap_or(self.selected_range.clone());
self.last_replaced =
(range.start < range.end).then(|| self.content[range.clone()].to_string());
self.record_edit(&range, new_text);
self.content =
self.content[0..range.start].to_owned() + new_text + &self.content[range.end..];
let caret = range.start + new_text.len();
self.selected_range = caret..caret;
self.selection_reversed = false;
self.marked_range = None;
self.goal_x = None;
self.remap_diagnostics(&range, new_text.len());
if word_boundary_input(new_text) {
self.apply_auto_replace(range.start);
}
cx.emit(EditorEvent::Changed);
cx.notify();
}
fn replace_and_mark_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
new_selected_range_utf16: Option<Range<usize>>,
_: &mut Window,
cx: &mut Context<Self>,
) {
let range = range_utf16
.as_ref()
.map(|r| self.range_from_utf16(r))
.or(self.marked_range.clone())
.unwrap_or(self.selected_range.clone());
self.content_gen += 1;
self.content =
self.content[0..range.start].to_owned() + new_text + &self.content[range.end..];
self.marked_range =
(!new_text.is_empty()).then(|| range.start..range.start + new_text.len());
self.selected_range = new_selected_range_utf16
.as_ref()
.map(|r| self.range_from_utf16(r))
.map(|r| r.start + range.start..r.end + range.start)
.unwrap_or_else(|| {
let caret = range.start + new_text.len();
caret..caret
});
self.remap_diagnostics(&range, new_text.len());
cx.emit(EditorEvent::Changed);
cx.notify();
}
fn bounds_for_range(
&mut self,
range_utf16: Range<usize>,
bounds: Bounds<Pixels>,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<Bounds<Pixels>> {
let range = self.range_from_utf16(&range_utf16);
let (row, col) = self.row_col(range.start);
let lh = self.line_h(row);
let line = self.wrapped.get(row)?;
let map = self.bidi_map(row);
let p = line_pos(line, map, self.display_col(row, col), lh)?;
let top = bounds.top() + self.line_tops.get(row).copied().unwrap_or(px(0.)) + p.y;
let x = bounds.left() + p.x + self.row_origin_x(row);
let (erow, ecol) = self.row_col(range.end);
let x2 = if erow == row && range.end > range.start {
line_pos(line, map, self.display_col(row, ecol), lh)
.filter(|e| e.y == p.y)
.map(|e| bounds.left() + e.x + self.row_origin_x(row))
.unwrap_or(x)
} else {
x
};
Some(Bounds::from_corners(
point(x, top),
point(x2.max(x), top + lh),
))
}
fn character_index_for_point(
&mut self,
point: Point<Pixels>,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<usize> {
Some(self.offset_to_utf16(self.index_for_mouse_position(point)))
}
}
impl Focusable for EditorState {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl EventEmitter<EditorEvent> for EditorState {}
impl Render for EditorState {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.relative()
.key_context(
if self.editing_block.is_some() || self.editing_inline.is_some() {
""
} else {
CONTEXT
},
)
.track_focus(&self.focus_handle)
.cursor(CursorStyle::IBeam)
.on_action(cx.listener(Self::backspace))
.on_action(cx.listener(Self::delete))
.on_action(cx.listener(Self::left))
.on_action(cx.listener(Self::right))
.on_action(cx.listener(Self::up))
.on_action(cx.listener(Self::down))
.on_action(cx.listener(Self::home))
.on_action(cx.listener(Self::end))
.on_action(cx.listener(Self::select_left))
.on_action(cx.listener(Self::select_right))
.on_action(cx.listener(Self::select_up))
.on_action(cx.listener(Self::select_down))
.on_action(cx.listener(Self::word_left))
.on_action(cx.listener(Self::word_right))
.on_action(cx.listener(Self::select_word_left))
.on_action(cx.listener(Self::select_word_right))
.on_action(cx.listener(Self::select_all))
.on_action(cx.listener(Self::newline))
.on_action(cx.listener(Self::bold))
.on_action(cx.listener(Self::italic))
.on_action(cx.listener(Self::underline))
.on_action(cx.listener(Self::strike))
.on_action(cx.listener(Self::code))
.on_action(cx.listener(Self::indent))
.on_action(cx.listener(Self::outdent))
.on_action(cx.listener(Self::paste))
.on_action(cx.listener(Self::copy))
.on_action(cx.listener(Self::cut))
.on_action(cx.listener(Self::show_character_palette))
.on_action(cx.listener(Self::undo))
.on_action(cx.listener(Self::redo))
.on_action(cx.listener(Self::dismiss))
.on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
.on_mouse_down(MouseButton::Right, cx.listener(Self::on_right_mouse_down))
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
.on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
.on_mouse_move(cx.listener(Self::on_mouse_move))
.on_scroll_wheel(cx.listener(Self::on_scroll_wheel))
.child(EditorElement {
editor: cx.entity(),
})
.children(self.embed_overlays(window))
.children(self.editing_block_overlay())
.children(self.editing_inline_overlay())
.children(self.menu.clone().map(|menu| {
let DiagMenu {
anchor,
range,
suggestions,
scroll,
turn_into: menu_turn_into,
} = menu;
let count = suggestions.len();
let st = self.markdown_style.as_ref();
let menu_bg = st.map_or(rgb(0x26262b).into(), |s| s.popover_bg);
let menu_border = st.map_or(rgb(0x45454c).into(), |s| s.popover_border);
let menu_fg = st.map_or(rgb(0xe6e6e6).into(), |s| s.popover_fg);
let hover = st.map_or(rgba(0x2f6fd628).into(), |s| s.popover_hover);
let mut thumb_c = st.map_or(rgba(0xffffff66).into(), |s| s.marker);
thumb_c.a = 0.5;
let rows: Vec<_> = suggestions
.into_iter()
.enumerate()
.map(|(i, sugg)| {
let range = range.clone();
let replacement = sugg.to_string();
div()
.id(("suggestion-row", i))
.flex_shrink_0()
.px(px(10.))
.py(px(3.))
.hover(move |s| s.bg(hover))
.child(sugg)
.on_mouse_down(
MouseButton::Left,
cx.listener(move |editor, _: &MouseDownEvent, window, cx| {
cx.stop_propagation();
editor.apply_suggestion(
range.clone(),
&replacement,
window,
cx,
);
}),
)
})
.collect();
const ROW_H: f32 = 24.0;
const PAD: f32 = 4.0;
const MAX_H: f32 = 180.0;
let rows_h = count as f32 * ROW_H;
let view_h = MAX_H - 2.0 * PAD;
let thumb = (rows_h > view_h).then(|| {
let scrolled = (-f32::from(scroll.offset().y)).clamp(0.0, rows_h - view_h);
let thumb_h = (view_h * view_h / rows_h).max(24.0);
let thumb_top = PAD + scrolled / (rows_h - view_h) * (view_h - thumb_h);
div()
.absolute()
.top(px(thumb_top))
.right(px(2.))
.w(px(6.))
.h(px(thumb_h))
.rounded(px(3.))
.bg(thumb_c)
});
let has_sel = !self.selected_range.is_empty();
let clip_item = |id: &'static str, label: SharedString| {
div()
.id(id)
.flex_shrink_0()
.px(px(10.))
.py(px(3.))
.hover(move |s| s.bg(hover))
.child(label)
};
let mut clipboard = div().flex().flex_col().py(px(4.));
if has_sel {
clipboard = clipboard
.child(
clip_item("menu-cut", self.labels.cut.clone()).on_mouse_down(
MouseButton::Left,
cx.listener(|editor, _: &MouseDownEvent, window, cx| {
cx.stop_propagation();
editor.menu = None;
editor.cut(&Cut, window, cx);
}),
),
)
.child(
clip_item("menu-copy", self.labels.copy.clone()).on_mouse_down(
MouseButton::Left,
cx.listener(|editor, _: &MouseDownEvent, window, cx| {
cx.stop_propagation();
editor.menu = None;
editor.copy(&Copy, window, cx);
}),
),
)
.child(
clip_item("menu-copy-md", self.labels.copy_as_markdown.clone())
.on_mouse_down(
MouseButton::Left,
cx.listener(|editor, _: &MouseDownEvent, window, cx| {
cx.stop_propagation();
editor.menu = None;
editor.copy_plain(window, cx);
}),
),
);
}
let clipboard = clipboard.child(
clip_item("menu-paste", self.labels.paste.clone()).on_mouse_down(
MouseButton::Left,
cx.listener(|editor, _: &MouseDownEvent, window, cx| {
cx.stop_propagation();
editor.menu = None;
editor.paste(&Paste, window, cx);
}),
),
);
let fmt_btn = |id: &'static str| {
div()
.id(id)
.w(px(28.))
.h(px(24.))
.rounded(px(4.))
.flex()
.items_center()
.justify_center()
.hover(move |s| s.bg(hover))
};
let format_bar = has_sel.then(|| {
div()
.flex()
.flex_row()
.items_center()
.gap(px(2.))
.px(px(6.))
.py(px(4.))
.child(
fmt_btn("menu-fmt-bold")
.font_weight(FontWeight::BOLD)
.child("B")
.on_mouse_down(
MouseButton::Left,
cx.listener(|editor, _: &MouseDownEvent, window, cx| {
cx.stop_propagation();
editor.menu = None;
editor.bold(&Bold, window, cx);
}),
),
)
.child(
fmt_btn("menu-fmt-italic")
.italic()
.child("I")
.on_mouse_down(
MouseButton::Left,
cx.listener(|editor, _: &MouseDownEvent, window, cx| {
cx.stop_propagation();
editor.menu = None;
editor.italic(&Italic, window, cx);
}),
),
)
.child(
fmt_btn("menu-fmt-underline")
.underline()
.child("U")
.on_mouse_down(
MouseButton::Left,
cx.listener(|editor, _: &MouseDownEvent, window, cx| {
cx.stop_propagation();
editor.menu = None;
editor.underline(&Underline, window, cx);
}),
),
)
.child(
fmt_btn("menu-fmt-strike")
.line_through()
.child("S")
.on_mouse_down(
MouseButton::Left,
cx.listener(|editor, _: &MouseDownEvent, window, cx| {
cx.stop_propagation();
editor.menu = None;
editor.strike(&Strike, window, cx);
}),
),
)
.child(
fmt_btn("menu-fmt-code")
.font_family(
self.markdown_style
.as_ref()
.map(|s| s.mono.family.clone())
.unwrap_or_else(|| "monospace".into()),
)
.text_size(px(12.))
.child("<>")
.on_mouse_down(
MouseButton::Left,
cx.listener(|editor, _: &MouseDownEvent, window, cx| {
cx.stop_propagation();
editor.menu = None;
editor.code(&Code, window, cx);
}),
),
)
});
let cur_kind = self.block_kind_at(self.row_col(self.selected_range.start).0);
let turn_row = div()
.id("menu-turn-into")
.flex_shrink_0()
.px(px(10.))
.py(px(3.))
.hover(move |s| s.bg(hover))
.flex()
.flex_row()
.items_center()
.justify_between()
.gap(px(16.))
.child(self.labels.turn_into.clone())
.child(div().text_size(px(10.)).child("\u{25b8}"))
.on_hover(cx.listener(|editor, hovered: &bool, _, cx| {
if *hovered
&& let Some(m) = editor.menu.as_mut()
&& !m.turn_into
{
m.turn_into = true;
cx.notify();
}
}));
let turn_labels = self.labels.clone();
let turn_flyout = menu_turn_into.then(|| {
let rows: Vec<_> = TurnKind::ALL
.iter()
.enumerate()
.map(|(i, &k)| {
let checked = k == cur_kind;
div()
.id(("turn-kind", i))
.flex_shrink_0()
.px(px(10.))
.py(px(3.))
.hover(move |s| s.bg(hover))
.flex()
.flex_row()
.items_center()
.gap(px(6.))
.child(div().w(px(12.)).flex_shrink_0().child(if checked {
"\u{2713}"
} else {
""
}))
.child(k.label(&turn_labels))
.on_mouse_down(
MouseButton::Left,
cx.listener(move |editor, _: &MouseDownEvent, window, cx| {
cx.stop_propagation();
editor.menu = None;
editor.turn_into(k, window, cx);
}),
)
})
.collect();
div()
.absolute()
.left(gpui::relative(1.))
.bottom(px(0.))
.ml(px(2.))
.occlude()
.min_w(px(140.))
.cursor(CursorStyle::Arrow)
.bg(menu_bg)
.border_1()
.border_color(menu_border)
.rounded(px(6.))
.shadow_md()
.text_color(menu_fg)
.text_size(px(13.))
.flex()
.flex_col()
.py(px(4.))
.children(rows)
});
gpui::deferred(
gpui::anchored().position(anchor).snap_to_window().child(
div().relative().children(turn_flyout).child(
div()
.relative()
.occlude()
.min_w(px(150.))
.cursor(CursorStyle::Arrow)
.bg(menu_bg)
.border_1()
.border_color(menu_border)
.rounded(px(6.))
.shadow_md()
.overflow_hidden()
.text_color(menu_fg)
.text_size(px(13.))
.on_mouse_down_out(cx.listener(
|editor, _: &MouseDownEvent, _, cx| {
editor.menu = None;
cx.notify();
},
))
.children(format_bar)
.children(has_sel.then(|| div().h(px(1.)).bg(menu_border)))
.children((count > 0).then(|| {
div()
.id("suggestion-menu")
.max_h(px(MAX_H))
.overflow_y_scroll()
.track_scroll(&scroll)
.flex()
.flex_col()
.py(px(PAD))
.children(rows)
}))
.children((count > 0).then(|| div().h(px(1.)).bg(menu_border)))
.child(clipboard)
.child(div().h(px(1.)).bg(menu_border))
.child(div().flex().flex_col().py(px(4.)).child(turn_row))
.children(thumb),
),
),
)
}))
.children(self.table_menu.map(|anchor| {
let st = self.markdown_style.as_ref();
let menu_bg = st.map_or(rgb(0x26262b).into(), |s| s.popover_bg);
let menu_border = st.map_or(rgb(0x45454c).into(), |s| s.popover_border);
let menu_fg = st.map_or(rgb(0xe6e6e6).into(), |s| s.popover_fg);
let hover = st.map_or(rgba(0x2f6fd628).into(), |s| s.popover_hover);
let divider = st.map_or(rgba(0xffffff2e).into(), |s| s.popover_divider);
let mut thumb_c = st.map_or(rgba(0xffffff66).into(), |s| s.marker);
thumb_c.a = 0.5;
const ROW_H: f32 = 24.0;
const DIV_H: f32 = 9.0;
const PAD: f32 = 4.0;
const MAX_H: f32 = 480.0;
let danger = st.map_or(rgb(0xE5484D).into(), |s| s.popover_danger);
let cur_align = self.caret_table_align();
let cur_style = self
.caret_table_region()
.map(|r| r.style)
.unwrap_or_default();
use markdown_syntax::TableStyle as TS;
enum Row {
Div,
Item {
glyph: &'static str,
label: SharedString,
action: TableMenuAction,
red: bool,
checked: bool,
},
}
let item = |glyph, label, action| Row::Item {
glyph,
label,
action,
red: false,
checked: false,
};
let specs = [
item(
"↑",
self.labels.insert_row_above.clone(),
TableMenuAction::InsertRowAbove,
),
item(
"↓",
self.labels.insert_row_below.clone(),
TableMenuAction::InsertRowBelow,
),
item(
"⧉",
self.labels.duplicate_row.clone(),
TableMenuAction::DuplicateRow,
),
Row::Div,
item(
"←",
self.labels.insert_column_left.clone(),
TableMenuAction::InsertColLeft,
),
item(
"→",
self.labels.insert_column_right.clone(),
TableMenuAction::InsertColRight,
),
Row::Div,
Row::Item {
glyph: "",
label: self.labels.align_left.clone(),
action: TableMenuAction::AlignLeft,
red: false,
checked: cur_align == Some(CellAlign::Left),
},
Row::Item {
glyph: "",
label: self.labels.align_center.clone(),
action: TableMenuAction::AlignCenter,
red: false,
checked: cur_align == Some(CellAlign::Center),
},
Row::Item {
glyph: "",
label: self.labels.align_right.clone(),
action: TableMenuAction::AlignRight,
red: false,
checked: cur_align == Some(CellAlign::Right),
},
Row::Div,
Row::Item {
glyph: "▦",
label: self.labels.grid_style.clone(),
action: TableMenuAction::SetStyle(None),
red: false,
checked: cur_style == TS::Grid,
},
Row::Item {
glyph: "▤",
label: self.labels.striped_style.clone(),
action: TableMenuAction::SetStyle(Some("striped")),
red: false,
checked: cur_style == TS::Striped,
},
Row::Item {
glyph: "▥",
label: self.labels.header_style.clone(),
action: TableMenuAction::SetStyle(Some("header")),
red: false,
checked: cur_style == TS::Header,
},
Row::Item {
glyph: "─",
label: self.labels.minimal_style.clone(),
action: TableMenuAction::SetStyle(Some("minimal")),
red: false,
checked: cur_style == TS::Minimal,
},
Row::Div,
item(
"⊞",
self.labels.copy_as_markdown.clone(),
TableMenuAction::CopyTable,
),
Row::Div,
Row::Item {
glyph: "✕",
label: self.labels.delete_row.clone(),
action: TableMenuAction::DeleteRow,
red: true,
checked: false,
},
Row::Item {
glyph: "✕",
label: self.labels.delete_column.clone(),
action: TableMenuAction::DeleteColumn,
red: true,
checked: false,
},
Row::Item {
glyph: "✕",
label: self.labels.delete_table.clone(),
action: TableMenuAction::DeleteTable,
red: true,
checked: false,
},
];
let n_items = specs
.iter()
.filter(|r| matches!(r, Row::Item { .. }))
.count();
let n_divs = specs.len() - n_items;
let mut rows: Vec<gpui::AnyElement> = Vec::new();
for (i, spec) in specs.into_iter().enumerate() {
match spec {
Row::Div => rows.push(
div()
.flex_shrink_0()
.h(px(1.))
.my(px(4.))
.mx(px(8.))
.bg(divider)
.into_any_element(),
),
Row::Item {
glyph,
label,
action,
red,
checked,
} => {
let fg = if red { danger } else { menu_fg };
let mut glyph_c = fg;
glyph_c.a *= 0.7;
rows.push(
div()
.id(("table-menu-row", i))
.flex_shrink_0()
.px(px(10.))
.py(px(3.))
.flex()
.flex_row()
.items_center()
.gap(px(6.))
.text_color(fg)
.hover(move |s| s.bg(hover))
.child(
div()
.w(px(16.))
.flex_none()
.text_color(glyph_c)
.child(glyph),
)
.child(div().flex_1().child(label))
.children(checked.then(|| div().text_color(glyph_c).child("✓")))
.on_mouse_down(
MouseButton::Left,
cx.listener(move |editor, _: &MouseDownEvent, _, cx| {
cx.stop_propagation();
action.apply(editor, cx);
}),
)
.into_any_element(),
);
}
}
}
let rows_h = n_items as f32 * ROW_H + n_divs as f32 * DIV_H;
let view_h = MAX_H - 2.0 * PAD;
let thumb = (rows_h > view_h).then(|| {
let scrolled =
(-f32::from(self.table_menu_scroll.offset().y)).clamp(0.0, rows_h - view_h);
let thumb_h = (view_h * view_h / rows_h).max(24.0);
let thumb_top = PAD + scrolled / (rows_h - view_h) * (view_h - thumb_h);
div()
.absolute()
.top(px(thumb_top))
.right(px(2.))
.w(px(6.))
.h(px(thumb_h))
.rounded(px(3.))
.bg(thumb_c)
});
gpui::deferred(
gpui::anchored().position(anchor).snap_to_window().child(
div()
.relative()
.occlude()
.min_w(px(190.))
.cursor(CursorStyle::Arrow)
.bg(menu_bg)
.border_1()
.border_color(menu_border)
.rounded(px(6.))
.shadow_md()
.overflow_hidden()
.text_color(menu_fg)
.text_size(px(13.))
.on_mouse_down_out(cx.listener(|editor, _: &MouseDownEvent, _, cx| {
editor.table_menu = None;
cx.notify();
}))
.child(
div()
.id("table-menu")
.max_h(px(MAX_H))
.overflow_y_scroll()
.track_scroll(&self.table_menu_scroll)
.flex()
.flex_col()
.py(px(PAD))
.children(rows),
)
.children(thumb),
),
)
}))
.children(self.prop_menu.map(|(row, anchor)| {
let st = self.markdown_style.as_ref();
let menu_bg = st.map_or(rgb(0x26262b).into(), |s| s.popover_bg);
let menu_border = st.map_or(rgb(0x45454c).into(), |s| s.popover_border);
let menu_fg = st.map_or(rgb(0xe6e6e6).into(), |s| s.popover_fg);
let hover = st.map_or(rgba(0x2f6fd628).into(), |s| s.popover_hover);
let item = |id: &'static str, label: SharedString| {
div()
.id(id)
.px(px(10.))
.py(px(3.))
.hover(move |s| s.bg(hover))
.child(label)
};
gpui::deferred(
gpui::anchored().position(anchor).snap_to_window().child(
div()
.occlude()
.min_w(px(160.))
.cursor(CursorStyle::Arrow)
.bg(menu_bg)
.border_1()
.border_color(menu_border)
.rounded(px(6.))
.shadow_md()
.overflow_hidden()
.text_color(menu_fg)
.text_size(px(13.))
.py(px(4.))
.on_mouse_down_out(cx.listener(|editor, _: &MouseDownEvent, _, cx| {
editor.prop_menu = None;
cx.notify();
}))
.child(
item("prop-menu-edit", self.labels.edit_properties.clone())
.on_mouse_down(
MouseButton::Left,
cx.listener(move |editor, _: &MouseDownEvent, _, cx| {
cx.stop_propagation();
editor.prop_menu = None;
if let Some((range, source)) =
editor.property_block_at(row)
{
let block_row = row - editor.row_col(range.start).0;
cx.emit(EditorEvent::EditProperties {
range,
source,
at_end: false,
row: Some(block_row),
});
}
}),
),
)
.child(
item("prop-menu-delete", self.labels.delete_property.clone())
.on_mouse_down(
MouseButton::Left,
cx.listener(move |editor, _: &MouseDownEvent, _, cx| {
cx.stop_propagation();
editor.prop_menu = None;
editor.delete_property_row(row, cx);
}),
),
),
),
)
}))
.children(self.image_menu.map(|(line, anchor)| {
let st = self.markdown_style.as_ref();
let menu_bg = st.map_or(rgb(0x26262b).into(), |s| s.popover_bg);
let menu_border = st.map_or(rgb(0x45454c).into(), |s| s.popover_border);
let menu_fg = st.map_or(rgb(0xe6e6e6).into(), |s| s.popover_fg);
let hover = st.map_or(rgba(0x2f6fd628).into(), |s| s.popover_hover);
gpui::deferred(
gpui::anchored().position(anchor).snap_to_window().child(
div()
.occlude()
.min_w(px(140.))
.cursor(CursorStyle::Arrow)
.bg(menu_bg)
.border_1()
.border_color(menu_border)
.rounded(px(6.))
.shadow_md()
.overflow_hidden()
.text_color(menu_fg)
.text_size(px(13.))
.py(px(4.))
.on_mouse_down_out(cx.listener(|editor, _: &MouseDownEvent, _, cx| {
editor.image_menu = None;
cx.notify();
}))
.child(
div()
.id("image-menu-delete")
.px(px(10.))
.py(px(3.))
.hover(move |s| s.bg(hover))
.child(self.labels.delete_image.clone())
.on_mouse_down(
MouseButton::Left,
cx.listener(move |editor, _: &MouseDownEvent, _, cx| {
cx.stop_propagation();
editor.image_menu = None;
editor.delete_image_row(line, cx);
}),
),
),
),
)
}))
.children(self.code_lang_menu.map(|(row, anchor)| {
let st = self.markdown_style.as_ref();
let menu_bg = st.map_or(rgb(0x26262b).into(), |s| s.popover_bg);
let menu_border = st.map_or(rgb(0x45454c).into(), |s| s.popover_border);
let menu_fg = st.map_or(rgb(0xe6e6e6).into(), |s| s.popover_fg);
let hover = st.map_or(rgba(0x2f6fd628).into(), |s| s.popover_hover);
let mut thumb_c = st.map_or(rgba(0xffffff66).into(), |s| s.marker);
thumb_c.a = 0.5;
let current = self.code_block_at(row).map(|(l, _)| l).unwrap_or_default();
const ROW_H: f32 = 22.0;
const MAX_H: f32 = 260.0;
const PAD: f32 = 4.0;
let langs = self.code_langs.clone();
let rows_h = langs.len() as f32 * ROW_H;
let view_h = MAX_H - 2.0 * PAD;
let thumb = (rows_h > view_h).then(|| {
let scrolled =
(-f32::from(self.code_lang_scroll.offset().y)).clamp(0.0, rows_h - view_h);
let thumb_h = (view_h * view_h / rows_h).max(24.0);
let thumb_top = PAD + scrolled / (rows_h - view_h) * (view_h - thumb_h);
div()
.absolute()
.top(px(thumb_top))
.right(px(2.))
.w(px(6.))
.h(px(thumb_h))
.rounded(px(3.))
.bg(thumb_c)
});
gpui::deferred(
gpui::anchored().position(anchor).snap_to_window().child(
div()
.relative()
.occlude()
.min_w(px(140.))
.cursor(CursorStyle::Arrow)
.bg(menu_bg)
.border_1()
.border_color(menu_border)
.rounded(px(6.))
.shadow_md()
.overflow_hidden()
.text_color(menu_fg)
.text_size(px(13.))
.py(px(PAD))
.on_mouse_down_out(cx.listener(|editor, _: &MouseDownEvent, _, cx| {
editor.code_lang_menu = None;
cx.notify();
}))
.child(
div()
.id("code-lang-list")
.max_h(px(MAX_H - 2.0 * PAD))
.overflow_y_scroll()
.track_scroll(&self.code_lang_scroll)
.children(langs.into_iter().enumerate().map(|(i, lang)| {
let is_current = *lang == current
|| (current.is_empty() && *lang == *"text");
let label: SharedString = if is_current {
format!("{lang} ✓").into()
} else {
lang.clone()
};
div()
.id(("code-lang-row", i))
.flex_shrink_0()
.h(px(ROW_H))
.px(px(10.))
.py(px(2.))
.hover(move |s| s.bg(hover))
.child(label)
.on_mouse_down(
MouseButton::Left,
cx.listener(
move |editor, _: &MouseDownEvent, _, cx| {
cx.stop_propagation();
editor.code_lang_menu = None;
editor.set_code_lang(row, &lang, cx);
},
),
)
.into_any_element()
})),
)
.children(thumb),
),
)
}))
}
}
fn measure_width(window: &mut Window, text: &str, font: &Font, font_size: Pixels) -> Pixels {
if text.is_empty() {
return px(0.);
}
let run = TextRun {
len: text.len(),
font: font.clone(),
color: Hsla::default(),
background_color: None,
underline: None,
strikethrough: None,
};
window
.text_system()
.shape_line(
SharedString::from(text.to_string()),
font_size,
&[run],
None,
)
.width()
}
fn shape_runs(
window: &mut Window,
text: &SharedString,
font_size: Pixels,
runs: &[TextRun],
wrap_width: Option<Pixels>,
) -> Vec<WrappedLine> {
window
.text_system()
.shape_text(text.clone(), font_size, runs, wrap_width, None)
.map(|lines| lines.into_vec())
.unwrap_or_default()
}
#[derive(Clone)]
struct BlockImg {
img: Arc<RenderImage>,
width: Pixels,
height: Pixels,
resizable: bool,
align: MathAlign,
}
#[derive(Clone)]
struct InlineMath {
display_off: usize,
len: usize,
source: Range<usize>,
latex: SharedString,
img: Arc<RenderImage>,
width: Pixels,
height: Pixels,
}
#[derive(Clone)]
enum Block {
Image(BlockImg),
Chip {
src: SharedString,
label: SharedString,
link: Hsla,
bg: Hsla,
border: Hsla,
height: Pixels,
wiki: bool,
},
Properties(PropPanel),
}
#[derive(Clone)]
enum PanelSeg {
Plain(SharedString),
Pill {
text: SharedString,
color: Hsla,
target: zorite_markdown::syntax::LinkHit,
},
}
#[derive(Clone)]
struct PropPanel {
rows: Vec<(SharedString, Option<SharedString>, Vec<PanelSeg>)>,
key_w: Pixels,
width: Pixels,
row_h: Pixels,
height: Pixels,
icon_sz: Pixels,
key_indent: Pixels,
key_color: Hsla,
value_color: Hsla,
hover_border: Hsla,
}
impl Block {
fn height(&self) -> Pixels {
match self {
Block::Image(i) => i.height,
Block::Chip { height, .. } => *height,
Block::Properties(p) => p.height,
}
}
}
#[derive(Clone)]
struct CodeChipHit {
lang: Bounds<Pixels>,
copy: Bounds<Pixels>,
fence_row: usize,
}
struct CodeChip {
lang_text: SharedString,
copy_text: SharedString,
lang_bounds: Bounds<Pixels>,
copy_bounds: Bounds<Pixels>,
fence_row: usize,
bg: Hsla,
fg: Hsla,
lang_hb: Hitbox,
copy_hb: Hitbox,
}
#[derive(Clone, Copy)]
struct CodeBg {
color: Hsla,
width: Pixels,
top: bool,
bottom: bool,
}
#[derive(Clone)]
struct TableRow {
cells: Vec<SharedString>,
cell_ranges: Vec<Range<usize>>,
aligns: Vec<markdown_syntax::Align>,
col_widths: Vec<Pixels>,
is_header: bool,
is_separator: bool,
is_last: bool,
body_index: Option<usize>,
style: markdown_syntax::TableStyle,
border: Hsla,
shade: Hsla,
rtl: bool,
}
const CHECKBOX_SCALE: f32 = 0.9;
#[derive(Clone, Copy)]
enum LineMark {
Quote { bar: Hsla, text: Option<Hsla> },
Alert {
bar: Hsla,
label: &'static str,
kind: markdown_syntax::AlertKind,
text_inset: Pixels,
fold: Option<bool>,
chevron_x: Pixels,
},
List {
bullet_x: Pixels,
text_inset: Pixels,
ordered: bool,
num: u32,
level: usize,
color: Hsla,
},
Check {
bullet_x: Pixels,
text_inset: Pixels,
checked: bool,
color: Hsla,
accent: Hsla,
},
Rule(Hsla),
}
impl LineMark {
fn inset(self) -> Pixels {
match self {
LineMark::Quote { .. } => px(QUOTE_INSET),
LineMark::Alert { text_inset, .. } => text_inset,
LineMark::List { text_inset, .. } | LineMark::Check { text_inset, .. } => text_inset,
LineMark::Rule(_) => px(0.),
}
}
}
#[derive(Default)]
struct ShapedDoc {
wrapped: Vec<WrappedLine>,
heights: Vec<Pixels>,
widgets: Vec<Option<Block>>,
backgrounds: Vec<Option<CodeBg>>,
tables: Vec<Option<TableRow>>,
maps: Vec<Option<std::rc::Rc<Vec<usize>>>>,
marks: Vec<Option<LineMark>>,
inline_maths: Vec<Vec<InlineMath>>,
wrap_rows: Vec<usize>,
rtl_rows: Vec<Option<(bool, Vec<gpui_bidi::paragraph::Row>)>>,
}
impl ShapedDoc {
#[allow(clippy::too_many_arguments)]
fn push_placeholder(
&mut self,
window: &mut Window,
base_font_size: Pixels,
wrap_width: Option<Pixels>,
h: Pixels,
widget: Option<Block>,
mark: Option<LineMark>,
rows: usize,
) {
let wl = shape_runs(
window,
&SharedString::default(),
base_font_size,
&[],
wrap_width,
)
.into_iter()
.next()
.expect("a line always shapes to one wrapped line");
self.wrapped.push(wl);
self.heights.push(h);
self.widgets.push(widget);
self.backgrounds.push(None);
self.tables.push(None);
self.maps.push(None);
self.marks.push(mark);
self.inline_maths.push(Vec::new());
self.wrap_rows.push(rows);
self.rtl_rows.push(None);
}
}
fn set_image_width(line: &str, width: u32) -> String {
let Some((_, _, marker_len)) = markdown_syntax::image_row(line) else {
return line.to_string();
};
let trimmed_end = line.trim_end_matches([' ', '\t']);
let trailing_ws = &line[trimmed_end.len()..];
let close = marker_len + line[marker_len..].find(')').map_or(0, |i| i + 1);
let body = trimmed_end[..close.min(trimmed_end.len())].trim_end();
format!("{body}{{width={width}}}{trailing_ws}")
}
fn display_col_in(map: Option<&std::rc::Rc<Vec<usize>>>, source_col: usize) -> usize {
match map {
Some(m) => m.partition_point(|&s| s < source_col),
None => source_col,
}
}
fn line_pos(
line: &WrappedLine,
rtl: Option<&RtlRow>,
dcol: usize,
lh: Pixels,
) -> Option<Point<Pixels>> {
match rtl {
Some(r) => {
let (row, local) = r.row_of(dcol);
let x = px(r.rows.get(row)?.map.x_for_index(local)) + r.shift_delta(row);
Some(point(x, lh * row))
}
None => line.position_for_index(dcol, lh),
}
}
fn line_index_at(line: &WrappedLine, rtl: Option<&RtlRow>, p: Point<Pixels>, lh: Pixels) -> usize {
match rtl {
Some(r) if !r.rows.is_empty() => {
let row = ((f32::from(p.y) / f32::from(lh).max(1.0)).floor().max(0.) as usize)
.min(r.rows.len() - 1);
let x = p.x - r.shift_delta(row);
let Some(rr) = r.rows.get(row) else {
return 0;
};
rr.start + rr.map.index_for_x(f32::from(x))
}
_ => match line.closest_index_for_position(p, lh) {
Ok(i) | Err(i) => i,
},
}
}
pub(crate) struct RtlRow {
rows: Vec<gpui_bidi::paragraph::Row>,
shifts: Vec<Pixels>,
base_rtl: bool,
}
impl RtlRow {
fn row_of(&self, dcol: usize) -> (usize, usize) {
row_of_spans(self.rows.iter().map(|r| (r.start, r.len)), dcol)
}
pub(crate) fn row_extent(&self, row: usize) -> (Pixels, Pixels) {
let d = self.shift_delta(row);
(d, d + self.rows.get(row).map_or(px(0.), |r| r.width))
}
pub(crate) fn row_count(&self) -> usize {
self.rows.len()
}
fn shift_delta(&self, row: usize) -> Pixels {
self.shifts.get(row).copied().unwrap_or(px(0.))
- self.shifts.first().copied().unwrap_or(px(0.))
}
}
fn row_of_spans(rows: impl Iterator<Item = (usize, usize)>, dcol: usize) -> (usize, usize) {
let mut last = (0, dcol);
let mut seen = false;
for (i, (start, len)) in rows.enumerate() {
seen = true;
last = (i, dcol.saturating_sub(start));
if dcol < start + len {
return (i, dcol - start);
}
}
if seen { last } else { (0, dcol) }
}
fn rtl_shift(content_width: Pixels, inset: Pixels, line_width: Pixels) -> Pixels {
(content_width - inset * 2. - line_width).max(px(0.))
}
fn rtl_marker_x(content_width: Pixels, marker_x: Pixels, marker_width: Pixels) -> Pixels {
(content_width - marker_x - marker_width).max(px(0.))
}
fn checkbox_x(bullet_x: Pixels, size: Pixels, content_width: Pixels, rtl: bool) -> Pixels {
if rtl {
rtl_marker_x(content_width, bullet_x, size)
} else {
bullet_x
}
}
pub fn find_in_source(content: &str, query: &str) -> Vec<Range<usize>> {
let query: String = query.chars().flat_map(char::to_lowercase).collect();
if query.is_empty() {
return Vec::new();
}
let mut lower = String::with_capacity(content.len());
let mut map = Vec::with_capacity(content.len() + 1);
for (off, ch) in content.char_indices() {
for lc in ch.to_lowercase() {
lower.push(lc);
map.resize(lower.len(), off);
}
}
lower
.match_indices(&query)
.map(|(i, m)| map[i]..map.get(i + m.len()).copied().unwrap_or(content.len()))
.collect()
}
pub fn paint_doc_icon(
x: Pixels,
y: Pixels,
w: Pixels,
h: Pixels,
color: Hsla,
window: &mut Window,
) {
let f = w * 0.33; let mut outline = PathBuilder::stroke(px(1.3));
outline.move_to(point(x, y));
outline.line_to(point(x + w - f, y));
outline.line_to(point(x + w, y + f));
outline.line_to(point(x + w, y + h));
outline.line_to(point(x, y + h));
outline.line_to(point(x, y));
if let Ok(p) = outline.build() {
window.paint_path(p, color);
}
let mut fold = PathBuilder::stroke(px(1.3));
fold.move_to(point(x + w - f, y));
fold.line_to(point(x + w - f, y + f));
fold.line_to(point(x + w, y + f));
if let Ok(p) = fold.build() {
window.paint_path(p, color);
}
for fy in [0.6_f32, 0.78] {
let mut ln = PathBuilder::stroke(px(1.));
ln.move_to(point(x + w * 0.26, y + h * fy));
ln.line_to(point(x + w * 0.74, y + h * fy));
if let Ok(p) = ln.build() {
window.paint_path(p, color);
}
}
}
fn grip_left(bounds_left: Pixels, inset: Pixels) -> Pixels {
bounds_left - px(22.) - inset
}
struct CachedLineRuns {
src: String,
line_base: Hsla,
disp: SharedString,
runs: std::rc::Rc<Vec<TextRun>>,
map: std::rc::Rc<Vec<usize>>,
}
type RowKeys = (Option<u64>, Vec<Option<u64>>);
type RegionCols = Option<(u64, std::rc::Rc<Vec<Vec<Pixels>>>)>;
#[derive(Default)]
struct ShapeCaches {
line_runs: std::cell::RefCell<std::collections::HashMap<u64, CachedLineRuns>>,
region_cols: std::cell::RefCell<RegionCols>,
cell_rows: std::cell::RefCell<std::collections::HashMap<u64, usize>>,
line_heights: std::cell::RefCell<std::collections::HashMap<u64, (Pixels, usize)>>,
row_keys: std::cell::RefCell<RowKeys>,
}
fn line_run_epoch(font: &Font, st: Option<&SyntaxStyle>) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
font.family.hash(&mut h);
font.weight.0.to_bits().hash(&mut h);
let hash_hsla = |c: Hsla, h: &mut std::collections::hash_map::DefaultHasher| {
c.h.to_bits().hash(h);
c.s.to_bits().hash(h);
c.l.to_bits().hash(h);
c.a.to_bits().hash(h);
};
if let Some(st) = st {
for c in [
st.marker, st.code, st.code_bg, st.link, st.tag, st.quote, st.mark_bg,
] {
hash_hsla(c, &mut h);
}
st.mono.family.hash(&mut h);
st.block_label_gen.hash(&mut h);
}
h.finish()
}
pub type ScrollCompensatorFn = std::rc::Rc<dyn Fn(Pixels, &mut Window, &mut App)>;
pub(crate) struct ScanData {
generation: u64,
ordered: Vec<(u32, usize)>,
tables: Vec<markdown_syntax::TableRegion>,
mermaid: Vec<(Range<usize>, String)>,
math: Vec<markdown_syntax::MathRegion>,
props: Vec<Range<usize>>,
alert_folds: Vec<(Range<usize>, bool)>,
fence_odd: Vec<bool>,
}
struct ShapeMemo {
wrap_width: Option<Pixels>,
caret_row: Option<usize>,
selection: (usize, usize),
font_size: Pixels,
shaped: ShapedDoc,
}
fn word_boundary_input(new_text: &str) -> bool {
match new_text.as_bytes() {
[c] => !c.is_ascii_alphanumeric() && !matches!(c, b'_' | b'-' | b'/' | b'#' | b'[' | b':'),
[b'\n', ..] => true,
_ => false,
}
}
#[cfg(test)]
mod tests {
#[test]
fn caret_off_marker_line_cases() {
use super::caret_off_marker_line;
let doc = "<!-- table:grid cols=40,40 -->\n| a | b |\n| --- | --- |\n| 1 | 2 |";
assert_eq!(caret_off_marker_line(doc, 0), 31);
let doc = "<!-- math:center -->\n$$\nx^2\n$$\nafter";
assert_eq!(caret_off_marker_line(doc, 0), 31);
assert_eq!(&doc[31..], "after");
assert_eq!(caret_off_marker_line("hello\nworld", 0), 0);
assert_eq!(caret_off_marker_line("", 0), 0);
}
#[test]
fn strip_block_prefix_cases() {
use super::strip_block_prefix;
assert_eq!(strip_block_prefix("# Title"), "Title");
assert_eq!(strip_block_prefix("### Deep"), "Deep");
assert_eq!(strip_block_prefix("- [x] done"), "done");
assert_eq!(strip_block_prefix("- item"), "item");
assert_eq!(strip_block_prefix("12. nth"), "nth");
assert_eq!(strip_block_prefix("> quoted"), "quoted");
assert_eq!(strip_block_prefix(">bare"), "bare");
assert_eq!(strip_block_prefix("plain"), "plain");
assert_eq!(strip_block_prefix("* [ ] star task"), "star task");
assert_eq!(strip_block_prefix("+ [x] plus task"), "plus task");
assert_eq!(strip_block_prefix("3) paren"), "paren");
assert_eq!(strip_block_prefix("#### H4"), "H4");
assert_eq!(strip_block_prefix("#tag"), "#tag");
assert_eq!(strip_block_prefix("-dash"), "-dash");
}
#[test]
fn find_in_source_cases() {
use super::find_in_source;
assert_eq!(find_in_source("aa bb aa", "aa"), vec![0..2, 6..8]);
assert_eq!(
find_in_source("Grüße hier", "grüsse"),
Vec::<std::ops::Range<usize>>::new()
);
assert_eq!(find_in_source("Grüße", "grüße"), vec![0..7]);
assert_eq!(find_in_source("İstanbul", "i̇stanbul"), vec![0..9]);
assert_eq!(
find_in_source("abc", ""),
Vec::<std::ops::Range<usize>>::new()
);
}
use super::{display_col_in, set_image_width, word_boundary_input};
#[test]
fn word_boundaries_offer_auto_replace_but_colon_never_does() {
assert!(word_boundary_input(" "));
assert!(word_boundary_input("."));
assert!(word_boundary_input("\n"));
assert!(word_boundary_input("\n- "));
assert!(!word_boundary_input("a"));
assert!(!word_boundary_input("["));
assert!(!word_boundary_input("#"));
assert!(!word_boundary_input(":"));
}
#[test]
fn display_col_leftmost_for_inline_math_spacer() {
let map = std::rc::Rc::new(vec![0, 1, 2, 2, 2, 5, 6, 7]);
assert_eq!(display_col_in(Some(&map), 2), 2);
assert_eq!(display_col_in(Some(&map), 5), 5);
let plain = std::rc::Rc::new(vec![0, 1, 2, 3]);
assert_eq!(display_col_in(Some(&plain), 2), 2);
assert_eq!(display_col_in(None, 4), 4);
}
#[test]
fn image_width_splice() {
assert_eq!(
set_image_width("", 200),
"{width=200}"
);
assert_eq!(
set_image_width("{width=320}", 200),
"{width=200}"
);
assert_eq!(
set_image_width("{width=320px}", 200),
"{width=200}"
);
assert_eq!(
set_image_width("- {width=10}", 50),
"- {width=50}"
);
assert_eq!(
set_image_width(" ", 80),
"{width=80} "
);
assert_eq!(set_image_width("just text", 100), "just text");
}
use super::{checkbox_x, px, row_of_spans, rtl_marker_x, rtl_shift};
#[test]
fn a_caret_offset_lands_on_the_row_that_holds_it() {
let rows = || [(0usize, 5usize), (5, 6), (11, 3)].into_iter();
assert_eq!(row_of_spans(rows(), 0), (0, 0));
assert_eq!(row_of_spans(rows(), 4), (0, 4));
assert_eq!(row_of_spans(rows(), 5), (1, 0));
assert_eq!(row_of_spans(rows(), 12), (2, 1));
assert_eq!(row_of_spans(rows(), 14), (2, 3));
assert_eq!(row_of_spans(rows(), 99), (2, 88));
assert_eq!(row_of_spans([].into_iter(), 0), (0, 0));
}
#[test]
fn rtl_shift_mirrors_the_row_inset() {
assert_eq!(rtl_shift(px(500.), px(0.), px(200.)), px(300.));
assert_eq!(rtl_shift(px(500.), px(24.), px(200.)), px(252.));
assert_eq!(px(24.) + rtl_shift(px(500.), px(24.), px(200.)), px(276.));
assert_eq!(rtl_shift(px(500.), px(0.), px(500.)), px(0.));
assert_eq!(rtl_shift(px(500.), px(0.), px(900.)), px(0.));
assert_eq!(rtl_shift(px(100.), px(60.), px(50.)), px(0.));
}
#[test]
fn rtl_markers_mirror_to_the_right_edge() {
assert_eq!(rtl_marker_x(px(500.), px(10.), px(8.)), px(482.));
let l1 = rtl_marker_x(px(500.), px(10.), px(8.));
let l2 = rtl_marker_x(px(500.), px(34.), px(8.));
assert!(l2 < l1, "level 2 must sit further in from the right");
assert_eq!(l1 - l2, px(24.), "the indent step survives the mirror");
assert_eq!(rtl_marker_x(px(20.), px(10.), px(40.)), px(0.));
assert_eq!(checkbox_x(px(10.), px(12.), px(500.), true), px(478.));
assert_eq!(checkbox_x(px(10.), px(12.), px(500.), false), px(10.));
}
use super::tables::{TABLE_GUTTER, table_left_x, table_visible_band};
const O: f32 = 100.;
const W: f32 = 500.;
#[test]
fn an_rtl_table_hugs_the_right_edge_with_the_gutter_mirrored() {
let g = TABLE_GUTTER;
let ltr = table_left_x(px(O), px(W), px(300.), px(0.), false);
let rtl = table_left_x(px(O), px(W), px(300.), px(0.), true);
assert_eq!(ltr, px(O + g));
assert_eq!(rtl + px(300.), px(O + W - g), "right edge, one gutter in");
assert_eq!(
f32::from(ltr - px(O)),
f32::from(px(O + W) - (rtl + px(300.)))
);
assert_eq!(
table_left_x(px(O), px(W), px(120.), px(0.), false),
px(O + g)
);
assert_eq!(
table_left_x(px(O), px(W), px(120.), px(0.), true),
px(O + W - g - 120.)
);
}
#[test]
fn a_wide_table_scrolls_to_its_own_far_edge_either_way() {
let g = TABLE_GUTTER;
let total = px(900.);
let avail = px(W - g); let max = total - avail;
assert_eq!(table_left_x(px(O), px(W), total, px(0.), false), px(O + g));
assert_eq!(
table_left_x(px(O), px(W), total, px(0.), true) + total,
px(O + W - g)
);
assert_eq!(
table_left_x(px(O), px(W), total, max, false) + total,
px(O + W)
);
assert_eq!(table_left_x(px(O), px(W), total, max, true), px(O));
let step = px(50.);
assert!(table_left_x(px(O), px(W), total, step, false) < px(O + g));
assert!(
table_left_x(px(O), px(W), total, step, true)
> table_left_x(px(O), px(W), total, px(0.), true)
);
}
#[test]
fn rtl_mirrors_the_column_order() {
use super::tables::{cell_span_width, col_offset};
let w = [px(10.), px(20.), px(30.)];
assert_eq!(col_offset(&w, 3, 0, false), px(0.));
assert_eq!(col_offset(&w, 3, 1, false), px(10.));
assert_eq!(col_offset(&w, 3, 2, false), px(30.));
assert_eq!(col_offset(&w, 3, 0, true), px(50.));
assert_eq!(col_offset(&w, 3, 1, true), px(30.));
assert_eq!(col_offset(&w, 3, 2, true), px(0.));
for rtl in [false, true] {
let mut spans: Vec<(f32, f32)> = (0..3)
.map(|c| {
let x = f32::from(col_offset(&w, 3, c, rtl));
(x, x + f32::from(cell_span_width(&w, 3, c)))
})
.collect();
spans.sort_by(|a, b| a.0.total_cmp(&b.0));
assert_eq!(spans[0].0, 0.0);
assert_eq!(spans[2].1, 60.0);
assert!(spans.windows(2).all(|s| s[0].1 == s[1].0), "{spans:?}");
}
}
#[test]
fn the_visible_band_is_the_column_less_its_gutter() {
let g = TABLE_GUTTER;
assert_eq!(
table_visible_band(px(O), px(W), false),
(px(O + g), px(O + W))
);
assert_eq!(
table_visible_band(px(O), px(W), true),
(px(O), px(O + W - g))
);
for rtl in [false, true] {
let (l, r) = table_visible_band(px(O), px(W), rtl);
assert_eq!(r - l, px(W - g));
}
}
}