use super::input_rule::RuleKind;
use std::sync::Arc;
use std::{cell::RefCell, ops::Range, rc::Rc};
use gpui::{
App, AppContext, Bounds, ClipboardItem, Context, Entity, EntityInputHandler, EventEmitter,
FocusHandle, Focusable, KeyBinding, Pixels, Point, SharedString, Subscription, UTF16Selection,
Window, actions,
};
use super::{
command::{CommandError, EditorCommand},
editor::BlockEditorModel,
frame::FrameLayout,
id::BlockId,
kind::BlockKind,
marks::MarkKind,
position::BlockPosition,
projection::{ProjectionCache, Snapshot},
selection::BlockSelection,
style::BlockEditorStyle,
transaction::EditOrigin,
};
use crate::support::BlinkCursor;
use gpui_kit::base::text::MarkdownExtensions;
pub(crate) const CONTEXT: &str = "BlockEditor";
actions!(
block_editor,
[
Backspace,
Delete,
Enter,
StructuralEnter,
MoveLeft,
MoveRight,
MoveUp,
MoveDown,
MoveHome,
MoveEnd,
SelectLeft,
SelectRight,
SelectUp,
SelectDown,
SelectHome,
SelectEnd,
MoveDocStart,
MoveDocEnd,
SelectDocStart,
SelectDocEnd,
SelectAll,
MoveWordLeft,
MoveWordRight,
SelectWordLeft,
SelectWordRight,
Undo,
Redo,
ToggleBold,
ToggleItalic,
EditLink,
SoftBreak,
ExitBlock,
ToggleCode,
NormalText,
TableInsertColumnLeft,
TableInsertColumnRight,
TableInsertRowAbove,
TableInsertRowBelow,
TableDeleteLine,
CloseTableMenu,
Indent,
Outdent,
Copy,
Cut,
Paste,
PastePlain,
DeleteWordLeft,
DeleteWordRight,
DeleteToLineStart,
DeleteToLineEnd,
MoveParagraphStart,
MoveParagraphEnd,
SelectParagraphStart,
SelectParagraphEnd,
]
);
pub(crate) fn init(cx: &mut App) {
let ctx = Some(CONTEXT);
#[cfg(target_os = "macos")]
let m = "cmd";
#[cfg(not(target_os = "macos"))]
let m = "ctrl";
#[cfg(target_os = "macos")]
let w = "alt";
#[cfg(not(target_os = "macos"))]
let w = "ctrl";
let doc = m;
cx.bind_keys(vec![
KeyBinding::new("backspace", Backspace, ctx),
KeyBinding::new("delete", Delete, ctx),
KeyBinding::new("enter", Enter, ctx),
KeyBinding::new("left", MoveLeft, ctx),
KeyBinding::new("right", MoveRight, ctx),
KeyBinding::new("up", MoveUp, ctx),
KeyBinding::new("down", MoveDown, ctx),
KeyBinding::new("home", MoveHome, ctx),
KeyBinding::new("end", MoveEnd, ctx),
KeyBinding::new(&format!("{w}-left"), MoveWordLeft, ctx),
KeyBinding::new(&format!("{w}-right"), MoveWordRight, ctx),
KeyBinding::new(&format!("{w}-shift-left"), SelectWordLeft, ctx),
KeyBinding::new(&format!("{w}-shift-right"), SelectWordRight, 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("shift-home", SelectHome, ctx),
KeyBinding::new("shift-end", SelectEnd, ctx),
KeyBinding::new(&format!("{doc}-home"), MoveDocStart, ctx),
KeyBinding::new(&format!("{doc}-end"), MoveDocEnd, ctx),
KeyBinding::new(&format!("{doc}-shift-home"), SelectDocStart, ctx),
KeyBinding::new(&format!("{doc}-shift-end"), SelectDocEnd, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-up", MoveDocStart, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-down", MoveDocEnd, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-shift-up", SelectDocStart, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-shift-down", SelectDocEnd, ctx),
KeyBinding::new(&format!("{w}-backspace"), DeleteWordLeft, ctx),
KeyBinding::new(&format!("{w}-delete"), DeleteWordRight, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-backspace", DeleteToLineStart, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-delete", DeleteToLineEnd, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("ctrl-k", DeleteToLineEnd, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("ctrl-a", MoveHome, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("ctrl-e", MoveEnd, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("ctrl-shift-a", SelectHome, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("ctrl-shift-e", SelectEnd, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("ctrl-d", Delete, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("ctrl-h", Backspace, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-left", MoveHome, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-right", MoveEnd, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-shift-left", SelectHome, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-shift-right", SelectEnd, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("alt-up", MoveParagraphStart, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("alt-down", MoveParagraphEnd, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("alt-shift-up", SelectParagraphStart, ctx),
#[cfg(target_os = "macos")]
KeyBinding::new("alt-shift-down", SelectParagraphEnd, ctx),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-up", MoveParagraphStart, ctx),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-down", MoveParagraphEnd, ctx),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-shift-up", SelectParagraphStart, ctx),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-shift-down", SelectParagraphEnd, ctx),
KeyBinding::new("tab", Indent, ctx),
KeyBinding::new("shift-tab", Outdent, ctx),
KeyBinding::new(&format!("{m}-a"), SelectAll, ctx),
KeyBinding::new(&format!("{m}-z"), Undo, ctx),
KeyBinding::new(&format!("{m}-shift-z"), Redo, ctx),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-y", Redo, ctx),
KeyBinding::new(&format!("{m}-b"), ToggleBold, ctx),
KeyBinding::new(&format!("{m}-i"), ToggleItalic, ctx),
KeyBinding::new(&format!("{m}-e"), ToggleCode, ctx),
KeyBinding::new(&format!("{m}-k"), EditLink, ctx),
KeyBinding::new(&format!("{m}-alt-0"), NormalText, ctx),
KeyBinding::new(&format!("{m}-alt-left"), TableInsertColumnLeft, ctx),
KeyBinding::new(&format!("{m}-alt-right"), TableInsertColumnRight, ctx),
KeyBinding::new(&format!("{m}-alt-up"), TableInsertRowAbove, ctx),
KeyBinding::new(&format!("{m}-alt-down"), TableInsertRowBelow, ctx),
KeyBinding::new(&format!("{m}-alt-backspace"), TableDeleteLine, ctx),
KeyBinding::new("escape", CloseTableMenu, Some("BlockEditor && table_menu")),
KeyBinding::new("shift-enter", SoftBreak, ctx),
KeyBinding::new(&format!("{m}-enter"), ExitBlock, ctx),
KeyBinding::new(&format!("{m}-c"), Copy, ctx),
KeyBinding::new(&format!("{m}-x"), Cut, ctx),
KeyBinding::new(&format!("{m}-v"), Paste, ctx),
KeyBinding::new(&format!("{m}-shift-v"), PastePlain, ctx),
KeyBinding::new(
"shift-enter",
StructuralEnter,
Some("BlockEditor && submit == enter"),
),
KeyBinding::new(
"ctrl-enter",
SoftBreak,
Some("BlockEditor && submit == enter"),
),
]);
}
pub(crate) const COPY_TAG: &str = "gpui-kit/block-editor/inline-markdown/v1";
pub(crate) const BLOCKS_COPY_TAG: &str = "gpui-kit/block-editor/blocks-markdown/v1";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum CopyKind {
External,
Inline,
Blocks,
}
impl CopyKind {
fn of(item: &ClipboardItem) -> Self {
match item.metadata() {
Some(m) if m == COPY_TAG => Self::Inline,
Some(m) if m == BLOCKS_COPY_TAG => Self::Blocks,
_ => Self::External,
}
}
fn tag(self) -> Option<&'static str> {
match self {
Self::External => None,
Self::Inline => Some(COPY_TAG),
Self::Blocks => Some(BLOCKS_COPY_TAG),
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub enum BlockEditorEvent {
Changed {
revision: u64,
origin: EditOrigin,
},
SelectionChanged,
EditLinkRequested {
url: Option<SharedString>,
},
LinkClicked {
url: SharedString,
},
LinkHovered {
url: Option<SharedString>,
},
Focus,
Blur,
SubmitRequested,
PasteRequested {
request: PasteRequest,
item: ClipboardItem,
},
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SubmitPolicy {
#[default]
Newline,
Enter,
ModifiedEnter,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ClipboardPolicy {
#[default]
Editor,
Host,
}
type InputRuleFilter = Arc<dyn Fn(super::InputRule, &str) -> bool + Send + Sync>;
#[derive(Clone, Debug)]
pub struct PasteRequest {
range: super::CapturedRange,
plain: bool,
copy: CopyKind,
sequence: u64,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct BlockDrag {
pub(crate) block: BlockId,
pub(crate) start: Point<Pixels>,
pub(crate) active: bool,
pub(crate) drop: Option<usize>,
}
#[derive(Clone, Debug)]
struct Marked {
block: BlockId,
range: Range<usize>,
}
pub struct BlockEditorState {
pub(crate) model: BlockEditorModel,
pub(crate) focus_handle: FocusHandle,
pub(crate) blink_cursor: Entity<BlinkCursor>,
pub(crate) style: BlockEditorStyle,
pub(crate) projection: ProjectionCache,
pub(crate) frame: Rc<RefCell<FrameLayout>>,
pub(crate) cull: Rc<RefCell<super::cull::CullState>>,
pub(crate) a11y_value: RefCell<Option<(u64, SharedString)>>,
pub(crate) scroll_handle: Option<gpui::ScrollHandle>,
pub(crate) reveal: std::cell::Cell<bool>,
marked: Option<Marked>,
composition_open: bool,
goal_x: Option<Pixels>,
pub(crate) dragging: bool,
pub(crate) block_drag: Option<BlockDrag>,
readonly: bool,
image_policy: super::ImagePolicy,
submit_policy: SubmitPolicy,
clipboard_policy: ClipboardPolicy,
paste_sequence: u64,
pending_paste: Option<u64>,
input_rules: Vec<super::input_rule::InputRule>,
input_rule_filter: Option<InputRuleFilter>,
hovered_link: Option<SharedString>,
pub(crate) pointer_hand: bool,
pub(crate) table_menu: Option<super::table_ui::TableMenu>,
pub(crate) table_hover: Vec<(BlockId, crate::TableLine)>,
pub(crate) grip_hover: bool,
_subscriptions: Vec<Subscription>,
}
impl EventEmitter<BlockEditorEvent> for BlockEditorState {}
impl Focusable for BlockEditorState {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl BlockEditorState {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();
let blink_cursor = cx.new(|_| BlinkCursor::new());
let subscriptions = vec![
cx.observe(&blink_cursor, |_, _, cx| cx.notify()),
cx.on_focus(&focus_handle, window, |this, _, cx| {
this.blink_cursor.update(cx, |b, cx| b.start(cx));
cx.emit(BlockEditorEvent::Focus);
}),
cx.on_blur(&focus_handle, window, |this, _, cx| {
this.blink_cursor.update(cx, |b, cx| b.stop(cx));
this.model.break_coalescing();
this.end_composition();
cx.emit(BlockEditorEvent::Blur);
}),
cx.observe_window_activation(window, |this, window, cx| {
if window.is_window_active() && this.focus_handle.is_focused(window) {
this.blink_cursor.update(cx, |b, cx| b.start(cx));
}
}),
];
let model = BlockEditorModel::from_markdown("", MarkdownExtensions::default())
.unwrap_or_else(|_| unreachable!("empty source always parses"));
Self {
model,
focus_handle,
blink_cursor,
style: BlockEditorStyle::default(),
projection: ProjectionCache::default(),
frame: Rc::default(),
cull: Rc::default(),
a11y_value: RefCell::default(),
scroll_handle: Some(gpui::ScrollHandle::new()),
reveal: Default::default(),
marked: None,
composition_open: false,
goal_x: None,
dragging: false,
block_drag: None,
readonly: false,
image_policy: super::ImagePolicy::default(),
submit_policy: SubmitPolicy::default(),
clipboard_policy: ClipboardPolicy::default(),
paste_sequence: 0,
pending_paste: None,
input_rules: super::input_rule::InputRule::markdown_defaults(),
input_rule_filter: None,
hovered_link: None,
pointer_hand: false,
table_menu: None,
table_hover: Vec::new(),
grip_hover: false,
_subscriptions: subscriptions,
}
}
pub fn with_submit_policy(mut self, policy: SubmitPolicy) -> Self {
self.submit_policy = policy;
self
}
pub fn set_submit_policy(&mut self, policy: SubmitPolicy) {
self.submit_policy = policy;
}
pub(crate) fn key_context(&self) -> &'static str {
match (
self.submit_policy == SubmitPolicy::Enter,
self.table_menu.is_some(),
) {
(true, false) => "BlockEditor submit=enter",
(true, true) => "BlockEditor submit=enter table_menu",
(false, false) => CONTEXT,
(false, true) => "BlockEditor table_menu",
}
}
pub fn with_clipboard_policy(mut self, policy: ClipboardPolicy) -> Self {
self.clipboard_policy = policy;
self
}
pub fn set_clipboard_policy(&mut self, policy: ClipboardPolicy) {
self.clipboard_policy = policy;
}
pub fn scroll_handle(&self) -> Option<gpui::ScrollHandle> {
self.scroll_handle.clone()
}
pub fn with_input_rules(
mut self,
rules: impl IntoIterator<Item = super::input_rule::InputRule>,
) -> Self {
self.input_rules = rules.into_iter().collect();
self
}
pub fn with_input_rule_filter(
mut self,
filter: impl Fn(super::InputRule, &str) -> bool + Send + Sync + 'static,
) -> Self {
self.input_rule_filter = Some(Arc::new(filter));
self
}
pub fn set_input_rules(
&mut self,
rules: impl IntoIterator<Item = super::input_rule::InputRule>,
) {
self.input_rules = rules.into_iter().collect();
}
pub fn set_task_checked(&mut self, block: BlockId, checked: bool, cx: &mut Context<Self>) {
if self.readonly {
return;
}
let leaf = if self.model.doc.inline(block).is_some() {
block
} else {
match self.model.doc.children(block).first() {
Some(child) => *child,
None => return,
}
};
self.model.break_coalescing();
self.edit(
EditOrigin::Command("set_task_checked"),
|m| m.set_item_checked(leaf, checked).map(|_| ()),
cx,
);
self.model.break_coalescing();
}
fn rule_on(&self, kind: super::input_rule::RuleKind) -> bool {
self.input_rules.iter().any(|rule| rule.0 == kind)
&& self.input_rule_filter.as_ref().is_none_or(|filter| {
filter(
super::InputRule(kind),
self.model.caret_prefix().unwrap_or_default(),
)
})
}
pub fn with_source(mut self, source: &str) -> Self {
let _ = self.model.set_source(source);
self.projection.invalidate();
self
}
pub fn set_source(&mut self, source: &str, cx: &mut Context<Self>) -> Result<(), SharedString> {
self.model.set_source(source)?;
self.marked = None;
self.composition_open = false;
self.pending_paste = None;
self.projection.invalidate();
cx.notify();
Ok(())
}
pub fn source(&self) -> String {
self.model.source()
}
pub fn source_snapshot(&self) -> super::SourceSnapshot {
self.model.source_snapshot()
}
pub fn capture_caret(&self) -> Result<super::CapturedRange, super::CapturedRangeError> {
self.model.capture_caret()
}
pub fn capture_selection(&self) -> Result<super::CapturedRange, super::CapturedRangeError> {
self.model.capture_selection()
}
pub fn capture_suffix(
&self,
len: usize,
) -> Result<super::CapturedRange, super::CapturedRangeError> {
self.model.capture_suffix(len)
}
pub fn capture_leaf_range(
&self,
range: std::ops::Range<usize>,
) -> Result<super::CapturedRange, super::CapturedRangeError> {
self.model.capture_leaf_range(range)
}
pub fn with_image_policy(mut self, policy: super::ImagePolicy) -> Self {
self.image_policy = policy;
self
}
pub fn set_image_policy(&mut self, policy: super::ImagePolicy, cx: &mut Context<Self>) {
self.image_policy = policy;
self.projection.invalidate();
cx.notify();
}
pub fn image_policy(&self) -> super::ImagePolicy {
self.image_policy
}
pub fn caret_prefix_snapshot(&self) -> Option<SharedString> {
self.model.caret_prefix_snapshot()
}
pub fn caret_prefix(&self) -> Option<&str> {
self.model.caret_prefix()
}
pub fn is_capture_current(&self, range: &super::CapturedRange) -> bool {
self.model.is_capture_current(range)
}
pub fn replace_captured(
&mut self,
range: &mut super::CapturedRange,
text: &str,
cx: &mut Context<Self>,
) -> Result<(), super::CapturedRangeError> {
if self.readonly || self.marked.is_some() {
return Err(super::CapturedRangeError::Stale);
}
self.model.replace_captured(range, text)?;
self.after_edit(EditOrigin::Api, cx);
Ok(())
}
pub fn select_end(&mut self, cx: &mut Context<Self>) -> Result<(), super::SelectionError> {
self.model.select_end()?;
self.selection_changed(cx);
Ok(())
}
pub fn reveal_caret(&mut self, cx: &mut Context<Self>) {
self.reveal.set(true);
cx.notify();
}
pub fn invalidate_captures(&mut self) {
self.model.invalidate_captures();
self.pending_paste = None;
}
pub fn is_composing(&self) -> bool {
self.marked.is_some()
}
pub fn model(&self) -> &BlockEditorModel {
&self.model
}
pub fn revision(&self) -> u64 {
self.model.revision()
}
pub fn selection(&self) -> &BlockSelection {
self.model.selection()
}
pub fn block_kind_at_caret(&self) -> Option<BlockKind> {
self.model.block_kind_at_caret()
}
pub fn set_editor_style(&mut self, style: BlockEditorStyle, cx: &mut Context<Self>) {
self.style = style;
cx.notify();
}
pub fn editor_style(&self) -> &BlockEditorStyle {
&self.style
}
pub fn set_scroll_handle(&mut self, handle: gpui::ScrollHandle) {
self.scroll_handle = Some(handle);
}
pub fn is_readonly(&self) -> bool {
self.readonly
}
pub fn set_readonly(&mut self, readonly: bool, cx: &mut Context<Self>) {
self.readonly = readonly;
cx.notify();
}
pub fn focus(&self, window: &mut Window, cx: &mut App) {
window.focus(&self.focus_handle, cx);
}
pub fn set_selection(
&mut self,
selection: BlockSelection,
cx: &mut Context<Self>,
) -> Result<(), super::SelectionError> {
self.model.set_selection(selection)?;
self.selection_changed(cx);
Ok(())
}
pub fn run(
&mut self,
command: EditorCommand,
cx: &mut Context<Self>,
) -> Result<bool, CommandError> {
if self.readonly {
return Ok(false);
}
let name = command_name(&command);
self.end_composition();
let revision = self.model.revision();
let changed = self.model.run(command)?;
if self.model.revision() != revision {
self.after_edit(EditOrigin::Command(name), cx);
} else {
cx.notify();
}
Ok(changed)
}
pub fn undo(&mut self, cx: &mut Context<Self>) {
if self.readonly {
return;
}
if self.model.undo() {
self.after_edit(EditOrigin::Undo, cx);
}
}
pub fn redo(&mut self, cx: &mut Context<Self>) {
if self.readonly {
return;
}
if self.model.redo() {
self.after_edit(EditOrigin::Redo, cx);
}
}
pub(crate) fn accessible_value(&self) -> SharedString {
let revision = self.model.revision();
if let Some((cached, value)) = &*self.a11y_value.borrow()
&& *cached == revision
{
return value.clone();
}
let doc = &self.model.doc;
let value: SharedString = doc
.leaves()
.iter()
.filter_map(|id| doc.leaf_text(*id))
.map(|text| text.replace(super::inline::ATOM_CHAR, ""))
.collect::<Vec<_>>()
.join("\n")
.into();
*self.a11y_value.borrow_mut() = Some((revision, value.clone()));
value
}
pub(crate) fn snapshot(&mut self) -> Rc<Snapshot> {
self.projection
.set_placeholders(self.image_policy == super::ImagePolicy::Placeholder);
self.projection.snapshot(&self.model.doc)
}
pub(crate) fn pinned_blocks(&self) -> Vec<BlockId> {
let doc = &self.model.doc;
let mut ids = match self.model.selection() {
BlockSelection::Text { anchor, head } => vec![anchor.block, head.block],
BlockSelection::Blocks { anchor, head } | BlockSelection::Cells { anchor, head } => {
vec![*anchor, *head]
}
};
if let Some((block, _)) = self.marked_range() {
ids.push(block);
}
ids.into_iter().flat_map(|id| doc.path(id)).collect()
}
pub(crate) fn selection_ranges(&self) -> Vec<(BlockId, Range<usize>)> {
if let Some(cells) = self.model.selected_cells() {
return cells
.into_iter()
.flatten()
.map(|c| (c, 0..self.model.doc.leaf_len(c).unwrap_or(0)))
.collect();
}
let Ok(range) = self.model.doc.resolve_selection(self.model.selection()) else {
return Vec::new();
};
if range.is_collapsed() {
return Vec::new();
}
self.model
.text_ranges(range)
.into_iter()
.map(|(id, lo, hi)| (id, lo..hi))
.collect()
}
pub(crate) fn marked_range(&self) -> Option<(BlockId, Range<usize>)> {
self.marked.as_ref().map(|m| (m.block, m.range.clone()))
}
pub(crate) fn show_caret(&self, window: &Window, cx: &App) -> bool {
self.focus_handle.is_focused(window) && self.blink_cursor.read(cx).visible()
}
fn after_edit(&mut self, origin: EditOrigin, cx: &mut Context<Self>) {
self.goal_x = None;
self.reveal.set(true);
self.pause_blink(cx);
cx.emit(BlockEditorEvent::Changed {
revision: self.model.revision(),
origin,
});
cx.emit(BlockEditorEvent::SelectionChanged);
cx.notify();
}
fn selection_changed(&mut self, cx: &mut Context<Self>) {
self.reveal.set(true);
self.pause_blink(cx);
cx.emit(BlockEditorEvent::SelectionChanged);
cx.notify();
}
fn pause_blink(&mut self, cx: &mut Context<Self>) {
self.blink_cursor.update(cx, |b, cx| b.pause(cx));
}
fn edit(
&mut self,
origin: EditOrigin,
f: impl FnOnce(&mut BlockEditorModel) -> Result<(), CommandError>,
cx: &mut Context<Self>,
) {
if self.readonly {
return;
}
let revision = self.model.revision();
let _ = f(&mut self.model);
if self.model.revision() != revision {
self.after_edit(origin, cx);
} else {
cx.notify();
}
}
fn end_composition(&mut self) {
self.marked = None;
if std::mem::take(&mut self.composition_open) {
self.model.history.commit_transaction();
}
}
#[cfg(test)]
pub(crate) fn composition_open(&self) -> bool {
self.composition_open
}
fn collapse_blocks(&mut self, forward: bool, cx: &mut Context<Self>) -> bool {
if !matches!(
self.model.selection(),
BlockSelection::Blocks { .. } | BlockSelection::Cells { .. }
) {
return false;
}
if let Ok(range) = self.model.doc.resolve_selection(self.model.selection()) {
let target = if forward { range.end } else { range.start };
self.move_to(target, false, cx);
}
true
}
fn head_anchor(&self) -> Option<(BlockPosition, BlockPosition)> {
match self.model.selection() {
BlockSelection::Text { anchor, head } => Some((*anchor, *head)),
BlockSelection::Blocks { .. } => None,
BlockSelection::Cells { anchor, head } => {
Some((BlockPosition::new(*anchor, 0), BlockPosition::new(*head, 0)))
}
}
}
fn move_to(&mut self, head: BlockPosition, extend: bool, cx: &mut Context<Self>) {
let anchor = match (extend, self.head_anchor()) {
(true, Some((anchor, _))) => anchor,
_ => head,
};
self.end_composition();
let doc = &self.model.doc;
let selection = match (
super::table::table_of(doc, anchor.block),
super::table::table_of(doc, head.block),
) {
(Some(a), Some(b)) if a == b && anchor.block != head.block => BlockSelection::Cells {
anchor: anchor.block,
head: head.block,
},
_ => BlockSelection::Text { anchor, head },
};
let _ = self.model.set_selection(selection);
self.selection_changed(cx);
}
pub(crate) fn step_horizontal(&self, pos: BlockPosition, forward: bool) -> BlockPosition {
let doc = &self.model.doc;
let len = doc.leaf_len(pos.block).unwrap_or(0);
if let Some(code) = doc.plain(pos.block) {
let next = if forward {
code[pos.offset..]
.chars()
.next()
.map(|c| pos.offset + c.len_utf8())
} else {
code[..pos.offset]
.chars()
.next_back()
.map(|c| pos.offset - c.len_utf8())
};
if let Some(offset) = next {
return BlockPosition::new(pos.block, offset);
}
} else if let Some(inline) = doc.inline(pos.block) {
let next = if forward {
inline.next_grapheme(pos.offset)
} else {
inline.prev_grapheme(pos.offset)
};
if let Some(offset) = next.filter(|o| *o != pos.offset) {
return BlockPosition::new(pos.block, offset);
}
} else if forward && pos.offset == 0 {
return BlockPosition::new(pos.block, 1);
} else if !forward && pos.offset == 1 {
return BlockPosition::new(pos.block, 0);
}
let leaves = doc.leaves();
let Some(ix) = leaves.iter().position(|id| *id == pos.block) else {
return pos;
};
let neighbour = if forward {
leaves.get(ix + 1)
} else {
ix.checked_sub(1).and_then(|i| leaves.get(i))
};
match neighbour {
Some(id) if forward => BlockPosition::new(*id, 0),
Some(id) => BlockPosition::new(*id, doc.leaf_len(*id).unwrap_or(0)),
None => BlockPosition::new(pos.block, if forward { len } else { 0 }),
}
}
fn step_word(&self, pos: BlockPosition, forward: bool) -> BlockPosition {
let Some(text) = self.model.doc.leaf_text(pos.block) else {
return self.step_horizontal(pos, forward);
};
let is_word = |c: char| c.is_alphanumeric() || c == '_';
let mut offset = pos.offset;
if forward {
let rest = &text[offset..];
let mut it = rest.char_indices().peekable();
while let Some((_, c)) = it.peek().copied()
&& !is_word(c)
{
it.next();
}
while let Some((_, c)) = it.peek().copied()
&& is_word(c)
{
it.next();
}
offset += it.peek().map(|(i, _)| *i).unwrap_or(rest.len());
} else {
let before = &text[..offset];
let mut it = before.char_indices().rev().peekable();
while let Some((_, c)) = it.peek().copied()
&& !is_word(c)
{
it.next();
}
let mut start = it.peek().map(|(i, c)| i + c.len_utf8()).unwrap_or(0);
while let Some((i, c)) = it.peek().copied()
&& is_word(c)
{
start = i;
it.next();
}
offset = if start < offset { start } else { 0 };
}
if offset == pos.offset {
return self.step_horizontal(pos, forward);
}
self.model.doc.clamp(BlockPosition::new(pos.block, offset))
}
fn word(&mut self, forward: bool, extend: bool, cx: &mut Context<Self>) {
if self.collapse_blocks(forward, cx) {
return;
}
let Some((_, head)) = self.head_anchor() else {
return;
};
self.goal_x = None;
let next = self.step_word(head, forward);
self.move_to(next, extend, cx);
}
pub(crate) fn on_move_word_left(
&mut self,
_: &MoveWordLeft,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.word(false, false, cx);
}
pub(crate) fn on_move_word_right(
&mut self,
_: &MoveWordRight,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.word(true, false, cx);
}
pub(crate) fn on_select_word_left(
&mut self,
_: &SelectWordLeft,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.word(false, true, cx);
}
pub(crate) fn on_select_word_right(
&mut self,
_: &SelectWordRight,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.word(true, true, cx);
}
fn horizontal(&mut self, forward: bool, extend: bool, cx: &mut Context<Self>) {
if self.collapse_blocks(forward, cx) {
return;
}
let Some((anchor, head)) = self.head_anchor() else {
return;
};
self.goal_x = None;
if !extend && anchor != head {
let ord = self.model.doc.cmp_positions(anchor, head).ok();
let pick_head = matches!(
(forward, ord),
(true, Some(std::cmp::Ordering::Less)) | (false, Some(std::cmp::Ordering::Greater))
);
let target = if pick_head { head } else { anchor };
return self.move_to(target, false, cx);
}
let next = self.step_horizontal(head, forward);
self.move_to(next, extend, cx);
}
fn vertical(&mut self, down: bool, extend: bool, cx: &mut Context<Self>) {
if self.collapse_blocks(down, cx) {
return;
}
let Some((_, head)) = self.head_anchor() else {
return;
};
let leaves = self.snapshot().leaves.clone();
let Some(ix) = leaves.iter().position(|l| l.block == head.block) else {
return;
};
let adjacent = if down {
leaves.get(ix + 1)
} else {
ix.checked_sub(1).and_then(|i| leaves.get(i))
};
let frame = self.frame.clone();
let frame = frame.borrow();
let laid_out = frame.caret_bounds(head, self.style.caret_width());
let x = laid_out.map(|caret| *self.goal_x.get_or_insert(caret.origin.x));
let visual = x.and_then(|x| frame.vertical(head, x, down));
let target = match (visual, adjacent) {
(Some(t), _) if t.block == head.block => t,
_ if down && !extend && self.model.in_last_table_row() => {
drop(frame);
self.edit(
EditOrigin::Command("table_exit"),
|m| m.exit_table().map(|_| ()),
cx,
);
return;
}
(Some(t), Some(next)) if t.block == next.block => t,
(Some(t), _)
if super::table::table_of(&self.model.doc, head.block).is_some_and(|table| {
super::table::table_of(&self.model.doc, t.block) == Some(table)
}) =>
{
t
}
(_, Some(next)) => {
let offset = if down { 0 } else { next.len };
BlockPosition::new(next.block, offset)
}
(_, None) if down && self.model.doc.plain(head.block).is_some() && !extend => {
drop(frame);
self.edit(
EditOrigin::Command("exit_code_block"),
|m| m.exit_code_block().map(|_| ()),
cx,
);
return;
}
(_, None) => {
let doc = &self.model.doc;
if down {
doc.last_caret()
} else {
doc.first_caret()
}
}
};
drop(frame);
let goal = self.goal_x;
self.move_to(self.model.doc.clamp(target), extend, cx);
self.goal_x = goal;
}
fn line_edge(&mut self, end: bool, extend: bool, cx: &mut Context<Self>) {
if self.collapse_blocks(end, cx) {
return;
}
let Some((_, head)) = self.head_anchor() else {
return;
};
let target = self.visual_line_edge(head, end);
self.goal_x = None;
self.move_to(target, extend, cx);
}
fn visual_line_edge(&self, head: BlockPosition, end: bool) -> BlockPosition {
let visual = self.frame.borrow().line_edge(head, end);
let visual = visual.map(|pos| {
if !end || pos.block != head.block {
return pos;
}
let frame = self.frame.borrow();
let (Some(leaf), Some(inline)) =
(frame.leaf(pos.block), self.model.doc.inline(pos.block))
else {
return pos;
};
let line_y = leaf.caret_point(head.offset).y;
if pos.offset < inline.len() && leaf.caret_point(pos.offset).y > line_y {
let prev = inline.prev_grapheme(pos.offset).unwrap_or(pos.offset);
BlockPosition::new(pos.block, prev)
} else {
pos
}
});
let target = visual.unwrap_or_else(|| {
let offset = if end {
self.model.doc.leaf_len(head.block).unwrap_or(0)
} else {
0
};
BlockPosition::new(head.block, offset)
});
self.model.doc.clamp(target)
}
fn delete_line_target(&self, head: BlockPosition, end: bool) -> BlockPosition {
if let Some(code) = self.model.doc.plain(head.block) {
let offset = if end {
code[head.offset..]
.find('\n')
.map_or(code.len(), |i| head.offset + i)
} else {
code[..head.offset].rfind('\n').map_or(0, |i| i + 1)
};
return BlockPosition::new(head.block, offset);
}
let target = self.visual_line_edge(head, end);
if target.block == head.block {
target
} else {
head
}
}
fn delete_word_target(&self, head: BlockPosition, forward: bool) -> BlockPosition {
use super::inline::ATOM_CHAR;
let Some(text) = self.model.doc.leaf_text(head.block) else {
return head;
};
let adjacent = if forward {
text[head.offset..].chars().next()
} else {
text[..head.offset].chars().next_back()
};
let atom = ATOM_CHAR.len_utf8();
match adjacent {
None => return head,
Some(ATOM_CHAR) if forward => {
return BlockPosition::new(head.block, head.offset + atom);
}
Some(ATOM_CHAR) => return BlockPosition::new(head.block, head.offset - atom),
Some(_) => {}
}
let step = self.step_word(head, forward);
if step.block != head.block {
return head;
}
let offset = if forward {
text[head.offset..step.offset]
.find(ATOM_CHAR)
.map_or(step.offset, |i| head.offset + i)
} else {
text[step.offset..head.offset]
.rfind(ATOM_CHAR)
.map_or(step.offset, |i| step.offset + i + atom)
};
self.model.doc.clamp(BlockPosition::new(head.block, offset))
}
fn delete_by(&mut self, line: bool, forward: bool, cx: &mut Context<Self>) {
self.end_composition();
if self.readonly {
return;
}
let origin = if forward {
EditOrigin::DeleteForward
} else {
EditOrigin::Backspace
};
let head = match self.model.selection() {
BlockSelection::Text { anchor, head } if anchor == head => *head,
_ => return self.edit(origin, |m| m.delete(forward), cx),
};
let target = if line {
self.delete_line_target(head, forward)
} else {
self.delete_word_target(head, forward)
};
if line {
self.model.break_coalescing();
}
if target == head {
if super::table::table_of(&self.model.doc, head.block).is_none() {
self.edit(origin, |m| m.delete(forward), cx);
}
} else {
let origin = if forward {
EditOrigin::WordDelete
} else {
EditOrigin::WordBackspace
};
self.edit(origin, |m| m.delete_span(target, head, forward), cx);
}
if line {
self.model.break_coalescing();
}
}
fn paragraph_edge(&mut self, forward: bool, extend: bool, cx: &mut Context<Self>) {
if self.collapse_blocks(forward, cx) {
return;
}
let Some((_, head)) = self.head_anchor() else {
return;
};
let doc = &self.model.doc;
let len = doc.leaf_len(head.block).unwrap_or(0);
let mut pos = head;
if (forward && pos.offset >= len) || (!forward && pos.offset == 0) {
pos = self.step_horizontal(pos, forward);
}
let offset = if forward {
doc.leaf_len(pos.block).unwrap_or(0)
} else {
0
};
let target = doc.clamp(BlockPosition::new(pos.block, offset));
self.goal_x = None;
self.move_to(target, extend, cx);
}
pub(crate) fn on_delete_word_left(
&mut self,
_: &DeleteWordLeft,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.delete_by(false, false, cx);
}
pub(crate) fn on_delete_word_right(
&mut self,
_: &DeleteWordRight,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.delete_by(false, true, cx);
}
pub(crate) fn on_delete_to_line_start(
&mut self,
_: &DeleteToLineStart,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.delete_by(true, false, cx);
}
pub(crate) fn on_delete_to_line_end(
&mut self,
_: &DeleteToLineEnd,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.delete_by(true, true, cx);
}
pub(crate) fn on_move_paragraph_start(
&mut self,
_: &MoveParagraphStart,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.paragraph_edge(false, false, cx);
}
pub(crate) fn on_move_paragraph_end(
&mut self,
_: &MoveParagraphEnd,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.paragraph_edge(true, false, cx);
}
pub(crate) fn on_select_paragraph_start(
&mut self,
_: &SelectParagraphStart,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.paragraph_edge(false, true, cx);
}
pub(crate) fn on_select_paragraph_end(
&mut self,
_: &SelectParagraphEnd,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.paragraph_edge(true, true, cx);
}
pub fn link_at(&self, at: Point<Pixels>) -> Option<(BlockId, Range<usize>, SharedString)> {
let pos = {
let frame = self.frame.borrow();
let pos = frame.hit_test(at)?;
if !frame.leaf(pos.block)?.bounds().contains(&at) {
return None;
}
pos
};
let inline = self.model.doc.inline(pos.block)?;
let url = inline.mark_after(pos.offset)?.link.as_ref()?.url.clone();
let range = link_extent(inline, pos.offset, &url)?;
Some((pos.block, range, url))
}
pub fn hovered_link(&self) -> Option<&SharedString> {
self.hovered_link.as_ref()
}
pub(crate) fn hover_at(&mut self, at: Point<Pixels>, secondary: bool, cx: &mut Context<Self>) {
self.hover_table(at, cx);
let url = self.link_at(at).map(|(_, _, url)| url);
let hand = self.task_checkbox_at(at).is_some() || (secondary && url.is_some());
if hand != self.pointer_hand {
self.pointer_hand = hand;
cx.notify();
}
if url != self.hovered_link {
self.hovered_link = url.clone();
cx.emit(BlockEditorEvent::LinkHovered { url });
cx.notify();
}
}
pub(crate) fn follow_link_at(&mut self, at: Point<Pixels>, cx: &mut Context<Self>) -> bool {
match self.link_at(at) {
Some((_, _, url)) => {
cx.emit(BlockEditorEvent::LinkClicked { url });
true
}
None => false,
}
}
pub fn link_at_caret(&self) -> Option<SharedString> {
self.link_range_at_caret().map(|(_, _, url)| url)
}
fn link_range_at_caret(&self) -> Option<(BlockId, Range<usize>, SharedString)> {
let head = match &self.model.selection {
BlockSelection::Text { anchor, head } => {
if anchor.block == head.block && anchor.offset < head.offset {
*anchor
} else {
*head
}
}
_ => return None,
};
let inline = self.model.doc.inline(head.block)?;
let mark = inline
.mark_after(head.offset)
.filter(|m| m.link.is_some())
.or_else(|| inline.mark_before(head.offset))?;
let url = mark.link.as_ref()?.url.clone();
let probe = if inline
.mark_after(head.offset)
.is_some_and(|m| m.link.is_some())
{
head.offset
} else {
head.offset.checked_sub(1)?
};
let range = link_extent(inline, probe, &url)?;
Some((head.block, range, url))
}
pub fn request_link_edit(&mut self, cx: &mut Context<Self>) {
if self.readonly {
return;
}
let url = self.link_at_caret();
cx.emit(BlockEditorEvent::EditLinkRequested { url });
}
pub(crate) fn on_edit_link(&mut self, _: &EditLink, _: &mut Window, cx: &mut Context<Self>) {
self.request_link_edit(cx);
}
pub fn set_link(&mut self, url: Option<&str>, cx: &mut Context<Self>) {
if self.readonly {
return;
}
let collapsed = matches!(
&self.model.selection,
BlockSelection::Text { anchor, head } if anchor == head
);
if collapsed && let Some((block, range, _)) = self.link_range_at_caret() {
let _ = self.model.set_selection(BlockSelection::Text {
anchor: BlockPosition::new(block, range.start),
head: BlockPosition::new(block, range.end),
});
} else if collapsed {
return;
}
let change = match url.map(str::trim).filter(|u| !u.is_empty()) {
Some(url) => super::marks::MarkChange::new().with_link(url.to_string(), None),
None => super::marks::MarkChange::new().clear(MarkKind::Link),
};
self.model.break_coalescing();
let _ = self.run(EditorCommand::ApplyMark(change), cx);
self.model.break_coalescing();
}
pub(crate) fn click_checkbox(&mut self, at: Point<Pixels>, cx: &mut Context<Self>) -> bool {
match self.task_checkbox_at(at) {
Some((leaf, checked)) if !self.readonly => {
self.end_composition();
self.dragging = false;
self.set_task_checked(leaf, !checked, cx);
true
}
_ => false,
}
}
pub(crate) fn task_checkbox_at(&self, at: Point<Pixels>) -> Option<(BlockId, bool)> {
use super::kind::BlockKindData;
use gpui::px;
let frame = self.frame.borrow();
frame.leaves.iter().find_map(|leaf| {
let b = leaf.bounds();
let line = px(28.).min(b.size.height);
if !(at.y >= b.top()
&& at.y < b.top() + line
&& at.x < b.left()
&& at.x >= b.left() - px(28.))
{
return None;
}
let block = leaf.info.block;
let (item, index) = self.model.doc.index_in_parent(block)?;
match self.model.doc.kind(item) {
Some(BlockKindData::ListItem {
checked: Some(c), ..
}) if index == 0 => Some((block, *c)),
_ => None,
}
})
}
pub(crate) fn mouse_at(&mut self, at: Point<Pixels>, extend: bool, cx: &mut Context<Self>) {
if !extend && let Some(block) = self.block_handle_at(at) {
self.goal_x = None;
self.end_composition();
let _ = self.model.set_selection(BlockSelection::Blocks {
anchor: block,
head: block,
});
self.dragging = false;
self.selection_changed(cx);
return;
}
let hit = self.frame.borrow().hit_test(at);
if !extend && let Some(pos) = hit.and_then(|pos| self.atom_under(pos, at)) {
self.goal_x = None;
self.end_composition();
let _ = self.model.set_selection(BlockSelection::Text {
anchor: pos,
head: BlockPosition::new(
pos.block,
pos.offset + super::inline::ATOM_CHAR.len_utf8(),
),
});
self.selection_changed(cx);
return;
}
if let Some(pos) = hit {
self.goal_x = None;
let pos = self.model.doc.clamp(pos);
self.move_to(pos, extend, cx);
}
}
pub(crate) fn select_unit(&mut self, block: bool, cx: &mut Context<Self>) {
let Some(caret) = self.model.caret() else {
return;
};
let Some(text) = self.model.doc.leaf_text(caret.block) else {
return;
};
let range = if block {
0..text.len()
} else {
match crate::support::word_range_at(text, caret.offset) {
Some(r) => r,
None => return,
}
};
let _ = self.model.set_selection(BlockSelection::Text {
anchor: BlockPosition::new(caret.block, range.start),
head: BlockPosition::new(caret.block, range.end),
});
self.dragging = false;
self.selection_changed(cx);
}
pub(crate) fn top_level_bounds(&self) -> Vec<(usize, BlockId, Bounds<Pixels>)> {
let doc = &self.model.doc;
let frame = self.frame.borrow();
let children = doc.children(doc.root);
let mut out: Vec<(usize, BlockId, Bounds<Pixels>)> = Vec::new();
for leaf in &frame.leaves {
let path = doc.path(leaf.info.block);
let Some(top) = path.get(1).copied() else {
continue;
};
let Some(ix) = children.iter().position(|c| *c == top) else {
continue;
};
let b = leaf.bounds();
match out.iter_mut().find(|(_, id, _)| *id == top) {
Some(entry) => entry.2 = entry.2.union(&b),
None => out.push((ix, top, b)),
}
}
out.sort_by_key(|(ix, _, _)| *ix);
out
}
pub(crate) fn drag_handle_at(&self, at: Point<Pixels>) -> Option<BlockId> {
if self.readonly {
return None;
}
let block = self.block_handle_at(at)?;
let doc = &self.model.doc;
let top = doc.path(block).get(1).copied()?;
(top == block).then_some(block)
}
fn drop_gap_at(&self, y: Pixels) -> Option<usize> {
let blocks = self.top_level_bounds();
let len = self.model.doc.children(self.model.doc.root).len();
if blocks.is_empty() {
return None;
}
Some(
blocks
.iter()
.find(|(_, _, b)| y < b.center().y)
.map_or_else(
|| blocks.last().map_or(len, |(ix, _, _)| ix + 1),
|(ix, _, _)| *ix,
),
)
}
pub(crate) fn arm_block_drag(&mut self, at: Point<Pixels>) {
self.block_drag = self.drag_handle_at(at).map(|block| BlockDrag {
block,
start: at,
active: false,
drop: None,
});
}
pub(crate) fn block_drag_to(&mut self, at: Point<Pixels>, cx: &mut Context<Self>) -> bool {
use gpui::px;
let Some(mut drag) = self.block_drag else {
return false;
};
if !drag.active {
let d = at - drag.start;
if d.x.abs().max(d.y.abs()) < px(4.) {
return false;
}
drag.active = true;
self.dragging = false;
self.end_composition();
let _ = self.model.set_selection(BlockSelection::Blocks {
anchor: drag.block,
head: drag.block,
});
self.selection_changed(cx);
}
drag.drop = self.drop_gap_at(at.y);
self.block_drag = Some(drag);
cx.notify();
true
}
pub(crate) fn finish_block_drag(&mut self, cx: &mut Context<Self>) {
let Some(drag) = self.block_drag.take() else {
return;
};
if drag.active {
if let Some(to) = drag.drop {
self.model.break_coalescing();
self.edit(
EditOrigin::Command("move_block_to"),
|m| m.move_block_to(drag.block, to).map(|_| ()),
cx,
);
self.model.break_coalescing();
}
cx.notify();
}
}
pub(crate) fn drop_indicator(&self) -> Option<Bounds<Pixels>> {
use gpui::{point, px, size};
let drag = self.block_drag.filter(|d| d.active)?;
let to = drag.drop?;
let blocks = self.top_level_bounds();
let left = blocks
.iter()
.map(|(_, _, b)| b.left())
.reduce(Pixels::min)?;
let right = blocks
.iter()
.map(|(_, _, b)| b.right())
.reduce(Pixels::max)?;
let prev = blocks.iter().rev().find(|(ix, _, _)| *ix < to);
let next = blocks.iter().find(|(ix, _, _)| *ix >= to);
let y = match (prev, next) {
(Some((_, _, a)), Some((_, _, b))) => (a.bottom() + b.top()) / 2.,
(None, Some((_, _, b))) => b.top() - px(3.),
(Some((_, _, a)), None) => a.bottom() + px(3.),
(None, None) => return None,
};
Some(gpui::Bounds::new(
point(left - px(8.), y - px(1.)),
size(right - left + px(8.), px(2.)),
))
}
pub(crate) fn block_handle_at(&self, at: Point<Pixels>) -> Option<BlockId> {
let frame = self.frame.borrow();
use gpui::px;
let in_edge = |b: &Bounds<Pixels>| {
at.y >= b.top() && at.y < b.bottom() && at.x < b.left() && at.x >= b.left() - px(24.)
};
if let Some((table, _)) = frame.tables.iter().find(|(_, b)| in_edge(b)) {
return Some(*table);
}
frame
.leaves
.iter()
.find(|leaf| self.model.doc.plain(leaf.info.block).is_some() && in_edge(&leaf.bounds()))
.map(|leaf| leaf.info.block)
}
fn atom_under(&self, pos: BlockPosition, at: Point<Pixels>) -> Option<BlockPosition> {
let frame = self.frame.borrow();
let range = frame.leaf(pos.block)?.atom_at(at)?;
Some(BlockPosition::new(pos.block, range.start))
}
fn toggle_mark(&mut self, kind: MarkKind, cx: &mut Context<Self>) {
let _ = self.run(EditorCommand::ToggleMark(kind), cx);
}
fn selected_text(&self) -> (String, CopyKind) {
if let Some(table) = self.model.cells_markdown() {
return (table, CopyKind::Blocks);
}
if let Some((parent, range)) = self.model.selected_blocks() {
let ids = &self.model.doc.children(parent)[range];
return (
super::serialize::join_blocks(&self.model.doc, ids),
CopyKind::Blocks,
);
}
let Ok(range) = self.model.doc.resolve_selection(self.model.selection()) else {
return (String::new(), CopyKind::External);
};
if range.start.block != range.end.block
&& let Some(markdown) = super::slice::selection_markdown(&self.model.doc, range)
{
return (markdown, CopyKind::Blocks);
}
let mut inline_only = true;
let text = self
.model
.text_ranges(range)
.into_iter()
.filter_map(|(id, lo, hi)| {
if let Some(inline) = self.model.doc.inline(id) {
return Some(super::serialize::inline_markdown(&inline.slice(lo..hi)));
}
inline_only = false;
let text = self.model.doc.leaf_text(id)?;
text.get(lo..hi).map(str::to_string)
})
.collect::<Vec<_>>()
.join("\n");
(
text,
if inline_only {
CopyKind::Inline
} else {
CopyKind::External
},
)
}
pub(crate) fn on_backspace(&mut self, _: &Backspace, _: &mut Window, cx: &mut Context<Self>) {
self.end_composition();
self.edit(EditOrigin::Backspace, |m| m.delete(false), cx);
}
pub(crate) fn on_delete(&mut self, _: &Delete, _: &mut Window, cx: &mut Context<Self>) {
self.end_composition();
self.edit(EditOrigin::DeleteForward, |m| m.delete(true), cx);
}
pub(crate) fn on_enter(&mut self, _: &Enter, _: &mut Window, cx: &mut Context<Self>) {
if self.readonly || self.is_composing() {
return;
}
if self.submit_policy == SubmitPolicy::Enter {
cx.emit(BlockEditorEvent::SubmitRequested);
return;
}
self.newline(cx);
}
pub(crate) fn on_structural_enter(
&mut self,
_: &StructuralEnter,
_: &mut Window,
cx: &mut Context<Self>,
) {
if self.readonly || self.is_composing() {
return;
}
self.newline(cx);
}
fn newline(&mut self, cx: &mut Context<Self>) {
self.end_composition();
if !self.readonly
&& self.rule_on(RuleKind::CodeFence)
&& let Some(caret) = self.model.caret()
&& self.model.doc.kind(caret.block) == Some(&super::kind::BlockKindData::Paragraph)
&& caret.offset == self.model.doc.leaf_len(caret.block).unwrap_or(0)
&& let Some(lang) = self
.model
.doc
.inline(caret.block)
.and_then(|i| code_fence(i.text()))
{
self.fence_to_code_block(caret.block, lang, cx);
return;
}
if !self.readonly && self.model.current_cell().is_some() {
self.model.break_coalescing();
self.edit(
EditOrigin::Command("table_enter"),
|m| m.table_enter().map(|_| ()),
cx,
);
self.model.break_coalescing();
return;
}
if !self.readonly
&& self.rule_on(RuleKind::Table)
&& let Some(caret) = self.model.caret()
&& caret.offset == self.model.doc.leaf_len(caret.block).unwrap_or(0)
&& self
.model
.doc
.inline(caret.block)
.is_some_and(|i| super::table::table_row_cells(i.text()).is_some())
{
let block = caret.block;
self.model.break_coalescing();
self.edit(
EditOrigin::InputRule("table"),
|m| m.paragraph_to_table(block).map(|_| ()),
cx,
);
self.model.break_coalescing();
return;
}
if !self.readonly
&& self.rule_on(RuleKind::HorizontalRule)
&& let Some(caret) = self.model.caret()
&& self.model.doc.kind(caret.block) == Some(&super::kind::BlockKindData::Paragraph)
&& self.model.doc.inline(caret.block).map(|i| i.text()) == Some("---")
{
let block = caret.block;
self.model.break_coalescing();
self.model.history.begin_transaction();
let _ = self.model.set_selection(BlockSelection::Text {
anchor: BlockPosition::new(block, 0),
head: BlockPosition::new(block, 3),
});
let _ = self.model.delete(false);
let _ = self.model.paragraph_to_rule();
self.model.history.commit_transaction();
self.model.break_coalescing();
self.after_edit(EditOrigin::InputRule("horizontal_rule"), cx);
return;
}
self.edit(EditOrigin::Command("split_block"), |m| m.split_block(), cx);
}
pub(crate) fn on_soft_break(&mut self, _: &SoftBreak, _: &mut Window, cx: &mut Context<Self>) {
if self.readonly || self.is_composing() {
return;
}
self.end_composition();
if self.model.current_cell().is_some() {
self.model.break_coalescing();
self.edit(
EditOrigin::Command("cell_soft_break"),
|m| m.cell_soft_break().map(|_| ()),
cx,
);
self.model.break_coalescing();
} else {
self.newline(cx);
}
}
pub(crate) fn on_exit_block(&mut self, _: &ExitBlock, _: &mut Window, cx: &mut Context<Self>) {
if self.readonly || self.is_composing() {
return;
}
if matches!(
self.submit_policy,
SubmitPolicy::Enter | SubmitPolicy::ModifiedEnter
) {
cx.emit(BlockEditorEvent::SubmitRequested);
return;
}
self.end_composition();
if self.readonly || self.model.current_cell().is_none() {
cx.propagate();
return;
}
self.model.break_coalescing();
self.edit(
EditOrigin::Command("table_exit"),
|m| m.exit_table().map(|_| ()),
cx,
);
self.model.break_coalescing();
}
pub(crate) fn on_move_left(&mut self, _: &MoveLeft, _: &mut Window, cx: &mut Context<Self>) {
self.horizontal(false, false, cx);
}
pub(crate) fn on_move_right(&mut self, _: &MoveRight, _: &mut Window, cx: &mut Context<Self>) {
self.horizontal(true, false, cx);
}
pub(crate) fn on_move_up(&mut self, _: &MoveUp, _: &mut Window, cx: &mut Context<Self>) {
self.vertical(false, false, cx);
}
pub(crate) fn on_move_down(&mut self, _: &MoveDown, _: &mut Window, cx: &mut Context<Self>) {
self.vertical(true, false, cx);
}
pub(crate) fn on_move_home(&mut self, _: &MoveHome, _: &mut Window, cx: &mut Context<Self>) {
self.line_edge(false, false, cx);
}
pub(crate) fn on_move_end(&mut self, _: &MoveEnd, _: &mut Window, cx: &mut Context<Self>) {
self.line_edge(true, false, cx);
}
pub(crate) fn on_select_left(
&mut self,
_: &SelectLeft,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.horizontal(false, true, cx);
}
pub(crate) fn on_select_right(
&mut self,
_: &SelectRight,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.horizontal(true, true, cx);
}
pub(crate) fn on_select_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
self.vertical(false, true, cx);
}
pub(crate) fn on_select_down(
&mut self,
_: &SelectDown,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.vertical(true, true, cx);
}
pub(crate) fn on_select_home(
&mut self,
_: &SelectHome,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.line_edge(false, true, cx);
}
pub(crate) fn on_select_end(&mut self, _: &SelectEnd, _: &mut Window, cx: &mut Context<Self>) {
self.line_edge(true, true, cx);
}
pub(crate) fn on_move_doc_start(
&mut self,
_: &MoveDocStart,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.goal_x = None;
self.move_to(self.model.doc.first_caret(), false, cx);
}
pub(crate) fn on_move_doc_end(
&mut self,
_: &MoveDocEnd,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.goal_x = None;
self.move_to(self.model.doc.last_caret(), false, cx);
}
pub(crate) fn on_select_doc_start(
&mut self,
_: &SelectDocStart,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.goal_x = None;
self.move_to(self.model.doc.first_caret(), true, cx);
}
pub(crate) fn on_select_doc_end(
&mut self,
_: &SelectDocEnd,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.goal_x = None;
self.move_to(self.model.doc.last_caret(), true, cx);
}
pub(crate) fn on_select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
self.end_composition();
let _ = self.model.run(EditorCommand::SelectAll);
self.selection_changed(cx);
}
pub(crate) fn on_undo(&mut self, _: &Undo, _: &mut Window, cx: &mut Context<Self>) {
self.end_composition();
self.undo(cx);
}
pub(crate) fn on_redo(&mut self, _: &Redo, _: &mut Window, cx: &mut Context<Self>) {
self.end_composition();
self.redo(cx);
}
pub(crate) fn on_normal_text(
&mut self,
_: &NormalText,
_: &mut Window,
cx: &mut Context<Self>,
) {
if self.readonly {
return;
}
self.end_composition();
self.model.break_coalescing();
let _ = self.run(EditorCommand::SetBlockKind(BlockKind::Paragraph), cx);
self.model.break_coalescing();
}
pub(crate) fn on_close_table_menu(
&mut self,
_: &CloseTableMenu,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.close_table_menu(cx);
}
fn table_key(&mut self, edit: Option<crate::TableEdit>, cx: &mut Context<Self>) {
if self.readonly || self.model.current_cell().is_none() {
cx.propagate();
return;
}
self.end_composition();
self.close_table_menu(cx);
if let Some(edit) = edit {
self.model.break_coalescing();
let _ = self.run(EditorCommand::EditTable(edit), cx);
self.model.break_coalescing();
}
}
pub(crate) fn on_table_insert_column_left(
&mut self,
_: &TableInsertColumnLeft,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.table_key(Some(crate::TableEdit::AddColumnLeft), cx);
}
pub(crate) fn on_table_insert_column_right(
&mut self,
_: &TableInsertColumnRight,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.table_key(Some(crate::TableEdit::AddColumnRight), cx);
}
pub(crate) fn on_table_insert_row_above(
&mut self,
_: &TableInsertRowAbove,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.table_key(Some(crate::TableEdit::AddRowAbove), cx);
}
pub(crate) fn on_table_insert_row_below(
&mut self,
_: &TableInsertRowBelow,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.table_key(Some(crate::TableEdit::AddRowBelow), cx);
}
pub(crate) fn on_table_delete_line(
&mut self,
_: &TableDeleteLine,
_: &mut Window,
cx: &mut Context<Self>,
) {
let edit = self.table_delete_edit();
self.table_key(edit, cx);
}
pub fn table_delete_edit(&self) -> Option<crate::TableEdit> {
if self.model.whole_columns_selected() {
return Some(crate::TableEdit::RemoveColumn);
}
let (_, rows, _) = self.model.table_span()?;
(*rows.start() > 0).then_some(crate::TableEdit::RemoveRow)
}
pub(crate) fn on_toggle_bold(
&mut self,
_: &ToggleBold,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.toggle_mark(MarkKind::Bold, cx);
}
pub(crate) fn on_toggle_italic(
&mut self,
_: &ToggleItalic,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.toggle_mark(MarkKind::Italic, cx);
}
pub(crate) fn on_toggle_code(
&mut self,
_: &ToggleCode,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.toggle_mark(MarkKind::Code, cx);
}
pub(crate) fn on_indent(&mut self, _: &Indent, _: &mut Window, cx: &mut Context<Self>) {
let _ = self.run(EditorCommand::Indent, cx);
}
pub(crate) fn on_outdent(&mut self, _: &Outdent, _: &mut Window, cx: &mut Context<Self>) {
let _ = self.run(EditorCommand::Outdent, cx);
}
pub(crate) fn on_copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
let (text, kind) = self.selected_text();
if text.is_empty() {
return;
}
cx.write_to_clipboard(match kind.tag() {
Some(tag) => ClipboardItem::new_string_with_metadata(text, tag.to_string()),
None => ClipboardItem::new_string(text),
});
}
pub(crate) fn on_cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
self.end_composition();
self.on_copy(&Copy, window, cx);
self.edit(
EditOrigin::Cut,
|m| m.run(EditorCommand::DeleteSelection).map(|_| ()),
cx,
);
}
pub(crate) fn on_paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {
if self.readonly {
return;
}
if let Some(item) = cx.read_from_clipboard() {
self.paste(item, false, cx);
}
}
pub(crate) fn on_native_paste(
&mut self,
_: &gpui_kit::base::input::Paste,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.on_paste(&Paste, window, cx);
}
pub(crate) fn on_paste_plain(
&mut self,
_: &PastePlain,
_: &mut Window,
cx: &mut Context<Self>,
) {
if self.readonly {
return;
}
if let Some(item) = cx.read_from_clipboard() {
self.paste(item, true, cx);
}
}
pub fn paste(&mut self, item: ClipboardItem, plain: bool, cx: &mut Context<Self>) {
if self.readonly {
return;
}
self.end_composition();
if self.clipboard_policy == ClipboardPolicy::Host {
self.paste_sequence += 1;
self.pending_paste = Some(self.paste_sequence);
let request = PasteRequest {
range: self.model.capture(self.model.selection.clone()),
plain,
copy: CopyKind::of(&item),
sequence: self.paste_sequence,
};
cx.emit(BlockEditorEvent::PasteRequested { request, item });
} else if let Some(text) = item.text() {
self.paste_text(&text, plain, CopyKind::of(&item), cx);
}
}
pub fn is_paste_current(&self, request: &PasteRequest) -> bool {
!self.readonly
&& self.pending_paste == Some(request.sequence)
&& self.model.is_capture_current(&request.range)
}
pub fn apply_paste(
&mut self,
request: &PasteRequest,
text: Option<&str>,
cx: &mut Context<Self>,
) -> bool {
if !self.is_paste_current(request) {
return false;
}
self.pending_paste = None;
if let Some(text) = text {
self.paste_text(text, request.plain, request.copy, cx);
}
true
}
fn paste_text(&mut self, text: &str, plain: bool, copy: CopyKind, cx: &mut Context<Self>) {
self.edit(
EditOrigin::Paste,
|model| {
if !plain && copy == CopyKind::Blocks && model.paste_blocks_markdown(text)? {
return Ok(());
}
if !plain && copy != CopyKind::External && model.paste_inline_markdown(text)? {
return Ok(());
}
if !plain && model.paste_blocks(text)? {
return Ok(());
}
model.insert_text(text, EditOrigin::Paste)
},
cx,
);
}
fn apply_input_rule(&mut self, cx: &mut Context<Self>) {
use super::kind::BlockKindData;
let Some(caret) = self.model.caret() else {
return;
};
let doc = &self.model.doc;
if doc.kind(caret.block) != Some(&BlockKindData::Paragraph) {
return;
}
if self.rule_on(RuleKind::CodeFence)
&& let Some(lang) = doc
.inline(caret.block)
.and_then(|i| code_fence(i.text().strip_suffix(' ')?))
.filter(|_| caret.offset == doc.leaf_len(caret.block).unwrap_or(0))
{
self.fence_to_code_block(caret.block, lang, cx);
return;
}
let in_list = doc
.parent(caret.block)
.and_then(|parent| doc.kind(parent))
.is_some_and(|kind| matches!(kind, BlockKindData::ListItem { .. }));
let Some(text) = doc.inline(caret.block).map(|i| i.text().to_string()) else {
return;
};
let prefix = &text[..caret.offset.min(text.len())];
let Some(marker) = prefix.strip_suffix(' ') else {
return;
};
let task = match marker {
"[ ]" => Some(false),
"[x]" | "[X]" => Some(true),
_ => None,
};
let (commands, rule) = if self.rule_on(RuleKind::Heading)
&& !marker.is_empty()
&& marker.len() <= 6
&& marker.bytes().all(|b| b == b'#')
{
let level = marker.len() as u8;
(
vec![EditorCommand::SetBlockKind(BlockKind::Heading { level })],
"heading",
)
} else if self.rule_on(RuleKind::BulletList)
&& !in_list
&& matches!(marker, "-" | "*" | "+")
{
(
vec![EditorCommand::ToggleList { ordered: false }],
"bullet_list",
)
} else if self.rule_on(RuleKind::OrderedList)
&& !in_list
&& marker.len() >= 2
&& marker.ends_with('.')
&& marker[..marker.len() - 1]
.bytes()
.all(|b| b.is_ascii_digit())
{
(
vec![EditorCommand::ToggleList { ordered: true }],
"ordered_list",
)
} else if self.rule_on(RuleKind::TaskItem)
&& let Some(checked) = task
{
let mut commands = Vec::new();
if !in_list {
commands.push(EditorCommand::ToggleList { ordered: false });
}
commands.push(EditorCommand::SetTaskChecked(checked));
(commands, "task_item")
} else if self.rule_on(RuleKind::Blockquote) && marker == ">" {
(Vec::new(), "blockquote")
} else {
return;
};
let block = caret.block;
let len = prefix.len();
self.model.break_coalescing();
self.model.history.begin_transaction();
let _ = self.model.set_selection(BlockSelection::Text {
anchor: BlockPosition::new(block, 0),
head: BlockPosition::new(block, len),
});
let _ = self.model.delete(false);
if commands.is_empty() {
let _ = self.model.wrap_in_blockquote();
}
for command in commands {
let _ = self.model.run(command);
}
self.model.history.commit_transaction();
self.model.break_coalescing();
self.after_edit(EditOrigin::InputRule(rule), cx);
}
fn apply_link_rule(&mut self, cx: &mut Context<Self>) {
let Some(caret) = self.model.caret() else {
return;
};
let Some(inline) = self.model.doc.inline(caret.block) else {
return;
};
let Some((start, text, url)) = link_rule(inline.text(), caret.offset) else {
return;
};
let block = caret.block;
let pos = |offset| BlockPosition::new(block, offset);
let text_end = start + 1 + text.len();
self.model.break_coalescing();
self.model.history.begin_transaction();
let _ = self.model.set_selection(BlockSelection::Text {
anchor: pos(text_end),
head: pos(caret.offset),
});
let _ = self.model.delete(false);
let _ = self.model.set_selection(BlockSelection::Text {
anchor: pos(start),
head: pos(start + 1),
});
let _ = self.model.delete(false);
let end = start + text.len();
let _ = self.model.set_selection(BlockSelection::Text {
anchor: pos(start),
head: pos(end),
});
let _ = self.model.run(EditorCommand::ApplyMark(
super::marks::MarkChange::new().with_link(url, None),
));
let _ = self.model.set_selection(BlockSelection::caret(pos(end)));
let after = super::marks::MarkChange::new()
.clear(super::marks::MarkKind::Link)
.apply(&self.model.active_marks().0);
self.model.history.commit_transaction();
self.model.break_coalescing();
self.model.stored_marks = Some(after);
self.after_edit(EditOrigin::InputRule("link"), cx);
}
fn apply_mark_rule(&mut self, cx: &mut Context<Self>) {
let Some(caret) = self.model.caret() else {
return;
};
let Some(inline) = self.model.doc.inline(caret.block) else {
return;
};
let Some((open, close, kind)) = mark_rule(inline, caret.offset) else {
return;
};
let block = caret.block;
let d = close.len();
let pos = |offset| BlockPosition::new(block, offset);
self.model.break_coalescing();
self.model.history.begin_transaction();
let _ = self.model.set_selection(BlockSelection::Text {
anchor: pos(close.start),
head: pos(close.end),
});
let _ = self.model.delete(false);
let _ = self.model.set_selection(BlockSelection::Text {
anchor: pos(open.start),
head: pos(open.end),
});
let _ = self.model.delete(false);
let end = close.start - d;
let _ = self.model.set_selection(BlockSelection::Text {
anchor: pos(open.start),
head: pos(end),
});
let _ = self.model.run(EditorCommand::ApplyMark(
super::marks::MarkChange::new().set(kind),
));
let _ = self.model.set_selection(BlockSelection::caret(pos(end)));
let after = super::marks::MarkChange::new()
.clear(kind)
.apply(&self.model.active_marks().0);
self.model.history.commit_transaction();
self.model.break_coalescing();
self.model.stored_marks = Some(after);
self.after_edit(EditOrigin::InputRule("inline_mark"), cx);
}
fn fence_to_code_block(&mut self, block: BlockId, lang: String, cx: &mut Context<Self>) {
let len = self.model.doc.leaf_len(block).unwrap_or(0);
self.model.break_coalescing();
self.model.history.begin_transaction();
let _ = self.model.set_selection(BlockSelection::Text {
anchor: BlockPosition::new(block, 0),
head: BlockPosition::new(block, len),
});
let _ = self.model.delete(false);
let (lang, meta) = lang
.split_once(char::is_whitespace)
.map_or((lang.as_str(), None), |(lang, meta)| {
(lang, Some(meta.trim_start().to_owned()))
});
let _ = self.model.set_code_block_info(Some(lang.to_owned()), meta);
self.model.history.commit_transaction();
self.model.break_coalescing();
self.after_edit(EditOrigin::InputRule("code_block"), cx);
}
fn ime_block(&self) -> Option<BlockId> {
if let Some(m) = &self.marked {
return Some(m.block);
}
let (_, head) = self.head_anchor()?;
Some(head.block)
}
fn ime_text(&self) -> &str {
self.ime_block()
.and_then(|id| self.model.doc.leaf_text(id))
.unwrap_or("")
}
fn select_utf16(&mut self, block: BlockId, range16: Range<usize>) {
let text = self.ime_text();
let range = utf16_to_byte(text, range16.start)..utf16_to_byte(text, range16.end);
let _ = self.model.set_selection(BlockSelection::Text {
anchor: BlockPosition::new(block, range.start),
head: BlockPosition::new(block, range.end),
});
}
fn select_marked_or(&mut self, range16: Option<Range<usize>>) {
let Some(block) = self.ime_block() else {
return;
};
if let Some(range16) = range16 {
self.select_utf16(block, range16);
} else if let Some(m) = self.marked.clone() {
let _ = self.model.set_selection(BlockSelection::Text {
anchor: BlockPosition::new(m.block, m.range.start),
head: BlockPosition::new(m.block, m.range.end),
});
}
}
}
fn command_name(command: &EditorCommand) -> &'static str {
match command {
EditorCommand::ToggleMark(_) => "toggle_mark",
EditorCommand::ApplyMark(_) => "apply_mark",
EditorCommand::SetBlockKind(_) => "set_block_kind",
EditorCommand::ToggleList { .. } => "toggle_list",
EditorCommand::ToggleTask => "toggle_task",
EditorCommand::SetTaskChecked(_) => "set_task_checked",
EditorCommand::Indent => "indent",
EditorCommand::Outdent => "outdent",
EditorCommand::MoveBlocks { .. } => "move_blocks",
EditorCommand::InsertBlock(_) => "insert_block",
EditorCommand::SelectAll => "select_all",
EditorCommand::DeleteSelection => "delete_selection",
EditorCommand::SetCodeBlock { .. } => "code_block",
EditorCommand::InsertTable { .. } => "insert_table",
EditorCommand::EditTable(_) => "edit_table",
}
}
pub(crate) fn utf16_to_byte(text: &str, utf16: usize) -> usize {
let mut count = 0;
for (ix, ch) in text.char_indices() {
if count >= utf16 {
return ix;
}
count += ch.len_utf16();
}
text.len()
}
pub(crate) fn byte_to_utf16(text: &str, byte: usize) -> usize {
text.get(..byte.min(text.len()))
.map(|s| s.encode_utf16().count())
.unwrap_or(0)
}
impl EntityInputHandler for BlockEditorState {
fn text_for_range(
&mut self,
range16: Range<usize>,
adjusted: &mut Option<Range<usize>>,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<String> {
let text = self.ime_text();
let range = utf16_to_byte(text, range16.start)..utf16_to_byte(text, range16.end);
adjusted.replace(byte_to_utf16(text, range.start)..byte_to_utf16(text, range.end));
text.get(range).map(str::to_string)
}
fn selected_text_range(
&mut self,
_: bool,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<UTF16Selection> {
let (anchor, head) = self.head_anchor()?;
let text = self.ime_text();
let in_leaf = |p: BlockPosition| {
if p.block == head.block {
p.offset
} else if self.model.doc.cmp_positions(p, head).ok() == Some(std::cmp::Ordering::Less) {
0
} else {
text.len()
}
};
let (a, h) = (in_leaf(anchor), head.offset);
Some(UTF16Selection {
range: byte_to_utf16(text, a.min(h))..byte_to_utf16(text, a.max(h)),
reversed: h < a,
})
}
fn marked_text_range(&self, _: &mut Window, _: &mut Context<Self>) -> Option<Range<usize>> {
let m = self.marked.as_ref()?;
let text = self.model.doc.leaf_text(m.block)?;
Some(byte_to_utf16(text, m.range.start)..byte_to_utf16(text, m.range.end))
}
fn unmark_text(&mut self, _: &mut Window, _: &mut Context<Self>) {
self.end_composition();
}
fn paste(&mut self, item: ClipboardItem, _: &mut Window, cx: &mut Context<Self>) {
BlockEditorState::paste(self, item, false, cx);
}
fn replace_text_in_range(
&mut self,
range16: Option<Range<usize>>,
text: &str,
_: &mut Window,
cx: &mut Context<Self>,
) {
if self.readonly {
return;
}
let composing = self.marked.is_some();
self.select_marked_or(range16);
let origin = if composing {
EditOrigin::Ime
} else {
EditOrigin::Typing
};
self.edit(origin, |m| m.insert_text(text, origin), cx);
self.end_composition();
if text == " " {
self.apply_input_rule(cx);
} else if text.ends_with(['`', '*', '~']) && self.rule_on(RuleKind::InlineMarks) {
self.apply_mark_rule(cx);
} else if text.ends_with(')') && self.rule_on(RuleKind::Link) {
self.apply_link_rule(cx);
}
}
fn replace_and_mark_text_in_range(
&mut self,
range16: Option<Range<usize>>,
new_text: &str,
new_selected16: Option<Range<usize>>,
_: &mut Window,
cx: &mut Context<Self>,
) {
if self.readonly {
return;
}
if new_text.is_empty() {
if let Some(marked) = self.marked.clone() {
self.select_marked_or(None);
if !marked.range.is_empty() {
self.edit(EditOrigin::Ime, |m| m.delete(false), cx);
}
}
self.end_composition();
return;
}
self.select_marked_or(range16);
let start = match self.model.doc.resolve_selection(self.model.selection()) {
Ok(range) => range.start,
Err(_) => return,
};
if !self.composition_open {
self.model.history.begin_transaction();
self.composition_open = true;
}
let revision = self.model.revision();
self.edit(
EditOrigin::Ime,
|m| m.insert_text(new_text, EditOrigin::Ime),
cx,
);
let inserted = self.model.revision() != revision;
let Some((_, head)) = self.head_anchor().filter(|_| inserted) else {
self.end_composition();
return;
};
let block = head.block;
let begin = if block == start.block {
start.offset
} else {
head.offset.saturating_sub(new_text.len())
};
let range = begin..begin + new_text.len();
self.marked = Some(Marked {
block,
range: range.clone(),
});
if let Some(sel16) = new_selected16 {
let sel = utf16_to_byte(new_text, sel16.start)..utf16_to_byte(new_text, sel16.end);
let _ = self.model.set_selection(BlockSelection::Text {
anchor: BlockPosition::new(block, range.start + sel.start),
head: BlockPosition::new(block, range.start + sel.end),
});
}
}
fn bounds_for_range(
&mut self,
range16: Range<usize>,
_: Bounds<Pixels>,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<Bounds<Pixels>> {
let block = self.ime_block()?;
let text = self.ime_text();
let start = utf16_to_byte(text, range16.start);
let end = utf16_to_byte(text, range16.end);
let frame = self.frame.borrow();
let width = self.style.caret_width();
let a = frame.caret_bounds(BlockPosition::new(block, start), width)?;
let b = frame.caret_bounds(BlockPosition::new(block, end), width)?;
Some(Bounds::from_corners(
a.origin,
gpui::point(
b.origin.x.max(a.origin.x + width),
a.origin.y + a.size.height,
),
))
}
fn character_index_for_point(
&mut self,
at: Point<Pixels>,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<usize> {
let pos = self.frame.borrow().hit_test(at)?;
if Some(pos.block) != self.ime_block() {
return None;
}
Some(byte_to_utf16(self.ime_text(), pos.offset))
}
}
fn link_extent(
inline: &super::inline::InlineText,
offset: usize,
url: &SharedString,
) -> Option<Range<usize>> {
let pieces: Vec<_> = inline
.pieces()
.map(|(range, marks, _)| (range, marks.link.as_ref().map(|l| l.url.clone())))
.collect();
let ix = pieces
.iter()
.position(|(r, _)| r.start <= offset && offset < r.end)?;
let same = |i: usize| pieces[i].1.as_ref() == Some(url);
let (mut lo, mut hi) = (ix, ix);
while lo > 0 && same(lo - 1) {
lo -= 1;
}
while hi + 1 < pieces.len() && same(hi + 1) {
hi += 1;
}
Some(pieces[lo].0.start..pieces[hi].0.end)
}
fn link_rule(text: &str, caret: usize) -> Option<(usize, String, String)> {
let before = text.get(..caret)?.strip_suffix(')')?;
let open_paren = before.rfind("](")?;
let url = &before[open_paren + 2..];
if url.is_empty() || url.contains(char::is_whitespace) {
return None;
}
let head = &before[..open_paren];
let open = head.rfind('[')?;
let label = &head[open + 1..];
if label.is_empty() || label.contains(['[', ']']) {
return None;
}
Some((open, label.to_string(), url.to_string()))
}
fn code_fence(text: &str) -> Option<String> {
let lang = text.strip_prefix("```")?;
lang.chars()
.all(|c| c.is_alphanumeric() || matches!(c, '+' | '-' | '_' | '#' | '.'))
.then(|| lang.to_string())
}
fn mark_rule(
inline: &super::inline::InlineText,
caret: usize,
) -> Option<(Range<usize>, Range<usize>, MarkKind)> {
let text = inline.text();
let prefix = text.get(..caret)?;
let (delim, kind) = if prefix.ends_with('`') {
("`", MarkKind::Code)
} else if prefix.ends_with("~~") {
("~~", MarkKind::Strikethrough)
} else if prefix.ends_with("**") {
("**", MarkKind::Bold)
} else if prefix.ends_with('*') {
("*", MarkKind::Italic)
} else {
return None;
};
let close = caret - delim.len()..caret;
let before = &prefix[..close.start];
let start = before.rfind(delim)?;
if delim == "*" && (before[..start].ends_with('*') || before.ends_with('*')) {
return None;
}
let open = start..start + delim.len();
let content = &text[open.end..close.start];
if content.is_empty()
|| content.contains(super::inline::ATOM_CHAR)
|| (kind != MarkKind::Code
&& (content.starts_with(char::is_whitespace) || content.ends_with(char::is_whitespace)))
{
return None;
}
let in_code = inline
.pieces()
.any(|(r, m, _)| m.code && r.start < close.end && r.end > open.start);
(!in_code).then_some((open, close, kind))
}