Skip to main content

markdown_editor/
state.rs

1//! `BlockEditorState`: the entity around [`BlockEditorModel`] (design §6).
2
3use super::input_rule::RuleKind;
4use std::sync::Arc;
5use std::{cell::RefCell, ops::Range, rc::Rc};
6
7use gpui::{
8    App, AppContext, Bounds, ClipboardItem, Context, Entity, EntityInputHandler, EventEmitter,
9    FocusHandle, Focusable, KeyBinding, Pixels, Point, SharedString, Subscription, UTF16Selection,
10    Window, actions,
11};
12
13use super::{
14    command::{CommandError, EditorCommand},
15    editor::BlockEditorModel,
16    frame::FrameLayout,
17    id::BlockId,
18    kind::BlockKind,
19    marks::MarkKind,
20    position::BlockPosition,
21    projection::{ProjectionCache, Snapshot},
22    selection::BlockSelection,
23    style::BlockEditorStyle,
24    transaction::EditOrigin,
25};
26use crate::support::BlinkCursor;
27use gpui_kit::base::text::MarkdownExtensions;
28
29pub(crate) const CONTEXT: &str = "BlockEditor";
30
31actions!(
32    block_editor,
33    [
34        Backspace,
35        Delete,
36        Enter,
37        StructuralEnter,
38        MoveLeft,
39        MoveRight,
40        MoveUp,
41        MoveDown,
42        MoveHome,
43        MoveEnd,
44        SelectLeft,
45        SelectRight,
46        SelectUp,
47        SelectDown,
48        SelectHome,
49        SelectEnd,
50        MoveDocStart,
51        MoveDocEnd,
52        SelectDocStart,
53        SelectDocEnd,
54        SelectAll,
55        MoveWordLeft,
56        MoveWordRight,
57        SelectWordLeft,
58        SelectWordRight,
59        Undo,
60        Redo,
61        ToggleBold,
62        ToggleItalic,
63        EditLink,
64        SoftBreak,
65        ExitBlock,
66        ToggleCode,
67        NormalText,
68        TableInsertColumnLeft,
69        TableInsertColumnRight,
70        TableInsertRowAbove,
71        TableInsertRowBelow,
72        TableDeleteLine,
73        CloseTableMenu,
74        Indent,
75        Outdent,
76        Copy,
77        Cut,
78        Paste,
79        PastePlain,
80        DeleteWordLeft,
81        DeleteWordRight,
82        DeleteToLineStart,
83        DeleteToLineEnd,
84        MoveParagraphStart,
85        MoveParagraphEnd,
86        SelectParagraphStart,
87        SelectParagraphEnd,
88    ]
89);
90
91pub(crate) fn init(cx: &mut App) {
92    let ctx = Some(CONTEXT);
93    #[cfg(target_os = "macos")]
94    let m = "cmd";
95    #[cfg(not(target_os = "macos"))]
96    let m = "ctrl";
97    // Word motion: Option on macOS, Ctrl elsewhere.
98    #[cfg(target_os = "macos")]
99    let w = "alt";
100    #[cfg(not(target_os = "macos"))]
101    let w = "ctrl";
102    // Document start/end: Ctrl+Home/End (Cmd+Home/End also work on macOS,
103    // next to Cmd+Up/Down).
104    let doc = m;
105    cx.bind_keys(vec![
106        KeyBinding::new("backspace", Backspace, ctx),
107        KeyBinding::new("delete", Delete, ctx),
108        KeyBinding::new("enter", Enter, ctx),
109        KeyBinding::new("left", MoveLeft, ctx),
110        KeyBinding::new("right", MoveRight, ctx),
111        KeyBinding::new("up", MoveUp, ctx),
112        KeyBinding::new("down", MoveDown, ctx),
113        KeyBinding::new("home", MoveHome, ctx),
114        KeyBinding::new("end", MoveEnd, ctx),
115        KeyBinding::new(&format!("{w}-left"), MoveWordLeft, ctx),
116        KeyBinding::new(&format!("{w}-right"), MoveWordRight, ctx),
117        KeyBinding::new(&format!("{w}-shift-left"), SelectWordLeft, ctx),
118        KeyBinding::new(&format!("{w}-shift-right"), SelectWordRight, ctx),
119        KeyBinding::new("shift-left", SelectLeft, ctx),
120        KeyBinding::new("shift-right", SelectRight, ctx),
121        KeyBinding::new("shift-up", SelectUp, ctx),
122        KeyBinding::new("shift-down", SelectDown, ctx),
123        KeyBinding::new("shift-home", SelectHome, ctx),
124        KeyBinding::new("shift-end", SelectEnd, ctx),
125        KeyBinding::new(&format!("{doc}-home"), MoveDocStart, ctx),
126        KeyBinding::new(&format!("{doc}-end"), MoveDocEnd, ctx),
127        KeyBinding::new(&format!("{doc}-shift-home"), SelectDocStart, ctx),
128        KeyBinding::new(&format!("{doc}-shift-end"), SelectDocEnd, ctx),
129        #[cfg(target_os = "macos")]
130        KeyBinding::new("cmd-up", MoveDocStart, ctx),
131        #[cfg(target_os = "macos")]
132        KeyBinding::new("cmd-down", MoveDocEnd, ctx),
133        #[cfg(target_os = "macos")]
134        KeyBinding::new("cmd-shift-up", SelectDocStart, ctx),
135        #[cfg(target_os = "macos")]
136        KeyBinding::new("cmd-shift-down", SelectDocEnd, ctx),
137        // D21: word deletes (Option on macOS, Ctrl elsewhere).
138        KeyBinding::new(&format!("{w}-backspace"), DeleteWordLeft, ctx),
139        KeyBinding::new(&format!("{w}-delete"), DeleteWordRight, ctx),
140        // D21: macOS line keys and the emacs bindings of Cocoa text fields.
141        #[cfg(target_os = "macos")]
142        KeyBinding::new("cmd-backspace", DeleteToLineStart, ctx),
143        #[cfg(target_os = "macos")]
144        KeyBinding::new("cmd-delete", DeleteToLineEnd, ctx),
145        #[cfg(target_os = "macos")]
146        KeyBinding::new("ctrl-k", DeleteToLineEnd, ctx),
147        #[cfg(target_os = "macos")]
148        KeyBinding::new("ctrl-a", MoveHome, ctx),
149        #[cfg(target_os = "macos")]
150        KeyBinding::new("ctrl-e", MoveEnd, ctx),
151        #[cfg(target_os = "macos")]
152        KeyBinding::new("ctrl-shift-a", SelectHome, ctx),
153        #[cfg(target_os = "macos")]
154        KeyBinding::new("ctrl-shift-e", SelectEnd, ctx),
155        #[cfg(target_os = "macos")]
156        KeyBinding::new("ctrl-d", Delete, ctx),
157        #[cfg(target_os = "macos")]
158        KeyBinding::new("ctrl-h", Backspace, ctx),
159        #[cfg(target_os = "macos")]
160        KeyBinding::new("cmd-left", MoveHome, ctx),
161        #[cfg(target_os = "macos")]
162        KeyBinding::new("cmd-right", MoveEnd, ctx),
163        #[cfg(target_os = "macos")]
164        KeyBinding::new("cmd-shift-left", SelectHome, ctx),
165        #[cfg(target_os = "macos")]
166        KeyBinding::new("cmd-shift-right", SelectEnd, ctx),
167        #[cfg(target_os = "macos")]
168        KeyBinding::new("alt-up", MoveParagraphStart, ctx),
169        #[cfg(target_os = "macos")]
170        KeyBinding::new("alt-down", MoveParagraphEnd, ctx),
171        #[cfg(target_os = "macos")]
172        KeyBinding::new("alt-shift-up", SelectParagraphStart, ctx),
173        #[cfg(target_os = "macos")]
174        KeyBinding::new("alt-shift-down", SelectParagraphEnd, ctx),
175        // Elsewhere paragraph motion is Ctrl+Up/Down (Ctrl+Alt+arrows stay
176        // the table keys, Ctrl+Home/End the document edges).
177        #[cfg(not(target_os = "macos"))]
178        KeyBinding::new("ctrl-up", MoveParagraphStart, ctx),
179        #[cfg(not(target_os = "macos"))]
180        KeyBinding::new("ctrl-down", MoveParagraphEnd, ctx),
181        #[cfg(not(target_os = "macos"))]
182        KeyBinding::new("ctrl-shift-up", SelectParagraphStart, ctx),
183        #[cfg(not(target_os = "macos"))]
184        KeyBinding::new("ctrl-shift-down", SelectParagraphEnd, ctx),
185        KeyBinding::new("tab", Indent, ctx),
186        KeyBinding::new("shift-tab", Outdent, ctx),
187        KeyBinding::new(&format!("{m}-a"), SelectAll, ctx),
188        KeyBinding::new(&format!("{m}-z"), Undo, ctx),
189        KeyBinding::new(&format!("{m}-shift-z"), Redo, ctx),
190        #[cfg(not(target_os = "macos"))]
191        KeyBinding::new("ctrl-y", Redo, ctx),
192        KeyBinding::new(&format!("{m}-b"), ToggleBold, ctx),
193        KeyBinding::new(&format!("{m}-i"), ToggleItalic, ctx),
194        KeyBinding::new(&format!("{m}-e"), ToggleCode, ctx),
195        KeyBinding::new(&format!("{m}-k"), EditLink, ctx),
196        // "Normal text": the block (or each selected block) becomes a paragraph.
197        KeyBinding::new(&format!("{m}-alt-0"), NormalText, ctx),
198        // Tables (D20): only act with the caret or a cell selection in a
199        // table; elsewhere the keystroke propagates.
200        KeyBinding::new(&format!("{m}-alt-left"), TableInsertColumnLeft, ctx),
201        KeyBinding::new(&format!("{m}-alt-right"), TableInsertColumnRight, ctx),
202        KeyBinding::new(&format!("{m}-alt-up"), TableInsertRowAbove, ctx),
203        KeyBinding::new(&format!("{m}-alt-down"), TableInsertRowBelow, ctx),
204        KeyBinding::new(&format!("{m}-alt-backspace"), TableDeleteLine, ctx),
205        KeyBinding::new("escape", CloseTableMenu, Some("BlockEditor && table_menu")),
206        KeyBinding::new("shift-enter", SoftBreak, ctx),
207        KeyBinding::new(&format!("{m}-enter"), ExitBlock, ctx),
208        KeyBinding::new(&format!("{m}-c"), Copy, ctx),
209        KeyBinding::new(&format!("{m}-x"), Cut, ctx),
210        KeyBinding::new(&format!("{m}-v"), Paste, ctx),
211        KeyBinding::new(&format!("{m}-shift-v"), PastePlain, ctx),
212        // Composer overrides only; standalone Newline/ModifiedEnter keep
213        // Shift+Enter soft breaks and the platform secondary-Enter command.
214        KeyBinding::new(
215            "shift-enter",
216            StructuralEnter,
217            Some("BlockEditor && submit == enter"),
218        ),
219        KeyBinding::new(
220            "ctrl-enter",
221            SoftBreak,
222            Some("BlockEditor && submit == enter"),
223        ),
224    ]);
225}
226
227/// Clipboard metadata that marks a copy as the editor's own inline Markdown
228/// (D17). Paste parses tagged text as inline Markdown; untagged text keeps
229/// the conservative import.
230pub(crate) const COPY_TAG: &str = "gpui-kit/block-editor/inline-markdown/v1";
231
232/// Clipboard metadata for a copy of several blocks, written as Markdown
233/// (headings, lists, quotes, code, tables, tasks and marks). Paste parses it
234/// back into blocks.
235pub(crate) const BLOCKS_COPY_TAG: &str = "gpui-kit/block-editor/blocks-markdown/v1";
236
237/// What the clipboard item says about where it came from.
238#[derive(Clone, Copy, Debug, PartialEq, Eq)]
239pub(crate) enum CopyKind {
240    /// Text from elsewhere: the conservative import.
241    External,
242    /// The editor's own copy inside one block: inline Markdown (D17).
243    Inline,
244    /// The editor's own copy of whole or partial blocks: block Markdown.
245    Blocks,
246}
247
248impl CopyKind {
249    fn of(item: &ClipboardItem) -> Self {
250        match item.metadata() {
251            Some(m) if m == COPY_TAG => Self::Inline,
252            Some(m) if m == BLOCKS_COPY_TAG => Self::Blocks,
253            _ => Self::External,
254        }
255    }
256
257    fn tag(self) -> Option<&'static str> {
258        match self {
259            Self::External => None,
260            Self::Inline => Some(COPY_TAG),
261            Self::Blocks => Some(BLOCKS_COPY_TAG),
262        }
263    }
264}
265
266/// Events emitted by [`BlockEditorState`].
267#[non_exhaustive]
268#[derive(Clone, Debug)]
269pub enum BlockEditorEvent {
270    /// Once per committed transaction, including undo and redo.
271    Changed {
272        revision: u64,
273        origin: EditOrigin,
274    },
275    SelectionChanged,
276    /// Cmd/Ctrl+K: the host should show its link popover (D14) for the
277    /// selection or the link at the caret, then call
278    /// [`BlockEditorState::set_link`]. `url` is the current link, if any.
279    EditLinkRequested {
280        url: Option<SharedString>,
281    },
282    /// Cmd/Ctrl+click on a link. The editor never opens a browser.
283    LinkClicked {
284        url: SharedString,
285    },
286    /// The link under the mouse changed (`None` when it left a link).
287    LinkHovered {
288        url: Option<SharedString>,
289    },
290    Focus,
291    Blur,
292    /// Enter requested submission; emitted only outside IME composition. The
293    /// owner applies completion/picker and domain-send policy, in that order.
294    SubmitRequested,
295    /// Semantic paste interception, shared by keyboard and native actions.
296    /// The owner must resolve this request with `apply_paste`; stale requests
297    /// are rejected instead of pasting into a later selection/document.
298    PasteRequested {
299        request: PasteRequest,
300        item: ClipboardItem,
301    },
302}
303
304/// Which Enter action requests submission. Newline is the document-editor default.
305#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
306pub enum SubmitPolicy {
307    #[default]
308    Newline,
309    /// Enter submits; Shift+Enter edits structurally and Ctrl+Enter inserts
310    /// a cell soft break. On macOS Cmd+Enter also submits.
311    Enter,
312    ModifiedEnter,
313}
314
315/// Whether paste uses editor defaults or is first offered to the host.
316#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
317pub enum ClipboardPolicy {
318    #[default]
319    Editor,
320    Host,
321}
322
323type InputRuleFilter = Arc<dyn Fn(super::InputRule, &str) -> bool + Send + Sync>;
324
325/// An intercepted semantic paste tied to the editor's current document and
326/// selection. Resolve once; no synthetic key events or OS clipboard access is
327/// needed when delivering an explicit `ClipboardItem`.
328#[derive(Clone, Debug)]
329pub struct PasteRequest {
330    range: super::CapturedRange,
331    plain: bool,
332    /// Where the item came from (the editor's own tagged copies, D17).
333    copy: CopyKind,
334    sequence: u64,
335}
336
337/// D22: a press on a top-level block's handle. It becomes a reorder drag
338/// once the pointer moves past a small threshold; until then the press is
339/// an ordinary click.
340#[derive(Clone, Copy, Debug)]
341pub(crate) struct BlockDrag {
342    pub(crate) block: BlockId,
343    pub(crate) start: Point<Pixels>,
344    pub(crate) active: bool,
345    /// The gap between top-level blocks the block would drop into.
346    pub(crate) drop: Option<usize>,
347}
348
349/// IME composition in progress: the marked byte range in one leaf.
350#[derive(Clone, Debug)]
351struct Marked {
352    block: BlockId,
353    range: Range<usize>,
354}
355
356pub struct BlockEditorState {
357    pub(crate) model: BlockEditorModel,
358    pub(crate) focus_handle: FocusHandle,
359    pub(crate) blink_cursor: Entity<BlinkCursor>,
360    pub(crate) style: BlockEditorStyle,
361    pub(crate) projection: ProjectionCache,
362    /// Last complete frame, written by the element after prepaint.
363    pub(crate) frame: Rc<RefCell<FrameLayout>>,
364    pub(crate) cull: Rc<RefCell<super::cull::CullState>>,
365    /// The accessible value (document text), cached per revision (D8).
366    pub(crate) a11y_value: RefCell<Option<(u64, SharedString)>>,
367    /// Scroll container to keep the caret in view (see `set_scroll_handle`).
368    pub(crate) scroll_handle: Option<gpui::ScrollHandle>,
369    /// The caret moved and should be scrolled into view after layout.
370    pub(crate) reveal: std::cell::Cell<bool>,
371    marked: Option<Marked>,
372    /// An IME composition opened an undo group that is not committed yet.
373    composition_open: bool,
374    /// Remembered x for repeated up/down.
375    goal_x: Option<Pixels>,
376    pub(crate) dragging: bool,
377    /// A press on a block handle, and the drag to reorder it (D22).
378    pub(crate) block_drag: Option<BlockDrag>,
379    readonly: bool,
380    image_policy: super::ImagePolicy,
381    submit_policy: SubmitPolicy,
382    clipboard_policy: ClipboardPolicy,
383    paste_sequence: u64,
384    pending_paste: Option<u64>,
385    input_rules: Vec<super::input_rule::InputRule>,
386    input_rule_filter: Option<InputRuleFilter>,
387    hovered_link: Option<SharedString>,
388    /// The mouse is over a task checkbox, or a link with Cmd/Ctrl held.
389    pub(crate) pointer_hand: bool,
390    /// The open table grip menu (D20).
391    pub(crate) table_menu: Option<super::table_ui::TableMenu>,
392    /// The table grips the pointer shows (to repaint on change).
393    pub(crate) table_hover: Vec<(BlockId, crate::TableLine)>,
394    /// The pointer is on a table grip (grab cursor).
395    pub(crate) grip_hover: bool,
396    _subscriptions: Vec<Subscription>,
397}
398
399impl EventEmitter<BlockEditorEvent> for BlockEditorState {}
400
401impl Focusable for BlockEditorState {
402    fn focus_handle(&self, _: &App) -> FocusHandle {
403        self.focus_handle.clone()
404    }
405}
406
407impl BlockEditorState {
408    /// Create an editor holding one empty paragraph.
409    pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
410        let focus_handle = cx.focus_handle();
411        let blink_cursor = cx.new(|_| BlinkCursor::new());
412        // Focus and blink wiring as in `InputBaseState::new_in_mode` (§7.1).
413        let subscriptions = vec![
414            cx.observe(&blink_cursor, |_, _, cx| cx.notify()),
415            cx.on_focus(&focus_handle, window, |this, _, cx| {
416                this.blink_cursor.update(cx, |b, cx| b.start(cx));
417                cx.emit(BlockEditorEvent::Focus);
418            }),
419            cx.on_blur(&focus_handle, window, |this, _, cx| {
420                this.blink_cursor.update(cx, |b, cx| b.stop(cx));
421                this.model.break_coalescing();
422                this.end_composition();
423                cx.emit(BlockEditorEvent::Blur);
424            }),
425            cx.observe_window_activation(window, |this, window, cx| {
426                if window.is_window_active() && this.focus_handle.is_focused(window) {
427                    this.blink_cursor.update(cx, |b, cx| b.start(cx));
428                }
429            }),
430        ];
431        let model = BlockEditorModel::from_markdown("", MarkdownExtensions::default())
432            .unwrap_or_else(|_| unreachable!("empty source always parses"));
433        Self {
434            model,
435            focus_handle,
436            blink_cursor,
437            style: BlockEditorStyle::default(),
438            projection: ProjectionCache::default(),
439            frame: Rc::default(),
440            cull: Rc::default(),
441            a11y_value: RefCell::default(),
442            scroll_handle: Some(gpui::ScrollHandle::new()),
443            reveal: Default::default(),
444            marked: None,
445            composition_open: false,
446            goal_x: None,
447            dragging: false,
448            block_drag: None,
449            readonly: false,
450            image_policy: super::ImagePolicy::default(),
451            submit_policy: SubmitPolicy::default(),
452            clipboard_policy: ClipboardPolicy::default(),
453            paste_sequence: 0,
454            pending_paste: None,
455            input_rules: super::input_rule::InputRule::markdown_defaults(),
456            input_rule_filter: None,
457            hovered_link: None,
458            pointer_hand: false,
459            table_menu: None,
460            table_hover: Vec::new(),
461            grip_hover: false,
462            _subscriptions: subscriptions,
463        }
464    }
465
466    pub fn with_submit_policy(mut self, policy: SubmitPolicy) -> Self {
467        self.submit_policy = policy;
468        self
469    }
470    /// Replace the policy; notify the editor after changing it so the
471    /// retained view's policy-scoped key context is refreshed.
472    pub fn set_submit_policy(&mut self, policy: SubmitPolicy) {
473        self.submit_policy = policy;
474    }
475    pub(crate) fn key_context(&self) -> &'static str {
476        match (
477            self.submit_policy == SubmitPolicy::Enter,
478            self.table_menu.is_some(),
479        ) {
480            (true, false) => "BlockEditor submit=enter",
481            (true, true) => "BlockEditor submit=enter table_menu",
482            (false, false) => CONTEXT,
483            (false, true) => "BlockEditor table_menu",
484        }
485    }
486    pub fn with_clipboard_policy(mut self, policy: ClipboardPolicy) -> Self {
487        self.clipboard_policy = policy;
488        self
489    }
490    pub fn set_clipboard_policy(&mut self, policy: ClipboardPolicy) {
491        self.clipboard_policy = policy;
492    }
493    /// The retained scroll owner; content-sized wrappers share this handle.
494    pub fn scroll_handle(&self) -> Option<gpui::ScrollHandle> {
495        self.scroll_handle.clone()
496    }
497
498    /// Builder: the Markdown input rules to run while typing (D13). The
499    /// default is [`InputRule::markdown_defaults`](super::InputRule); pass an
500    /// empty list to type Markdown literally.
501    pub fn with_input_rules(
502        mut self,
503        rules: impl IntoIterator<Item = super::input_rule::InputRule>,
504    ) -> Self {
505        self.input_rules = rules.into_iter().collect();
506        self
507    }
508
509    /// Pure policy invoked before an enabled input rule rewrites typed content.
510    /// Return false when a host workflow (e.g. completion) owns the same sigil.
511    /// Receives leaf-native text before the caret, never serialized offsets;
512    /// must not perform I/O or re-enter the editor. Defaults to allowing rules.
513    pub fn with_input_rule_filter(
514        mut self,
515        filter: impl Fn(super::InputRule, &str) -> bool + Send + Sync + 'static,
516    ) -> Self {
517        self.input_rule_filter = Some(Arc::new(filter));
518        self
519    }
520
521    /// Replace the input rules (see [`Self::with_input_rules`]).
522    pub fn set_input_rules(
523        &mut self,
524        rules: impl IntoIterator<Item = super::input_rule::InputRule>,
525    ) {
526        self.input_rules = rules.into_iter().collect();
527    }
528
529    /// Check or uncheck the task item holding `block` (a list item or its
530    /// text leaf); one undo step (D14). The selection is kept.
531    pub fn set_task_checked(&mut self, block: BlockId, checked: bool, cx: &mut Context<Self>) {
532        if self.readonly {
533            return;
534        }
535        let leaf = if self.model.doc.inline(block).is_some() {
536            block
537        } else {
538            match self.model.doc.children(block).first() {
539                Some(child) => *child,
540                None => return,
541            }
542        };
543        self.model.break_coalescing();
544        self.edit(
545            EditOrigin::Command("set_task_checked"),
546            |m| m.set_item_checked(leaf, checked).map(|_| ()),
547            cx,
548        );
549        self.model.break_coalescing();
550    }
551
552    fn rule_on(&self, kind: super::input_rule::RuleKind) -> bool {
553        self.input_rules.iter().any(|rule| rule.0 == kind)
554            && self.input_rule_filter.as_ref().is_none_or(|filter| {
555                filter(
556                    super::InputRule(kind),
557                    self.model.caret_prefix().unwrap_or_default(),
558                )
559            })
560    }
561
562    /// Builder: initial Markdown.
563    pub fn with_source(mut self, source: &str) -> Self {
564        let _ = self.model.set_source(source);
565        self.projection.invalidate();
566        self
567    }
568
569    /// Replace the content. No `Changed` event; clears history.
570    pub fn set_source(&mut self, source: &str, cx: &mut Context<Self>) -> Result<(), SharedString> {
571        self.model.set_source(source)?;
572        self.marked = None;
573        self.composition_open = false;
574        self.pending_paste = None;
575        self.projection.invalidate();
576        cx.notify();
577        Ok(())
578    }
579
580    /// The document as canonical Markdown.
581    pub fn source(&self) -> String {
582        self.model.source()
583    }
584
585    /// Shared canonical source, materialized at most once per revision.
586    pub fn source_snapshot(&self) -> super::SourceSnapshot {
587        self.model.source_snapshot()
588    }
589
590    pub fn capture_caret(&self) -> Result<super::CapturedRange, super::CapturedRangeError> {
591        self.model.capture_caret()
592    }
593    pub fn capture_selection(&self) -> Result<super::CapturedRange, super::CapturedRangeError> {
594        self.model.capture_selection()
595    }
596    pub fn capture_suffix(
597        &self,
598        len: usize,
599    ) -> Result<super::CapturedRange, super::CapturedRangeError> {
600        self.model.capture_suffix(len)
601    }
602    pub fn capture_leaf_range(
603        &self,
604        range: std::ops::Range<usize>,
605    ) -> Result<super::CapturedRange, super::CapturedRangeError> {
606        self.model.capture_leaf_range(range)
607    }
608    /// Configure presentation/resource admission without mutating the document.
609    pub fn with_image_policy(mut self, policy: super::ImagePolicy) -> Self {
610        self.image_policy = policy;
611        self
612    }
613    pub fn set_image_policy(&mut self, policy: super::ImagePolicy, cx: &mut Context<Self>) {
614        self.image_policy = policy;
615        self.projection.invalidate();
616        cx.notify();
617    }
618    pub fn image_policy(&self) -> super::ImagePolicy {
619        self.image_policy
620    }
621
622    pub fn caret_prefix_snapshot(&self) -> Option<SharedString> {
623        self.model.caret_prefix_snapshot()
624    }
625    pub fn caret_prefix(&self) -> Option<&str> {
626        self.model.caret_prefix()
627    }
628    pub fn is_capture_current(&self, range: &super::CapturedRange) -> bool {
629        self.model.is_capture_current(range)
630    }
631    pub fn replace_captured(
632        &mut self,
633        range: &mut super::CapturedRange,
634        text: &str,
635        cx: &mut Context<Self>,
636    ) -> Result<(), super::CapturedRangeError> {
637        if self.readonly || self.marked.is_some() {
638            return Err(super::CapturedRangeError::Stale);
639        }
640        self.model.replace_captured(range, text)?;
641        self.after_edit(EditOrigin::Api, cx);
642        Ok(())
643    }
644    pub fn select_end(&mut self, cx: &mut Context<Self>) -> Result<(), super::SelectionError> {
645        self.model.select_end()?;
646        self.selection_changed(cx);
647        Ok(())
648    }
649    /// Reveal the current caret after viewport/layout changes without moving it.
650    pub fn reveal_caret(&mut self, cx: &mut Context<Self>) {
651        self.reveal.set(true);
652        cx.notify();
653    }
654    /// Fence host callbacks on external owner changes without reparsing content.
655    pub fn invalidate_captures(&mut self) {
656        self.model.invalidate_captures();
657        self.pending_paste = None;
658    }
659    pub fn is_composing(&self) -> bool {
660        self.marked.is_some()
661    }
662
663    /// The headless model (document, selection, history).
664    pub fn model(&self) -> &BlockEditorModel {
665        &self.model
666    }
667
668    /// Document revision; bumped by every committed transaction.
669    pub fn revision(&self) -> u64 {
670        self.model.revision()
671    }
672
673    /// The current selection.
674    pub fn selection(&self) -> &BlockSelection {
675        self.model.selection()
676    }
677
678    /// Kind of the text block holding the caret, if any.
679    pub fn block_kind_at_caret(&self) -> Option<BlockKind> {
680        self.model.block_kind_at_caret()
681    }
682
683    /// Override caret, selection and composition colours (`BlockEditorStyle`).
684    pub fn set_editor_style(&mut self, style: BlockEditorStyle, cx: &mut Context<Self>) {
685        self.style = style;
686        cx.notify();
687    }
688
689    /// The editor style overrides in effect.
690    pub fn editor_style(&self) -> &BlockEditorStyle {
691        &self.style
692    }
693
694    /// The scroll container the editor sits in (its `track_scroll` handle).
695    /// With one set, the caret is scrolled into view after it moves; blocks
696    /// outside the container's visible area are culled either way.
697    pub fn set_scroll_handle(&mut self, handle: gpui::ScrollHandle) {
698        self.scroll_handle = Some(handle);
699    }
700
701    /// Whether edits are blocked (navigation and selection still work).
702    pub fn is_readonly(&self) -> bool {
703        self.readonly
704    }
705
706    /// Block or allow edits.
707    pub fn set_readonly(&mut self, readonly: bool, cx: &mut Context<Self>) {
708        self.readonly = readonly;
709        cx.notify();
710    }
711
712    /// Focus the editor.
713    pub fn focus(&self, window: &mut Window, cx: &mut App) {
714        window.focus(&self.focus_handle, cx);
715    }
716
717    /// Set the selection; emits `SelectionChanged`.
718    pub fn set_selection(
719        &mut self,
720        selection: BlockSelection,
721        cx: &mut Context<Self>,
722    ) -> Result<(), super::SelectionError> {
723        self.model.set_selection(selection)?;
724        self.selection_changed(cx);
725        Ok(())
726    }
727
728    /// Run a model command as one transaction.
729    pub fn run(
730        &mut self,
731        command: EditorCommand,
732        cx: &mut Context<Self>,
733    ) -> Result<bool, CommandError> {
734        if self.readonly {
735            return Ok(false);
736        }
737        let name = command_name(&command);
738        self.end_composition();
739        let revision = self.model.revision();
740        let changed = self.model.run(command)?;
741        if self.model.revision() != revision {
742            self.after_edit(EditOrigin::Command(name), cx);
743        } else {
744            // Stored-mark toggles change no text but show in the toolbar.
745            cx.notify();
746        }
747        Ok(changed)
748    }
749
750    /// Undo the last transaction (or IME composition) group.
751    pub fn undo(&mut self, cx: &mut Context<Self>) {
752        if self.readonly {
753            return;
754        }
755        if self.model.undo() {
756            self.after_edit(EditOrigin::Undo, cx);
757        }
758    }
759
760    /// Redo the last undone group.
761    pub fn redo(&mut self, cx: &mut Context<Self>) {
762        if self.readonly {
763            return;
764        }
765        if self.model.redo() {
766            self.after_edit(EditOrigin::Redo, cx);
767        }
768    }
769
770    /// The document as plain text for assistive technology (D8): leaves in
771    /// order, one per line, inline atoms dropped. Built at most once per
772    /// revision, and only asked for while accessibility is active.
773    pub(crate) fn accessible_value(&self) -> SharedString {
774        let revision = self.model.revision();
775        if let Some((cached, value)) = &*self.a11y_value.borrow()
776            && *cached == revision
777        {
778            return value.clone();
779        }
780        let doc = &self.model.doc;
781        let value: SharedString = doc
782            .leaves()
783            .iter()
784            .filter_map(|id| doc.leaf_text(*id))
785            .map(|text| text.replace(super::inline::ATOM_CHAR, ""))
786            .collect::<Vec<_>>()
787            .join("\n")
788            .into();
789        *self.a11y_value.borrow_mut() = Some((revision, value.clone()));
790        value
791    }
792
793    pub(crate) fn snapshot(&mut self) -> Rc<Snapshot> {
794        self.projection
795            .set_placeholders(self.image_policy == super::ImagePolicy::Placeholder);
796        self.projection.snapshot(&self.model.doc)
797    }
798
799    /// Blocks that are always laid out: the selection ends, the IME
800    /// composition, and their ancestors.
801    pub(crate) fn pinned_blocks(&self) -> Vec<BlockId> {
802        let doc = &self.model.doc;
803        let mut ids = match self.model.selection() {
804            BlockSelection::Text { anchor, head } => vec![anchor.block, head.block],
805            BlockSelection::Blocks { anchor, head } | BlockSelection::Cells { anchor, head } => {
806                vec![*anchor, *head]
807            }
808        };
809        if let Some((block, _)) = self.marked_range() {
810            ids.push(block);
811        }
812        // Every ancestor, so a chunk of a long list holding the caret's item
813        // is pinned too.
814        ids.into_iter().flat_map(|id| doc.path(id)).collect()
815    }
816
817    /// Per-leaf selected byte ranges, for painting.
818    pub(crate) fn selection_ranges(&self) -> Vec<(BlockId, Range<usize>)> {
819        if let Some(cells) = self.model.selected_cells() {
820            return cells
821                .into_iter()
822                .flatten()
823                .map(|c| (c, 0..self.model.doc.leaf_len(c).unwrap_or(0)))
824                .collect();
825        }
826        let Ok(range) = self.model.doc.resolve_selection(self.model.selection()) else {
827            return Vec::new();
828        };
829        if range.is_collapsed() {
830            return Vec::new();
831        }
832        self.model
833            .text_ranges(range)
834            .into_iter()
835            .map(|(id, lo, hi)| (id, lo..hi))
836            .collect()
837    }
838
839    pub(crate) fn marked_range(&self) -> Option<(BlockId, Range<usize>)> {
840        self.marked.as_ref().map(|m| (m.block, m.range.clone()))
841    }
842
843    pub(crate) fn show_caret(&self, window: &Window, cx: &App) -> bool {
844        self.focus_handle.is_focused(window) && self.blink_cursor.read(cx).visible()
845    }
846
847    // ---- internal edit plumbing -------------------------------------------
848
849    fn after_edit(&mut self, origin: EditOrigin, cx: &mut Context<Self>) {
850        self.goal_x = None;
851        self.reveal.set(true);
852        self.pause_blink(cx);
853        cx.emit(BlockEditorEvent::Changed {
854            revision: self.model.revision(),
855            origin,
856        });
857        cx.emit(BlockEditorEvent::SelectionChanged);
858        cx.notify();
859    }
860
861    fn selection_changed(&mut self, cx: &mut Context<Self>) {
862        self.reveal.set(true);
863        self.pause_blink(cx);
864        cx.emit(BlockEditorEvent::SelectionChanged);
865        cx.notify();
866    }
867
868    fn pause_blink(&mut self, cx: &mut Context<Self>) {
869        self.blink_cursor.update(cx, |b, cx| b.pause(cx));
870    }
871
872    fn edit(
873        &mut self,
874        origin: EditOrigin,
875        f: impl FnOnce(&mut BlockEditorModel) -> Result<(), CommandError>,
876        cx: &mut Context<Self>,
877    ) {
878        if self.readonly {
879            return;
880        }
881        let revision = self.model.revision();
882        // Errors (e.g. typing into a code block, Phase 4) leave the document
883        // untouched; the spike ignores them.
884        let _ = f(&mut self.model);
885        if self.model.revision() != revision {
886            self.after_edit(origin, cx);
887        } else {
888            cx.notify();
889        }
890    }
891
892    fn end_composition(&mut self) {
893        self.marked = None;
894        if std::mem::take(&mut self.composition_open) {
895            self.model.history.commit_transaction();
896        }
897    }
898
899    /// Whether an IME undo group is open (tests: it must never leak).
900    #[cfg(test)]
901    pub(crate) fn composition_open(&self) -> bool {
902        self.composition_open
903    }
904
905    /// A whole-block selection collapses to its start or end.
906    fn collapse_blocks(&mut self, forward: bool, cx: &mut Context<Self>) -> bool {
907        if !matches!(
908            self.model.selection(),
909            BlockSelection::Blocks { .. } | BlockSelection::Cells { .. }
910        ) {
911            return false;
912        }
913        if let Ok(range) = self.model.doc.resolve_selection(self.model.selection()) {
914            let target = if forward { range.end } else { range.start };
915            self.move_to(target, false, cx);
916        }
917        true
918    }
919
920    fn head_anchor(&self) -> Option<(BlockPosition, BlockPosition)> {
921        match self.model.selection() {
922            BlockSelection::Text { anchor, head } => Some((*anchor, *head)),
923            BlockSelection::Blocks { .. } => None,
924            BlockSelection::Cells { anchor, head } => {
925                Some((BlockPosition::new(*anchor, 0), BlockPosition::new(*head, 0)))
926            }
927        }
928    }
929
930    fn move_to(&mut self, head: BlockPosition, extend: bool, cx: &mut Context<Self>) {
931        let anchor = match (extend, self.head_anchor()) {
932            (true, Some((anchor, _))) => anchor,
933            _ => head,
934        };
935        self.end_composition();
936        // A drag or Shift+arrow from one cell into another of the same table
937        // selects cells, not text.
938        let doc = &self.model.doc;
939        let selection = match (
940            super::table::table_of(doc, anchor.block),
941            super::table::table_of(doc, head.block),
942        ) {
943            (Some(a), Some(b)) if a == b && anchor.block != head.block => BlockSelection::Cells {
944                anchor: anchor.block,
945                head: head.block,
946            },
947            _ => BlockSelection::Text { anchor, head },
948        };
949        let _ = self.model.set_selection(selection);
950        self.selection_changed(cx);
951    }
952
953    /// One grapheme left/right, crossing leaf boundaries in document order.
954    pub(crate) fn step_horizontal(&self, pos: BlockPosition, forward: bool) -> BlockPosition {
955        let doc = &self.model.doc;
956        let len = doc.leaf_len(pos.block).unwrap_or(0);
957        if let Some(code) = doc.plain(pos.block) {
958            let next = if forward {
959                code[pos.offset..]
960                    .chars()
961                    .next()
962                    .map(|c| pos.offset + c.len_utf8())
963            } else {
964                code[..pos.offset]
965                    .chars()
966                    .next_back()
967                    .map(|c| pos.offset - c.len_utf8())
968            };
969            if let Some(offset) = next {
970                return BlockPosition::new(pos.block, offset);
971            }
972        } else if let Some(inline) = doc.inline(pos.block) {
973            let next = if forward {
974                inline.next_grapheme(pos.offset)
975            } else {
976                inline.prev_grapheme(pos.offset)
977            };
978            if let Some(offset) = next.filter(|o| *o != pos.offset) {
979                return BlockPosition::new(pos.block, offset);
980            }
981        } else if forward && pos.offset == 0 {
982            return BlockPosition::new(pos.block, 1);
983        } else if !forward && pos.offset == 1 {
984            return BlockPosition::new(pos.block, 0);
985        }
986        let leaves = doc.leaves();
987        let Some(ix) = leaves.iter().position(|id| *id == pos.block) else {
988            return pos;
989        };
990        let neighbour = if forward {
991            leaves.get(ix + 1)
992        } else {
993            ix.checked_sub(1).and_then(|i| leaves.get(i))
994        };
995        match neighbour {
996            Some(id) if forward => BlockPosition::new(*id, 0),
997            Some(id) => BlockPosition::new(*id, doc.leaf_len(*id).unwrap_or(0)),
998            None => BlockPosition::new(pos.block, if forward { len } else { 0 }),
999        }
1000    }
1001
1002    /// The next word boundary from `pos` (skip spaces, then a word); at a
1003    /// leaf edge, one step into the neighbouring leaf.
1004    fn step_word(&self, pos: BlockPosition, forward: bool) -> BlockPosition {
1005        let Some(text) = self.model.doc.leaf_text(pos.block) else {
1006            return self.step_horizontal(pos, forward);
1007        };
1008        let is_word = |c: char| c.is_alphanumeric() || c == '_';
1009        let mut offset = pos.offset;
1010        if forward {
1011            let rest = &text[offset..];
1012            let mut it = rest.char_indices().peekable();
1013            while let Some((_, c)) = it.peek().copied()
1014                && !is_word(c)
1015            {
1016                it.next();
1017            }
1018            while let Some((_, c)) = it.peek().copied()
1019                && is_word(c)
1020            {
1021                it.next();
1022            }
1023            offset += it.peek().map(|(i, _)| *i).unwrap_or(rest.len());
1024        } else {
1025            let before = &text[..offset];
1026            let mut it = before.char_indices().rev().peekable();
1027            while let Some((_, c)) = it.peek().copied()
1028                && !is_word(c)
1029            {
1030                it.next();
1031            }
1032            let mut start = it.peek().map(|(i, c)| i + c.len_utf8()).unwrap_or(0);
1033            while let Some((i, c)) = it.peek().copied()
1034                && is_word(c)
1035            {
1036                start = i;
1037                it.next();
1038            }
1039            offset = if start < offset { start } else { 0 };
1040        }
1041        if offset == pos.offset {
1042            return self.step_horizontal(pos, forward);
1043        }
1044        self.model.doc.clamp(BlockPosition::new(pos.block, offset))
1045    }
1046
1047    fn word(&mut self, forward: bool, extend: bool, cx: &mut Context<Self>) {
1048        if self.collapse_blocks(forward, cx) {
1049            return;
1050        }
1051        let Some((_, head)) = self.head_anchor() else {
1052            return;
1053        };
1054        self.goal_x = None;
1055        let next = self.step_word(head, forward);
1056        self.move_to(next, extend, cx);
1057    }
1058
1059    pub(crate) fn on_move_word_left(
1060        &mut self,
1061        _: &MoveWordLeft,
1062        _: &mut Window,
1063        cx: &mut Context<Self>,
1064    ) {
1065        self.word(false, false, cx);
1066    }
1067    pub(crate) fn on_move_word_right(
1068        &mut self,
1069        _: &MoveWordRight,
1070        _: &mut Window,
1071        cx: &mut Context<Self>,
1072    ) {
1073        self.word(true, false, cx);
1074    }
1075    pub(crate) fn on_select_word_left(
1076        &mut self,
1077        _: &SelectWordLeft,
1078        _: &mut Window,
1079        cx: &mut Context<Self>,
1080    ) {
1081        self.word(false, true, cx);
1082    }
1083    pub(crate) fn on_select_word_right(
1084        &mut self,
1085        _: &SelectWordRight,
1086        _: &mut Window,
1087        cx: &mut Context<Self>,
1088    ) {
1089        self.word(true, true, cx);
1090    }
1091
1092    fn horizontal(&mut self, forward: bool, extend: bool, cx: &mut Context<Self>) {
1093        if self.collapse_blocks(forward, cx) {
1094            return;
1095        }
1096        let Some((anchor, head)) = self.head_anchor() else {
1097            return;
1098        };
1099        self.goal_x = None;
1100        // Collapsing a range moves to its edge, like every text field.
1101        if !extend && anchor != head {
1102            let ord = self.model.doc.cmp_positions(anchor, head).ok();
1103            let pick_head = matches!(
1104                (forward, ord),
1105                (true, Some(std::cmp::Ordering::Less)) | (false, Some(std::cmp::Ordering::Greater))
1106            );
1107            let target = if pick_head { head } else { anchor };
1108            return self.move_to(target, false, cx);
1109        }
1110        let next = self.step_horizontal(head, forward);
1111        self.move_to(next, extend, cx);
1112    }
1113
1114    fn vertical(&mut self, down: bool, extend: bool, cx: &mut Context<Self>) {
1115        if self.collapse_blocks(down, cx) {
1116            return;
1117        }
1118        let Some((_, head)) = self.head_anchor() else {
1119            return;
1120        };
1121        let leaves = self.snapshot().leaves.clone();
1122        let Some(ix) = leaves.iter().position(|l| l.block == head.block) else {
1123            return;
1124        };
1125        // The model-adjacent caret leaf, which may be culled (no geometry).
1126        let adjacent = if down {
1127            leaves.get(ix + 1)
1128        } else {
1129            ix.checked_sub(1).and_then(|i| leaves.get(i))
1130        };
1131        let frame = self.frame.clone();
1132        let frame = frame.borrow();
1133        let laid_out = frame.caret_bounds(head, self.style.caret_width());
1134        let x = laid_out.map(|caret| *self.goal_x.get_or_insert(caret.origin.x));
1135        let visual = x.and_then(|x| frame.vertical(head, x, down));
1136        let target = match (visual, adjacent) {
1137            // Within the block, or into the laid-out adjacent leaf.
1138            (Some(t), _) if t.block == head.block => t,
1139            // Down past a table's last row leaves the table (D13).
1140            _ if down && !extend && self.model.in_last_table_row() => {
1141                drop(frame);
1142                self.edit(
1143                    EditOrigin::Command("table_exit"),
1144                    |m| m.exit_table().map(|_| ()),
1145                    cx,
1146                );
1147                return;
1148            }
1149            (Some(t), Some(next)) if t.block == next.block => t,
1150            // Up/down inside a table goes to the cell above/below (cells of
1151            // one table are laid out together).
1152            (Some(t), _)
1153                if super::table::table_of(&self.model.doc, head.block).is_some_and(|table| {
1154                    super::table::table_of(&self.model.doc, t.block) == Some(table)
1155                }) =>
1156            {
1157                t
1158            }
1159            // Into a culled (or not yet laid-out) leaf: model fallback.
1160            (_, Some(next)) => {
1161                let offset = if down { 0 } else { next.len };
1162                BlockPosition::new(next.block, offset)
1163            }
1164            (_, None) if down && self.model.doc.plain(head.block).is_some() && !extend => {
1165                drop(frame);
1166                self.edit(
1167                    EditOrigin::Command("exit_code_block"),
1168                    |m| m.exit_code_block().map(|_| ()),
1169                    cx,
1170                );
1171                return;
1172            }
1173            (_, None) => {
1174                let doc = &self.model.doc;
1175                if down {
1176                    doc.last_caret()
1177                } else {
1178                    doc.first_caret()
1179                }
1180            }
1181        };
1182        drop(frame);
1183        let goal = self.goal_x;
1184        self.move_to(self.model.doc.clamp(target), extend, cx);
1185        self.goal_x = goal;
1186    }
1187
1188    /// Home/End: the visual (soft-wrapped) line from the last frame, else
1189    /// the block edge.
1190    fn line_edge(&mut self, end: bool, extend: bool, cx: &mut Context<Self>) {
1191        if self.collapse_blocks(end, cx) {
1192            return;
1193        }
1194        let Some((_, head)) = self.head_anchor() else {
1195            return;
1196        };
1197        let target = self.visual_line_edge(head, end);
1198        self.goal_x = None;
1199        self.move_to(target, extend, cx);
1200    }
1201
1202    /// Start or end of the visual (soft-wrapped) line holding `head`; the
1203    /// leaf's edge when it has no geometry.
1204    fn visual_line_edge(&self, head: BlockPosition, end: bool) -> BlockPosition {
1205        let visual = self.frame.borrow().line_edge(head, end);
1206        // End at a soft wrap: the line's end index is also the next line's
1207        // start, where the caret would draw. Stay before the wrap point.
1208        let visual = visual.map(|pos| {
1209            if !end || pos.block != head.block {
1210                return pos;
1211            }
1212            let frame = self.frame.borrow();
1213            let (Some(leaf), Some(inline)) =
1214                (frame.leaf(pos.block), self.model.doc.inline(pos.block))
1215            else {
1216                return pos;
1217            };
1218            let line_y = leaf.caret_point(head.offset).y;
1219            if pos.offset < inline.len() && leaf.caret_point(pos.offset).y > line_y {
1220                let prev = inline.prev_grapheme(pos.offset).unwrap_or(pos.offset);
1221                BlockPosition::new(pos.block, prev)
1222            } else {
1223                pos
1224            }
1225        });
1226        let target = visual.unwrap_or_else(|| {
1227            let offset = if end {
1228                self.model.doc.leaf_len(head.block).unwrap_or(0)
1229            } else {
1230                0
1231            };
1232            BlockPosition::new(head.block, offset)
1233        });
1234        self.model.doc.clamp(target)
1235    }
1236
1237    /// Where a D21 line delete stops: the visual line edge in text, the code
1238    /// line (between newlines) in a code block; always inside `head`'s leaf.
1239    fn delete_line_target(&self, head: BlockPosition, end: bool) -> BlockPosition {
1240        if let Some(code) = self.model.doc.plain(head.block) {
1241            let offset = if end {
1242                code[head.offset..]
1243                    .find('\n')
1244                    .map_or(code.len(), |i| head.offset + i)
1245            } else {
1246                code[..head.offset].rfind('\n').map_or(0, |i| i + 1)
1247            };
1248            return BlockPosition::new(head.block, offset);
1249        }
1250        let target = self.visual_line_edge(head, end);
1251        if target.block == head.block {
1252            target
1253        } else {
1254            head
1255        }
1256    }
1257
1258    /// Where a D21 word delete stops: the word motion's boundary, but never
1259    /// past an inline atom (an adjacent atom is deleted on its own), and
1260    /// never outside `head`'s leaf.
1261    fn delete_word_target(&self, head: BlockPosition, forward: bool) -> BlockPosition {
1262        use super::inline::ATOM_CHAR;
1263        let Some(text) = self.model.doc.leaf_text(head.block) else {
1264            return head;
1265        };
1266        let adjacent = if forward {
1267            text[head.offset..].chars().next()
1268        } else {
1269            text[..head.offset].chars().next_back()
1270        };
1271        let atom = ATOM_CHAR.len_utf8();
1272        match adjacent {
1273            None => return head,
1274            Some(ATOM_CHAR) if forward => {
1275                return BlockPosition::new(head.block, head.offset + atom);
1276            }
1277            Some(ATOM_CHAR) => return BlockPosition::new(head.block, head.offset - atom),
1278            Some(_) => {}
1279        }
1280        let step = self.step_word(head, forward);
1281        if step.block != head.block {
1282            return head;
1283        }
1284        let offset = if forward {
1285            text[head.offset..step.offset]
1286                .find(ATOM_CHAR)
1287                .map_or(step.offset, |i| head.offset + i)
1288        } else {
1289            text[step.offset..head.offset]
1290                .rfind(ATOM_CHAR)
1291                .map_or(step.offset, |i| step.offset + i + atom)
1292        };
1293        self.model.doc.clamp(BlockPosition::new(head.block, offset))
1294    }
1295
1296    /// D21: delete a word or to the line edge. A range (or block / cell
1297    /// selection) deletes just the selection; at the leaf's edge it is a
1298    /// plain Backspace / Delete, except in a table cell, where it does
1299    /// nothing.
1300    fn delete_by(&mut self, line: bool, forward: bool, cx: &mut Context<Self>) {
1301        self.end_composition();
1302        if self.readonly {
1303            return;
1304        }
1305        let origin = if forward {
1306            EditOrigin::DeleteForward
1307        } else {
1308            EditOrigin::Backspace
1309        };
1310        let head = match self.model.selection() {
1311            BlockSelection::Text { anchor, head } if anchor == head => *head,
1312            _ => return self.edit(origin, |m| m.delete(forward), cx),
1313        };
1314        let target = if line {
1315            self.delete_line_target(head, forward)
1316        } else {
1317            self.delete_word_target(head, forward)
1318        };
1319        if line {
1320            self.model.break_coalescing();
1321        }
1322        if target == head {
1323            if super::table::table_of(&self.model.doc, head.block).is_none() {
1324                self.edit(origin, |m| m.delete(forward), cx);
1325            }
1326        } else {
1327            let origin = if forward {
1328                EditOrigin::WordDelete
1329            } else {
1330                EditOrigin::WordBackspace
1331            };
1332            self.edit(origin, |m| m.delete_span(target, head, forward), cx);
1333        }
1334        if line {
1335            self.model.break_coalescing();
1336        }
1337    }
1338
1339    /// D21 Option+Up/Down: the start / end of the block, or of the previous
1340    /// / next one when already there.
1341    fn paragraph_edge(&mut self, forward: bool, extend: bool, cx: &mut Context<Self>) {
1342        if self.collapse_blocks(forward, cx) {
1343            return;
1344        }
1345        let Some((_, head)) = self.head_anchor() else {
1346            return;
1347        };
1348        let doc = &self.model.doc;
1349        let len = doc.leaf_len(head.block).unwrap_or(0);
1350        let mut pos = head;
1351        if (forward && pos.offset >= len) || (!forward && pos.offset == 0) {
1352            pos = self.step_horizontal(pos, forward);
1353        }
1354        let offset = if forward {
1355            doc.leaf_len(pos.block).unwrap_or(0)
1356        } else {
1357            0
1358        };
1359        let target = doc.clamp(BlockPosition::new(pos.block, offset));
1360        self.goal_x = None;
1361        self.move_to(target, extend, cx);
1362    }
1363
1364    pub(crate) fn on_delete_word_left(
1365        &mut self,
1366        _: &DeleteWordLeft,
1367        _: &mut Window,
1368        cx: &mut Context<Self>,
1369    ) {
1370        self.delete_by(false, false, cx);
1371    }
1372    pub(crate) fn on_delete_word_right(
1373        &mut self,
1374        _: &DeleteWordRight,
1375        _: &mut Window,
1376        cx: &mut Context<Self>,
1377    ) {
1378        self.delete_by(false, true, cx);
1379    }
1380    pub(crate) fn on_delete_to_line_start(
1381        &mut self,
1382        _: &DeleteToLineStart,
1383        _: &mut Window,
1384        cx: &mut Context<Self>,
1385    ) {
1386        self.delete_by(true, false, cx);
1387    }
1388    pub(crate) fn on_delete_to_line_end(
1389        &mut self,
1390        _: &DeleteToLineEnd,
1391        _: &mut Window,
1392        cx: &mut Context<Self>,
1393    ) {
1394        self.delete_by(true, true, cx);
1395    }
1396    pub(crate) fn on_move_paragraph_start(
1397        &mut self,
1398        _: &MoveParagraphStart,
1399        _: &mut Window,
1400        cx: &mut Context<Self>,
1401    ) {
1402        self.paragraph_edge(false, false, cx);
1403    }
1404    pub(crate) fn on_move_paragraph_end(
1405        &mut self,
1406        _: &MoveParagraphEnd,
1407        _: &mut Window,
1408        cx: &mut Context<Self>,
1409    ) {
1410        self.paragraph_edge(true, false, cx);
1411    }
1412    pub(crate) fn on_select_paragraph_start(
1413        &mut self,
1414        _: &SelectParagraphStart,
1415        _: &mut Window,
1416        cx: &mut Context<Self>,
1417    ) {
1418        self.paragraph_edge(false, true, cx);
1419    }
1420    pub(crate) fn on_select_paragraph_end(
1421        &mut self,
1422        _: &SelectParagraphEnd,
1423        _: &mut Window,
1424        cx: &mut Context<Self>,
1425    ) {
1426        self.paragraph_edge(true, true, cx);
1427    }
1428
1429    /// The link drawn under the window point `at`: its leaf, byte range
1430    /// and url.
1431    pub fn link_at(&self, at: Point<Pixels>) -> Option<(BlockId, Range<usize>, SharedString)> {
1432        let pos = {
1433            let frame = self.frame.borrow();
1434            let pos = frame.hit_test(at)?;
1435            if !frame.leaf(pos.block)?.bounds().contains(&at) {
1436                return None;
1437            }
1438            pos
1439        };
1440        let inline = self.model.doc.inline(pos.block)?;
1441        let url = inline.mark_after(pos.offset)?.link.as_ref()?.url.clone();
1442        let range = link_extent(inline, pos.offset, &url)?;
1443        Some((pos.block, range, url))
1444    }
1445
1446    /// The url of the link under the mouse, if any (D14 hover).
1447    pub fn hovered_link(&self) -> Option<&SharedString> {
1448        self.hovered_link.as_ref()
1449    }
1450
1451    pub(crate) fn hover_at(&mut self, at: Point<Pixels>, secondary: bool, cx: &mut Context<Self>) {
1452        self.hover_table(at, cx);
1453        let url = self.link_at(at).map(|(_, _, url)| url);
1454        let hand = self.task_checkbox_at(at).is_some() || (secondary && url.is_some());
1455        if hand != self.pointer_hand {
1456            self.pointer_hand = hand;
1457            cx.notify();
1458        }
1459        if url != self.hovered_link {
1460            self.hovered_link = url.clone();
1461            cx.emit(BlockEditorEvent::LinkHovered { url });
1462            cx.notify();
1463        }
1464    }
1465
1466    /// Cmd/Ctrl+click: emit [`BlockEditorEvent::LinkClicked`] for the link
1467    /// under `at`. Returns whether there was one.
1468    pub(crate) fn follow_link_at(&mut self, at: Point<Pixels>, cx: &mut Context<Self>) -> bool {
1469        match self.link_at(at) {
1470            Some((_, _, url)) => {
1471                cx.emit(BlockEditorEvent::LinkClicked { url });
1472                true
1473            }
1474            None => false,
1475        }
1476    }
1477
1478    /// The url of the link at the caret (or the selection start).
1479    pub fn link_at_caret(&self) -> Option<SharedString> {
1480        self.link_range_at_caret().map(|(_, _, url)| url)
1481    }
1482
1483    fn link_range_at_caret(&self) -> Option<(BlockId, Range<usize>, SharedString)> {
1484        let head = match &self.model.selection {
1485            BlockSelection::Text { anchor, head } => {
1486                if anchor.block == head.block && anchor.offset < head.offset {
1487                    *anchor
1488                } else {
1489                    *head
1490                }
1491            }
1492            _ => return None,
1493        };
1494        let inline = self.model.doc.inline(head.block)?;
1495        let mark = inline
1496            .mark_after(head.offset)
1497            .filter(|m| m.link.is_some())
1498            .or_else(|| inline.mark_before(head.offset))?;
1499        let url = mark.link.as_ref()?.url.clone();
1500        let probe = if inline
1501            .mark_after(head.offset)
1502            .is_some_and(|m| m.link.is_some())
1503        {
1504            head.offset
1505        } else {
1506            head.offset.checked_sub(1)?
1507        };
1508        let range = link_extent(inline, probe, &url)?;
1509        Some((head.block, range, url))
1510    }
1511
1512    /// Cmd/Ctrl+K: ask the host for its link popover.
1513    pub fn request_link_edit(&mut self, cx: &mut Context<Self>) {
1514        if self.readonly {
1515            return;
1516        }
1517        let url = self.link_at_caret();
1518        cx.emit(BlockEditorEvent::EditLinkRequested { url });
1519    }
1520
1521    pub(crate) fn on_edit_link(&mut self, _: &EditLink, _: &mut Window, cx: &mut Context<Self>) {
1522        self.request_link_edit(cx);
1523    }
1524
1525    /// Add, change (`Some(url)`) or remove (`None`) the link on the
1526    /// selection, or on the whole link at a collapsed caret. One undo step.
1527    pub fn set_link(&mut self, url: Option<&str>, cx: &mut Context<Self>) {
1528        if self.readonly {
1529            return;
1530        }
1531        let collapsed = matches!(
1532            &self.model.selection,
1533            BlockSelection::Text { anchor, head } if anchor == head
1534        );
1535        if collapsed && let Some((block, range, _)) = self.link_range_at_caret() {
1536            let _ = self.model.set_selection(BlockSelection::Text {
1537                anchor: BlockPosition::new(block, range.start),
1538                head: BlockPosition::new(block, range.end),
1539            });
1540        } else if collapsed {
1541            return;
1542        }
1543        let change = match url.map(str::trim).filter(|u| !u.is_empty()) {
1544            Some(url) => super::marks::MarkChange::new().with_link(url.to_string(), None),
1545            None => super::marks::MarkChange::new().clear(MarkKind::Link),
1546        };
1547        self.model.break_coalescing();
1548        let _ = self.run(EditorCommand::ApplyMark(change), cx);
1549        self.model.break_coalescing();
1550    }
1551
1552    /// The task checkbox drawn left of a task item's first line under
1553    /// `at`: the item's text leaf and whether it is checked.
1554    /// Mouse-down on a task checkbox: toggle it, leaving the caret and
1555    /// selection alone. Returns whether it was one.
1556    pub(crate) fn click_checkbox(&mut self, at: Point<Pixels>, cx: &mut Context<Self>) -> bool {
1557        match self.task_checkbox_at(at) {
1558            Some((leaf, checked)) if !self.readonly => {
1559                self.end_composition();
1560                self.dragging = false;
1561                self.set_task_checked(leaf, !checked, cx);
1562                true
1563            }
1564            _ => false,
1565        }
1566    }
1567
1568    pub(crate) fn task_checkbox_at(&self, at: Point<Pixels>) -> Option<(BlockId, bool)> {
1569        use super::kind::BlockKindData;
1570        use gpui::px;
1571        let frame = self.frame.borrow();
1572        frame.leaves.iter().find_map(|leaf| {
1573            let b = leaf.bounds();
1574            let line = px(28.).min(b.size.height);
1575            if !(at.y >= b.top()
1576                && at.y < b.top() + line
1577                && at.x < b.left()
1578                && at.x >= b.left() - px(28.))
1579            {
1580                return None;
1581            }
1582            let block = leaf.info.block;
1583            let (item, index) = self.model.doc.index_in_parent(block)?;
1584            match self.model.doc.kind(item) {
1585                Some(BlockKindData::ListItem {
1586                    checked: Some(c), ..
1587                }) if index == 0 => Some((block, *c)),
1588                _ => None,
1589            }
1590        })
1591    }
1592
1593    /// Mouse: place or extend the caret at a window point.
1594    pub(crate) fn mouse_at(&mut self, at: Point<Pixels>, extend: bool, cx: &mut Context<Self>) {
1595        // Block handle: a click in the left edge beside a table or code
1596        // block (left of its text) selects the block as a node.
1597        if !extend && let Some(block) = self.block_handle_at(at) {
1598            self.goal_x = None;
1599            self.end_composition();
1600            let _ = self.model.set_selection(BlockSelection::Blocks {
1601                anchor: block,
1602                head: block,
1603            });
1604            self.dragging = false;
1605            self.selection_changed(cx);
1606            return;
1607        }
1608        let hit = self.frame.borrow().hit_test(at);
1609        // A click on an inline atom selects the whole atom (#3113).
1610        if !extend && let Some(pos) = hit.and_then(|pos| self.atom_under(pos, at)) {
1611            self.goal_x = None;
1612            self.end_composition();
1613            let _ = self.model.set_selection(BlockSelection::Text {
1614                anchor: pos,
1615                head: BlockPosition::new(
1616                    pos.block,
1617                    pos.offset + super::inline::ATOM_CHAR.len_utf8(),
1618                ),
1619            });
1620            self.selection_changed(cx);
1621            return;
1622        }
1623        if let Some(pos) = hit {
1624            self.goal_x = None;
1625            let pos = self.model.doc.clamp(pos);
1626            self.move_to(pos, extend, cx);
1627        }
1628    }
1629
1630    /// Double-click: the word at the caret; triple-click: the whole block.
1631    pub(crate) fn select_unit(&mut self, block: bool, cx: &mut Context<Self>) {
1632        let Some(caret) = self.model.caret() else {
1633            return;
1634        };
1635        let Some(text) = self.model.doc.leaf_text(caret.block) else {
1636            return;
1637        };
1638        let range = if block {
1639            0..text.len()
1640        } else {
1641            match crate::support::word_range_at(text, caret.offset) {
1642                Some(r) => r,
1643                None => return,
1644            }
1645        };
1646        let _ = self.model.set_selection(BlockSelection::Text {
1647            anchor: BlockPosition::new(caret.block, range.start),
1648            head: BlockPosition::new(caret.block, range.end),
1649        });
1650        self.dragging = false;
1651        self.selection_changed(cx);
1652    }
1653
1654    /// Top-level blocks with laid-out geometry, in document order, with the
1655    /// union of their leaves' bounds (D22).
1656    pub(crate) fn top_level_bounds(&self) -> Vec<(usize, BlockId, Bounds<Pixels>)> {
1657        let doc = &self.model.doc;
1658        let frame = self.frame.borrow();
1659        let children = doc.children(doc.root);
1660        let mut out: Vec<(usize, BlockId, Bounds<Pixels>)> = Vec::new();
1661        for leaf in &frame.leaves {
1662            let path = doc.path(leaf.info.block);
1663            let Some(top) = path.get(1).copied() else {
1664                continue;
1665            };
1666            let Some(ix) = children.iter().position(|c| *c == top) else {
1667                continue;
1668            };
1669            let b = leaf.bounds();
1670            match out.iter_mut().find(|(_, id, _)| *id == top) {
1671                Some(entry) => entry.2 = entry.2.union(&b),
1672                None => out.push((ix, top, b)),
1673            }
1674        }
1675        out.sort_by_key(|(ix, _, _)| *ix);
1676        out
1677    }
1678
1679    /// D22: the top-level table or code block whose edge handle
1680    /// (`block_handle_at`) is under `at`; only those can be dragged.
1681    pub(crate) fn drag_handle_at(&self, at: Point<Pixels>) -> Option<BlockId> {
1682        if self.readonly {
1683            return None;
1684        }
1685        let block = self.block_handle_at(at)?;
1686        let doc = &self.model.doc;
1687        // A code block's handle is its leaf; tables report the table.
1688        let top = doc.path(block).get(1).copied()?;
1689        (top == block).then_some(block)
1690    }
1691
1692    /// D22: the gap between top-level blocks nearest the pointer's `y`.
1693    fn drop_gap_at(&self, y: Pixels) -> Option<usize> {
1694        let blocks = self.top_level_bounds();
1695        let len = self.model.doc.children(self.model.doc.root).len();
1696        if blocks.is_empty() {
1697            return None;
1698        }
1699        Some(
1700            blocks
1701                .iter()
1702                .find(|(_, _, b)| y < b.center().y)
1703                .map_or_else(
1704                    || blocks.last().map_or(len, |(ix, _, _)| ix + 1),
1705                    |(ix, _, _)| *ix,
1706                ),
1707        )
1708    }
1709
1710    /// D22: mouse-down on a handle arms a drag (the click itself is handled
1711    /// as before).
1712    pub(crate) fn arm_block_drag(&mut self, at: Point<Pixels>) {
1713        self.block_drag = self.drag_handle_at(at).map(|block| BlockDrag {
1714            block,
1715            start: at,
1716            active: false,
1717            drop: None,
1718        });
1719    }
1720
1721    /// D22: pointer moved with the button down. Returns whether a block
1722    /// drag owns the move (no text selection then).
1723    pub(crate) fn block_drag_to(&mut self, at: Point<Pixels>, cx: &mut Context<Self>) -> bool {
1724        use gpui::px;
1725        let Some(mut drag) = self.block_drag else {
1726            return false;
1727        };
1728        if !drag.active {
1729            let d = at - drag.start;
1730            if d.x.abs().max(d.y.abs()) < px(4.) {
1731                return false;
1732            }
1733            drag.active = true;
1734            self.dragging = false;
1735            self.end_composition();
1736            let _ = self.model.set_selection(BlockSelection::Blocks {
1737                anchor: drag.block,
1738                head: drag.block,
1739            });
1740            self.selection_changed(cx);
1741        }
1742        drag.drop = self.drop_gap_at(at.y);
1743        self.block_drag = Some(drag);
1744        cx.notify();
1745        true
1746    }
1747
1748    /// D22: button released. Drops an active drag (one undo step).
1749    pub(crate) fn finish_block_drag(&mut self, cx: &mut Context<Self>) {
1750        let Some(drag) = self.block_drag.take() else {
1751            return;
1752        };
1753        if drag.active {
1754            if let Some(to) = drag.drop {
1755                self.model.break_coalescing();
1756                self.edit(
1757                    EditOrigin::Command("move_block_to"),
1758                    |m| m.move_block_to(drag.block, to).map(|_| ()),
1759                    cx,
1760                );
1761                self.model.break_coalescing();
1762            }
1763            cx.notify();
1764        }
1765    }
1766
1767    /// D22: where to paint the drop indicator of an active drag: a line in
1768    /// the gap, spanning the blocks' width.
1769    pub(crate) fn drop_indicator(&self) -> Option<Bounds<Pixels>> {
1770        use gpui::{point, px, size};
1771        let drag = self.block_drag.filter(|d| d.active)?;
1772        let to = drag.drop?;
1773        let blocks = self.top_level_bounds();
1774        let left = blocks
1775            .iter()
1776            .map(|(_, _, b)| b.left())
1777            .reduce(Pixels::min)?;
1778        let right = blocks
1779            .iter()
1780            .map(|(_, _, b)| b.right())
1781            .reduce(Pixels::max)?;
1782        let prev = blocks.iter().rev().find(|(ix, _, _)| *ix < to);
1783        let next = blocks.iter().find(|(ix, _, _)| *ix >= to);
1784        let y = match (prev, next) {
1785            (Some((_, _, a)), Some((_, _, b))) => (a.bottom() + b.top()) / 2.,
1786            (None, Some((_, _, b))) => b.top() - px(3.),
1787            (Some((_, _, a)), None) => a.bottom() + px(3.),
1788            (None, None) => return None,
1789        };
1790        Some(gpui::Bounds::new(
1791            point(left - px(8.), y - px(1.)),
1792            size(right - left + px(8.), px(2.)),
1793        ))
1794    }
1795
1796    /// The table or code block whose left edge (the strip left of its
1797    /// text, inside the block's padding) is under `at`.
1798    pub(crate) fn block_handle_at(&self, at: Point<Pixels>) -> Option<BlockId> {
1799        let frame = self.frame.borrow();
1800        use gpui::px;
1801        let in_edge = |b: &Bounds<Pixels>| {
1802            at.y >= b.top() && at.y < b.bottom() && at.x < b.left() && at.x >= b.left() - px(24.)
1803        };
1804        if let Some((table, _)) = frame.tables.iter().find(|(_, b)| in_edge(b)) {
1805            return Some(*table);
1806        }
1807        frame
1808            .leaves
1809            .iter()
1810            .find(|leaf| self.model.doc.plain(leaf.info.block).is_some() && in_edge(&leaf.bounds()))
1811            .map(|leaf| leaf.info.block)
1812    }
1813
1814    /// The start of the inline atom drawn under the window point `at` in
1815    /// the leaf a hit test found.
1816    fn atom_under(&self, pos: BlockPosition, at: Point<Pixels>) -> Option<BlockPosition> {
1817        let frame = self.frame.borrow();
1818        let range = frame.leaf(pos.block)?.atom_at(at)?;
1819        Some(BlockPosition::new(pos.block, range.start))
1820    }
1821
1822    fn toggle_mark(&mut self, kind: MarkKind, cx: &mut Context<Self>) {
1823        let _ = self.run(EditorCommand::ToggleMark(kind), cx);
1824    }
1825
1826    /// The selection as clipboard text, and whether it is the editor's own
1827    /// inline Markdown (D17): then the copy is tagged with
1828    /// [`COPY_TAG`] so paste parses it back.
1829    fn selected_text(&self) -> (String, CopyKind) {
1830        if let Some(table) = self.model.cells_markdown() {
1831            return (table, CopyKind::Blocks);
1832        }
1833        if let Some((parent, range)) = self.model.selected_blocks() {
1834            let ids = &self.model.doc.children(parent)[range];
1835            return (
1836                super::serialize::join_blocks(&self.model.doc, ids),
1837                CopyKind::Blocks,
1838            );
1839        }
1840        let Ok(range) = self.model.doc.resolve_selection(self.model.selection()) else {
1841            return (String::new(), CopyKind::External);
1842        };
1843        // Several blocks: Markdown for the whole selection (D18).
1844        if range.start.block != range.end.block
1845            && let Some(markdown) = super::slice::selection_markdown(&self.model.doc, range)
1846        {
1847            return (markdown, CopyKind::Blocks);
1848        }
1849        let mut inline_only = true;
1850        let text = self
1851            .model
1852            .text_ranges(range)
1853            .into_iter()
1854            .filter_map(|(id, lo, hi)| {
1855                // Marks and inline atoms copy as Markdown (an image as
1856                // `![alt](url)`, never the U+FFFC placeholder, #3113). A
1857                // partial selection serializes as its own inline, so
1858                // delimiters are always balanced at the cut edges.
1859                if let Some(inline) = self.model.doc.inline(id) {
1860                    return Some(super::serialize::inline_markdown(&inline.slice(lo..hi)));
1861                }
1862                inline_only = false;
1863                let text = self.model.doc.leaf_text(id)?;
1864                text.get(lo..hi).map(str::to_string)
1865            })
1866            .collect::<Vec<_>>()
1867            .join("\n");
1868        (
1869            text,
1870            if inline_only {
1871                CopyKind::Inline
1872            } else {
1873                CopyKind::External
1874            },
1875        )
1876    }
1877
1878    // ---- action handlers --------------------------------------------------
1879
1880    pub(crate) fn on_backspace(&mut self, _: &Backspace, _: &mut Window, cx: &mut Context<Self>) {
1881        self.end_composition();
1882        self.edit(EditOrigin::Backspace, |m| m.delete(false), cx);
1883    }
1884    pub(crate) fn on_delete(&mut self, _: &Delete, _: &mut Window, cx: &mut Context<Self>) {
1885        self.end_composition();
1886        self.edit(EditOrigin::DeleteForward, |m| m.delete(true), cx);
1887    }
1888    pub(crate) fn on_enter(&mut self, _: &Enter, _: &mut Window, cx: &mut Context<Self>) {
1889        if self.readonly || self.is_composing() {
1890            return;
1891        }
1892        if self.submit_policy == SubmitPolicy::Enter {
1893            cx.emit(BlockEditorEvent::SubmitRequested);
1894            return;
1895        }
1896        self.newline(cx);
1897    }
1898    pub(crate) fn on_structural_enter(
1899        &mut self,
1900        _: &StructuralEnter,
1901        _: &mut Window,
1902        cx: &mut Context<Self>,
1903    ) {
1904        if self.readonly || self.is_composing() {
1905            return;
1906        }
1907        self.newline(cx);
1908    }
1909    fn newline(&mut self, cx: &mut Context<Self>) {
1910        self.end_composition();
1911        // "```lang" then Enter: a code block.
1912        if !self.readonly
1913            && self.rule_on(RuleKind::CodeFence)
1914            && let Some(caret) = self.model.caret()
1915            && self.model.doc.kind(caret.block) == Some(&super::kind::BlockKindData::Paragraph)
1916            && caret.offset == self.model.doc.leaf_len(caret.block).unwrap_or(0)
1917            && let Some(lang) = self
1918                .model
1919                .doc
1920                .inline(caret.block)
1921                .and_then(|i| code_fence(i.text()))
1922        {
1923            self.fence_to_code_block(caret.block, lang, cx);
1924            return;
1925        }
1926        // Enter in a table cell (D13).
1927        if !self.readonly && self.model.current_cell().is_some() {
1928            self.model.break_coalescing();
1929            self.edit(
1930                EditOrigin::Command("table_enter"),
1931                |m| m.table_enter().map(|_| ()),
1932                cx,
1933            );
1934            self.model.break_coalescing();
1935            return;
1936        }
1937        // "| a | b |" then Enter at the end of a paragraph: a table.
1938        if !self.readonly
1939            && self.rule_on(RuleKind::Table)
1940            && let Some(caret) = self.model.caret()
1941            && caret.offset == self.model.doc.leaf_len(caret.block).unwrap_or(0)
1942            && self
1943                .model
1944                .doc
1945                .inline(caret.block)
1946                .is_some_and(|i| super::table::table_row_cells(i.text()).is_some())
1947        {
1948            let block = caret.block;
1949            self.model.break_coalescing();
1950            self.edit(
1951                EditOrigin::InputRule("table"),
1952                |m| m.paragraph_to_table(block).map(|_| ()),
1953                cx,
1954            );
1955            self.model.break_coalescing();
1956            return;
1957        }
1958        // "---" then Enter in an otherwise empty paragraph: a rule.
1959        if !self.readonly
1960            && self.rule_on(RuleKind::HorizontalRule)
1961            && let Some(caret) = self.model.caret()
1962            && self.model.doc.kind(caret.block) == Some(&super::kind::BlockKindData::Paragraph)
1963            && self.model.doc.inline(caret.block).map(|i| i.text()) == Some("---")
1964        {
1965            let block = caret.block;
1966            self.model.break_coalescing();
1967            self.model.history.begin_transaction();
1968            let _ = self.model.set_selection(BlockSelection::Text {
1969                anchor: BlockPosition::new(block, 0),
1970                head: BlockPosition::new(block, 3),
1971            });
1972            let _ = self.model.delete(false);
1973            let _ = self.model.paragraph_to_rule();
1974            self.model.history.commit_transaction();
1975            self.model.break_coalescing();
1976            self.after_edit(EditOrigin::InputRule("horizontal_rule"), cx);
1977            return;
1978        }
1979        self.edit(EditOrigin::Command("split_block"), |m| m.split_block(), cx);
1980    }
1981    /// Shift+Enter (Ctrl+Enter in composer mode): a cell soft break;
1982    /// structural Enter elsewhere, without submission.
1983    pub(crate) fn on_soft_break(&mut self, _: &SoftBreak, _: &mut Window, cx: &mut Context<Self>) {
1984        if self.readonly || self.is_composing() {
1985            return;
1986        }
1987        self.end_composition();
1988        if self.model.current_cell().is_some() {
1989            self.model.break_coalescing();
1990            self.edit(
1991                EditOrigin::Command("cell_soft_break"),
1992                |m| m.cell_soft_break().map(|_| ()),
1993                cx,
1994            );
1995            self.model.break_coalescing();
1996        } else {
1997            self.newline(cx);
1998        }
1999    }
2000
2001    /// Cmd/Ctrl+Enter: leave a table to the block after it.
2002    pub(crate) fn on_exit_block(&mut self, _: &ExitBlock, _: &mut Window, cx: &mut Context<Self>) {
2003        if self.readonly || self.is_composing() {
2004            return;
2005        }
2006        if matches!(
2007            self.submit_policy,
2008            SubmitPolicy::Enter | SubmitPolicy::ModifiedEnter
2009        ) {
2010            cx.emit(BlockEditorEvent::SubmitRequested);
2011            return;
2012        }
2013        self.end_composition();
2014        if self.readonly || self.model.current_cell().is_none() {
2015            cx.propagate();
2016            return;
2017        }
2018        self.model.break_coalescing();
2019        self.edit(
2020            EditOrigin::Command("table_exit"),
2021            |m| m.exit_table().map(|_| ()),
2022            cx,
2023        );
2024        self.model.break_coalescing();
2025    }
2026
2027    pub(crate) fn on_move_left(&mut self, _: &MoveLeft, _: &mut Window, cx: &mut Context<Self>) {
2028        self.horizontal(false, false, cx);
2029    }
2030    pub(crate) fn on_move_right(&mut self, _: &MoveRight, _: &mut Window, cx: &mut Context<Self>) {
2031        self.horizontal(true, false, cx);
2032    }
2033    pub(crate) fn on_move_up(&mut self, _: &MoveUp, _: &mut Window, cx: &mut Context<Self>) {
2034        self.vertical(false, false, cx);
2035    }
2036    pub(crate) fn on_move_down(&mut self, _: &MoveDown, _: &mut Window, cx: &mut Context<Self>) {
2037        self.vertical(true, false, cx);
2038    }
2039    pub(crate) fn on_move_home(&mut self, _: &MoveHome, _: &mut Window, cx: &mut Context<Self>) {
2040        self.line_edge(false, false, cx);
2041    }
2042    pub(crate) fn on_move_end(&mut self, _: &MoveEnd, _: &mut Window, cx: &mut Context<Self>) {
2043        self.line_edge(true, false, cx);
2044    }
2045    pub(crate) fn on_select_left(
2046        &mut self,
2047        _: &SelectLeft,
2048        _: &mut Window,
2049        cx: &mut Context<Self>,
2050    ) {
2051        self.horizontal(false, true, cx);
2052    }
2053    pub(crate) fn on_select_right(
2054        &mut self,
2055        _: &SelectRight,
2056        _: &mut Window,
2057        cx: &mut Context<Self>,
2058    ) {
2059        self.horizontal(true, true, cx);
2060    }
2061    pub(crate) fn on_select_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
2062        self.vertical(false, true, cx);
2063    }
2064    pub(crate) fn on_select_down(
2065        &mut self,
2066        _: &SelectDown,
2067        _: &mut Window,
2068        cx: &mut Context<Self>,
2069    ) {
2070        self.vertical(true, true, cx);
2071    }
2072    pub(crate) fn on_select_home(
2073        &mut self,
2074        _: &SelectHome,
2075        _: &mut Window,
2076        cx: &mut Context<Self>,
2077    ) {
2078        self.line_edge(false, true, cx);
2079    }
2080    pub(crate) fn on_select_end(&mut self, _: &SelectEnd, _: &mut Window, cx: &mut Context<Self>) {
2081        self.line_edge(true, true, cx);
2082    }
2083    pub(crate) fn on_move_doc_start(
2084        &mut self,
2085        _: &MoveDocStart,
2086        _: &mut Window,
2087        cx: &mut Context<Self>,
2088    ) {
2089        self.goal_x = None;
2090        self.move_to(self.model.doc.first_caret(), false, cx);
2091    }
2092    pub(crate) fn on_move_doc_end(
2093        &mut self,
2094        _: &MoveDocEnd,
2095        _: &mut Window,
2096        cx: &mut Context<Self>,
2097    ) {
2098        self.goal_x = None;
2099        self.move_to(self.model.doc.last_caret(), false, cx);
2100    }
2101    pub(crate) fn on_select_doc_start(
2102        &mut self,
2103        _: &SelectDocStart,
2104        _: &mut Window,
2105        cx: &mut Context<Self>,
2106    ) {
2107        self.goal_x = None;
2108        self.move_to(self.model.doc.first_caret(), true, cx);
2109    }
2110    pub(crate) fn on_select_doc_end(
2111        &mut self,
2112        _: &SelectDocEnd,
2113        _: &mut Window,
2114        cx: &mut Context<Self>,
2115    ) {
2116        self.goal_x = None;
2117        self.move_to(self.model.doc.last_caret(), true, cx);
2118    }
2119    pub(crate) fn on_select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
2120        self.end_composition();
2121        let _ = self.model.run(EditorCommand::SelectAll);
2122        self.selection_changed(cx);
2123    }
2124    pub(crate) fn on_undo(&mut self, _: &Undo, _: &mut Window, cx: &mut Context<Self>) {
2125        self.end_composition();
2126        self.undo(cx);
2127    }
2128    pub(crate) fn on_redo(&mut self, _: &Redo, _: &mut Window, cx: &mut Context<Self>) {
2129        self.end_composition();
2130        self.redo(cx);
2131    }
2132    pub(crate) fn on_normal_text(
2133        &mut self,
2134        _: &NormalText,
2135        _: &mut Window,
2136        cx: &mut Context<Self>,
2137    ) {
2138        if self.readonly {
2139            return;
2140        }
2141        self.end_composition();
2142        self.model.break_coalescing();
2143        let _ = self.run(EditorCommand::SetBlockKind(BlockKind::Paragraph), cx);
2144        self.model.break_coalescing();
2145    }
2146    pub(crate) fn on_close_table_menu(
2147        &mut self,
2148        _: &CloseTableMenu,
2149        _: &mut Window,
2150        cx: &mut Context<Self>,
2151    ) {
2152        self.close_table_menu(cx);
2153    }
2154    fn table_key(&mut self, edit: Option<crate::TableEdit>, cx: &mut Context<Self>) {
2155        if self.readonly || self.model.current_cell().is_none() {
2156            cx.propagate();
2157            return;
2158        }
2159        self.end_composition();
2160        self.close_table_menu(cx);
2161        if let Some(edit) = edit {
2162            self.model.break_coalescing();
2163            let _ = self.run(EditorCommand::EditTable(edit), cx);
2164            self.model.break_coalescing();
2165        }
2166    }
2167    pub(crate) fn on_table_insert_column_left(
2168        &mut self,
2169        _: &TableInsertColumnLeft,
2170        _: &mut Window,
2171        cx: &mut Context<Self>,
2172    ) {
2173        self.table_key(Some(crate::TableEdit::AddColumnLeft), cx);
2174    }
2175    pub(crate) fn on_table_insert_column_right(
2176        &mut self,
2177        _: &TableInsertColumnRight,
2178        _: &mut Window,
2179        cx: &mut Context<Self>,
2180    ) {
2181        self.table_key(Some(crate::TableEdit::AddColumnRight), cx);
2182    }
2183    pub(crate) fn on_table_insert_row_above(
2184        &mut self,
2185        _: &TableInsertRowAbove,
2186        _: &mut Window,
2187        cx: &mut Context<Self>,
2188    ) {
2189        self.table_key(Some(crate::TableEdit::AddRowAbove), cx);
2190    }
2191    pub(crate) fn on_table_insert_row_below(
2192        &mut self,
2193        _: &TableInsertRowBelow,
2194        _: &mut Window,
2195        cx: &mut Context<Self>,
2196    ) {
2197        self.table_key(Some(crate::TableEdit::AddRowBelow), cx);
2198    }
2199    /// Cmd/Ctrl+Alt+Backspace: whole column(s) selected → delete them;
2200    /// otherwise delete the row(s) of the caret / cell selection, unless
2201    /// that includes the header row (then nothing happens).
2202    pub(crate) fn on_table_delete_line(
2203        &mut self,
2204        _: &TableDeleteLine,
2205        _: &mut Window,
2206        cx: &mut Context<Self>,
2207    ) {
2208        let edit = self.table_delete_edit();
2209        self.table_key(edit, cx);
2210    }
2211    /// What Cmd/Ctrl+Alt+Backspace does here (`None`: nothing).
2212    pub fn table_delete_edit(&self) -> Option<crate::TableEdit> {
2213        if self.model.whole_columns_selected() {
2214            return Some(crate::TableEdit::RemoveColumn);
2215        }
2216        let (_, rows, _) = self.model.table_span()?;
2217        (*rows.start() > 0).then_some(crate::TableEdit::RemoveRow)
2218    }
2219    pub(crate) fn on_toggle_bold(
2220        &mut self,
2221        _: &ToggleBold,
2222        _: &mut Window,
2223        cx: &mut Context<Self>,
2224    ) {
2225        self.toggle_mark(MarkKind::Bold, cx);
2226    }
2227    pub(crate) fn on_toggle_italic(
2228        &mut self,
2229        _: &ToggleItalic,
2230        _: &mut Window,
2231        cx: &mut Context<Self>,
2232    ) {
2233        self.toggle_mark(MarkKind::Italic, cx);
2234    }
2235    pub(crate) fn on_toggle_code(
2236        &mut self,
2237        _: &ToggleCode,
2238        _: &mut Window,
2239        cx: &mut Context<Self>,
2240    ) {
2241        self.toggle_mark(MarkKind::Code, cx);
2242    }
2243    pub(crate) fn on_indent(&mut self, _: &Indent, _: &mut Window, cx: &mut Context<Self>) {
2244        let _ = self.run(EditorCommand::Indent, cx);
2245    }
2246    pub(crate) fn on_outdent(&mut self, _: &Outdent, _: &mut Window, cx: &mut Context<Self>) {
2247        let _ = self.run(EditorCommand::Outdent, cx);
2248    }
2249    pub(crate) fn on_copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
2250        let (text, kind) = self.selected_text();
2251        if text.is_empty() {
2252            return;
2253        }
2254        cx.write_to_clipboard(match kind.tag() {
2255            Some(tag) => ClipboardItem::new_string_with_metadata(text, tag.to_string()),
2256            None => ClipboardItem::new_string(text),
2257        });
2258    }
2259    pub(crate) fn on_cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
2260        self.end_composition();
2261        self.on_copy(&Copy, window, cx);
2262        self.edit(
2263            EditOrigin::Cut,
2264            |m| m.run(EditorCommand::DeleteSelection).map(|_| ()),
2265            cx,
2266        );
2267    }
2268    pub(crate) fn on_paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {
2269        if self.readonly {
2270            return;
2271        }
2272        if let Some(item) = cx.read_from_clipboard() {
2273            self.paste(item, false, cx);
2274        }
2275    }
2276    pub(crate) fn on_native_paste(
2277        &mut self,
2278        _: &gpui_kit::base::input::Paste,
2279        window: &mut Window,
2280        cx: &mut Context<Self>,
2281    ) {
2282        self.on_paste(&Paste, window, cx);
2283    }
2284    pub(crate) fn on_paste_plain(
2285        &mut self,
2286        _: &PastePlain,
2287        _: &mut Window,
2288        cx: &mut Context<Self>,
2289    ) {
2290        if self.readonly {
2291            return;
2292        }
2293        if let Some(item) = cx.read_from_clipboard() {
2294            self.paste(item, true, cx);
2295        }
2296    }
2297
2298    /// Deliver semantic clipboard content. `plain` bypasses Markdown import.
2299    /// With Host policy this emits one request; there is no fallback insertion
2300    /// until the owner resolves it, so mixed image/text entries cannot double
2301    /// insert. This method itself never reads the system clipboard.
2302    pub fn paste(&mut self, item: ClipboardItem, plain: bool, cx: &mut Context<Self>) {
2303        if self.readonly {
2304            return;
2305        }
2306        self.end_composition();
2307        if self.clipboard_policy == ClipboardPolicy::Host {
2308            self.paste_sequence += 1;
2309            self.pending_paste = Some(self.paste_sequence);
2310            let request = PasteRequest {
2311                range: self.model.capture(self.model.selection.clone()),
2312                plain,
2313                copy: CopyKind::of(&item),
2314                sequence: self.paste_sequence,
2315            };
2316            cx.emit(BlockEditorEvent::PasteRequested { request, item });
2317        } else if let Some(text) = item.text() {
2318            self.paste_text(&text, plain, CopyKind::of(&item), cx);
2319        }
2320    }
2321
2322    /// Resolve an intercepted paste with text not consumed by the host's other
2323    /// admissions (e.g. image attachments). `None` means wholly handled or
2324    /// rejected. Returns false when navigation/edits made the request stale.
2325    pub fn is_paste_current(&self, request: &PasteRequest) -> bool {
2326        !self.readonly
2327            && self.pending_paste == Some(request.sequence)
2328            && self.model.is_capture_current(&request.range)
2329    }
2330    pub fn apply_paste(
2331        &mut self,
2332        request: &PasteRequest,
2333        text: Option<&str>,
2334        cx: &mut Context<Self>,
2335    ) -> bool {
2336        if !self.is_paste_current(request) {
2337            return false;
2338        }
2339        self.pending_paste = None;
2340        if let Some(text) = text {
2341            self.paste_text(text, request.plain, request.copy, cx);
2342        }
2343        true
2344    }
2345    fn paste_text(&mut self, text: &str, plain: bool, copy: CopyKind, cx: &mut Context<Self>) {
2346        self.edit(
2347            EditOrigin::Paste,
2348            |model| {
2349                if !plain && copy == CopyKind::Blocks && model.paste_blocks_markdown(text)? {
2350                    return Ok(());
2351                }
2352                if !plain && copy != CopyKind::External && model.paste_inline_markdown(text)? {
2353                    return Ok(());
2354                }
2355                if !plain && model.paste_blocks(text)? {
2356                    return Ok(());
2357                }
2358                model.insert_text(text, EditOrigin::Paste)
2359            },
2360            cx,
2361        );
2362    }
2363
2364    /// Markdown input rules (design §5.3): `# `..`###### `, `- `/`* `/`+ `
2365    /// and `1. ` at the start of a plain paragraph. The conversion is its own
2366    /// undo group after the typing, so one undo reverts it and keeps the
2367    /// typed text.
2368    fn apply_input_rule(&mut self, cx: &mut Context<Self>) {
2369        use super::kind::BlockKindData;
2370        let Some(caret) = self.model.caret() else {
2371            return;
2372        };
2373        let doc = &self.model.doc;
2374        if doc.kind(caret.block) != Some(&BlockKindData::Paragraph) {
2375            return;
2376        }
2377        // "```lang " typed into an otherwise empty paragraph.
2378        if self.rule_on(RuleKind::CodeFence)
2379            && let Some(lang) = doc
2380                .inline(caret.block)
2381                .and_then(|i| code_fence(i.text().strip_suffix(' ')?))
2382                .filter(|_| caret.offset == doc.leaf_len(caret.block).unwrap_or(0))
2383        {
2384            self.fence_to_code_block(caret.block, lang, cx);
2385            return;
2386        }
2387        let in_list = doc
2388            .parent(caret.block)
2389            .and_then(|parent| doc.kind(parent))
2390            .is_some_and(|kind| matches!(kind, BlockKindData::ListItem { .. }));
2391        let Some(text) = doc.inline(caret.block).map(|i| i.text().to_string()) else {
2392            return;
2393        };
2394        let prefix = &text[..caret.offset.min(text.len())];
2395        let Some(marker) = prefix.strip_suffix(' ') else {
2396            return;
2397        };
2398        let task = match marker {
2399            "[ ]" => Some(false),
2400            "[x]" | "[X]" => Some(true),
2401            _ => None,
2402        };
2403        let (commands, rule) = if self.rule_on(RuleKind::Heading)
2404            && !marker.is_empty()
2405            && marker.len() <= 6
2406            && marker.bytes().all(|b| b == b'#')
2407        {
2408            let level = marker.len() as u8;
2409            (
2410                vec![EditorCommand::SetBlockKind(BlockKind::Heading { level })],
2411                "heading",
2412            )
2413        } else if self.rule_on(RuleKind::BulletList)
2414            && !in_list
2415            && matches!(marker, "-" | "*" | "+")
2416        {
2417            (
2418                vec![EditorCommand::ToggleList { ordered: false }],
2419                "bullet_list",
2420            )
2421        } else if self.rule_on(RuleKind::OrderedList)
2422            && !in_list
2423            && marker.len() >= 2
2424            && marker.ends_with('.')
2425            && marker[..marker.len() - 1]
2426                .bytes()
2427                .all(|b| b.is_ascii_digit())
2428        {
2429            (
2430                vec![EditorCommand::ToggleList { ordered: true }],
2431                "ordered_list",
2432            )
2433        } else if self.rule_on(RuleKind::TaskItem)
2434            && let Some(checked) = task
2435        {
2436            let mut commands = Vec::new();
2437            if !in_list {
2438                commands.push(EditorCommand::ToggleList { ordered: false });
2439            }
2440            commands.push(EditorCommand::SetTaskChecked(checked));
2441            (commands, "task_item")
2442        } else if self.rule_on(RuleKind::Blockquote) && marker == ">" {
2443            (Vec::new(), "blockquote")
2444        } else {
2445            return;
2446        };
2447        let block = caret.block;
2448        let len = prefix.len();
2449        self.model.break_coalescing();
2450        self.model.history.begin_transaction();
2451        let _ = self.model.set_selection(BlockSelection::Text {
2452            anchor: BlockPosition::new(block, 0),
2453            head: BlockPosition::new(block, len),
2454        });
2455        let _ = self.model.delete(false);
2456        if commands.is_empty() {
2457            let _ = self.model.wrap_in_blockquote();
2458        }
2459        for command in commands {
2460            let _ = self.model.run(command);
2461        }
2462        self.model.history.commit_transaction();
2463        self.model.break_coalescing();
2464        self.after_edit(EditOrigin::InputRule(rule), cx);
2465    }
2466
2467    /// Link input rule: `[text](url)` typed, on the closing `)`, becomes
2468    /// `text` with a link mark. One undo group: undo restores the source.
2469    fn apply_link_rule(&mut self, cx: &mut Context<Self>) {
2470        let Some(caret) = self.model.caret() else {
2471            return;
2472        };
2473        let Some(inline) = self.model.doc.inline(caret.block) else {
2474            return;
2475        };
2476        let Some((start, text, url)) = link_rule(inline.text(), caret.offset) else {
2477            return;
2478        };
2479        let block = caret.block;
2480        let pos = |offset| BlockPosition::new(block, offset);
2481        let text_end = start + 1 + text.len();
2482        self.model.break_coalescing();
2483        self.model.history.begin_transaction();
2484        // Drop "](url)" then "[".
2485        let _ = self.model.set_selection(BlockSelection::Text {
2486            anchor: pos(text_end),
2487            head: pos(caret.offset),
2488        });
2489        let _ = self.model.delete(false);
2490        let _ = self.model.set_selection(BlockSelection::Text {
2491            anchor: pos(start),
2492            head: pos(start + 1),
2493        });
2494        let _ = self.model.delete(false);
2495        let end = start + text.len();
2496        let _ = self.model.set_selection(BlockSelection::Text {
2497            anchor: pos(start),
2498            head: pos(end),
2499        });
2500        let _ = self.model.run(EditorCommand::ApplyMark(
2501            super::marks::MarkChange::new().with_link(url, None),
2502        ));
2503        let _ = self.model.set_selection(BlockSelection::caret(pos(end)));
2504        let after = super::marks::MarkChange::new()
2505            .clear(super::marks::MarkKind::Link)
2506            .apply(&self.model.active_marks().0);
2507        self.model.history.commit_transaction();
2508        self.model.break_coalescing();
2509        self.model.stored_marks = Some(after);
2510        self.after_edit(EditOrigin::InputRule("link"), cx);
2511    }
2512
2513    /// Inline-mark input rules: a closing `` ` ``, `*`, `**` or `~~` typed
2514    /// after an opening one in the same leaf turns the text between into
2515    /// code, italic, bold or strikethrough and drops the delimiters. One undo
2516    /// group of its own: undo restores the literal delimiters.
2517    fn apply_mark_rule(&mut self, cx: &mut Context<Self>) {
2518        let Some(caret) = self.model.caret() else {
2519            return;
2520        };
2521        let Some(inline) = self.model.doc.inline(caret.block) else {
2522            return;
2523        };
2524        let Some((open, close, kind)) = mark_rule(inline, caret.offset) else {
2525            return;
2526        };
2527        let block = caret.block;
2528        let d = close.len();
2529        let pos = |offset| BlockPosition::new(block, offset);
2530        self.model.break_coalescing();
2531        self.model.history.begin_transaction();
2532        let _ = self.model.set_selection(BlockSelection::Text {
2533            anchor: pos(close.start),
2534            head: pos(close.end),
2535        });
2536        let _ = self.model.delete(false);
2537        let _ = self.model.set_selection(BlockSelection::Text {
2538            anchor: pos(open.start),
2539            head: pos(open.end),
2540        });
2541        let _ = self.model.delete(false);
2542        let end = close.start - d;
2543        let _ = self.model.set_selection(BlockSelection::Text {
2544            anchor: pos(open.start),
2545            head: pos(end),
2546        });
2547        let _ = self.model.run(EditorCommand::ApplyMark(
2548            super::marks::MarkChange::new().set(kind),
2549        ));
2550        let _ = self.model.set_selection(BlockSelection::caret(pos(end)));
2551        // Text typed next is outside the new span.
2552        let after = super::marks::MarkChange::new()
2553            .clear(kind)
2554            .apply(&self.model.active_marks().0);
2555        self.model.history.commit_transaction();
2556        self.model.break_coalescing();
2557        self.model.stored_marks = Some(after);
2558        self.after_edit(EditOrigin::InputRule("inline_mark"), cx);
2559    }
2560
2561    /// Replace a typed code fence with an empty code block, caret inside,
2562    /// as its own undo group (one undo restores the typed fence).
2563    fn fence_to_code_block(&mut self, block: BlockId, lang: String, cx: &mut Context<Self>) {
2564        let len = self.model.doc.leaf_len(block).unwrap_or(0);
2565        self.model.break_coalescing();
2566        self.model.history.begin_transaction();
2567        let _ = self.model.set_selection(BlockSelection::Text {
2568            anchor: BlockPosition::new(block, 0),
2569            head: BlockPosition::new(block, len),
2570        });
2571        let _ = self.model.delete(false);
2572        let (lang, meta) = lang
2573            .split_once(char::is_whitespace)
2574            .map_or((lang.as_str(), None), |(lang, meta)| {
2575                (lang, Some(meta.trim_start().to_owned()))
2576            });
2577        let _ = self.model.set_code_block_info(Some(lang.to_owned()), meta);
2578        self.model.history.commit_transaction();
2579        self.model.break_coalescing();
2580        self.after_edit(EditOrigin::InputRule("code_block"), cx);
2581    }
2582
2583    // ---- IME ---------------------------------------------------------------
2584
2585    /// The IME sees the caret leaf as its document.
2586    fn ime_block(&self) -> Option<BlockId> {
2587        if let Some(m) = &self.marked {
2588            return Some(m.block);
2589        }
2590        let (_, head) = self.head_anchor()?;
2591        Some(head.block)
2592    }
2593
2594    fn ime_text(&self) -> &str {
2595        self.ime_block()
2596            .and_then(|id| self.model.doc.leaf_text(id))
2597            .unwrap_or("")
2598    }
2599
2600    fn select_utf16(&mut self, block: BlockId, range16: Range<usize>) {
2601        let text = self.ime_text();
2602        let range = utf16_to_byte(text, range16.start)..utf16_to_byte(text, range16.end);
2603        let _ = self.model.set_selection(BlockSelection::Text {
2604            anchor: BlockPosition::new(block, range.start),
2605            head: BlockPosition::new(block, range.end),
2606        });
2607    }
2608
2609    fn select_marked_or(&mut self, range16: Option<Range<usize>>) {
2610        let Some(block) = self.ime_block() else {
2611            return;
2612        };
2613        if let Some(range16) = range16 {
2614            self.select_utf16(block, range16);
2615        } else if let Some(m) = self.marked.clone() {
2616            let _ = self.model.set_selection(BlockSelection::Text {
2617                anchor: BlockPosition::new(m.block, m.range.start),
2618                head: BlockPosition::new(m.block, m.range.end),
2619            });
2620        }
2621    }
2622}
2623
2624fn command_name(command: &EditorCommand) -> &'static str {
2625    match command {
2626        EditorCommand::ToggleMark(_) => "toggle_mark",
2627        EditorCommand::ApplyMark(_) => "apply_mark",
2628        EditorCommand::SetBlockKind(_) => "set_block_kind",
2629        EditorCommand::ToggleList { .. } => "toggle_list",
2630        EditorCommand::ToggleTask => "toggle_task",
2631        EditorCommand::SetTaskChecked(_) => "set_task_checked",
2632        EditorCommand::Indent => "indent",
2633        EditorCommand::Outdent => "outdent",
2634        EditorCommand::MoveBlocks { .. } => "move_blocks",
2635        EditorCommand::InsertBlock(_) => "insert_block",
2636        EditorCommand::SelectAll => "select_all",
2637        EditorCommand::DeleteSelection => "delete_selection",
2638        EditorCommand::SetCodeBlock { .. } => "code_block",
2639        EditorCommand::InsertTable { .. } => "insert_table",
2640        EditorCommand::EditTable(_) => "edit_table",
2641    }
2642}
2643
2644pub(crate) fn utf16_to_byte(text: &str, utf16: usize) -> usize {
2645    let mut count = 0;
2646    for (ix, ch) in text.char_indices() {
2647        if count >= utf16 {
2648            return ix;
2649        }
2650        count += ch.len_utf16();
2651    }
2652    text.len()
2653}
2654
2655pub(crate) fn byte_to_utf16(text: &str, byte: usize) -> usize {
2656    text.get(..byte.min(text.len()))
2657        .map(|s| s.encode_utf16().count())
2658        .unwrap_or(0)
2659}
2660
2661impl EntityInputHandler for BlockEditorState {
2662    fn text_for_range(
2663        &mut self,
2664        range16: Range<usize>,
2665        adjusted: &mut Option<Range<usize>>,
2666        _: &mut Window,
2667        _: &mut Context<Self>,
2668    ) -> Option<String> {
2669        let text = self.ime_text();
2670        let range = utf16_to_byte(text, range16.start)..utf16_to_byte(text, range16.end);
2671        adjusted.replace(byte_to_utf16(text, range.start)..byte_to_utf16(text, range.end));
2672        text.get(range).map(str::to_string)
2673    }
2674
2675    fn selected_text_range(
2676        &mut self,
2677        _: bool,
2678        _: &mut Window,
2679        _: &mut Context<Self>,
2680    ) -> Option<UTF16Selection> {
2681        let (anchor, head) = self.head_anchor()?;
2682        let text = self.ime_text();
2683        // Cross-block selections are reported as the caret leaf's part.
2684        let in_leaf = |p: BlockPosition| {
2685            if p.block == head.block {
2686                p.offset
2687            } else if self.model.doc.cmp_positions(p, head).ok() == Some(std::cmp::Ordering::Less) {
2688                0
2689            } else {
2690                text.len()
2691            }
2692        };
2693        let (a, h) = (in_leaf(anchor), head.offset);
2694        Some(UTF16Selection {
2695            range: byte_to_utf16(text, a.min(h))..byte_to_utf16(text, a.max(h)),
2696            reversed: h < a,
2697        })
2698    }
2699
2700    fn marked_text_range(&self, _: &mut Window, _: &mut Context<Self>) -> Option<Range<usize>> {
2701        let m = self.marked.as_ref()?;
2702        let text = self.model.doc.leaf_text(m.block)?;
2703        Some(byte_to_utf16(text, m.range.start)..byte_to_utf16(text, m.range.end))
2704    }
2705
2706    fn unmark_text(&mut self, _: &mut Window, _: &mut Context<Self>) {
2707        self.end_composition();
2708    }
2709
2710    fn paste(&mut self, item: ClipboardItem, _: &mut Window, cx: &mut Context<Self>) {
2711        BlockEditorState::paste(self, item, false, cx);
2712    }
2713
2714    fn replace_text_in_range(
2715        &mut self,
2716        range16: Option<Range<usize>>,
2717        text: &str,
2718        _: &mut Window,
2719        cx: &mut Context<Self>,
2720    ) {
2721        if self.readonly {
2722            return;
2723        }
2724        let composing = self.marked.is_some();
2725        self.select_marked_or(range16);
2726        let origin = if composing {
2727            EditOrigin::Ime
2728        } else {
2729            EditOrigin::Typing
2730        };
2731        self.edit(origin, |m| m.insert_text(text, origin), cx);
2732        self.end_composition();
2733        if text == " " {
2734            self.apply_input_rule(cx);
2735        } else if text.ends_with(['`', '*', '~']) && self.rule_on(RuleKind::InlineMarks) {
2736            self.apply_mark_rule(cx);
2737        } else if text.ends_with(')') && self.rule_on(RuleKind::Link) {
2738            self.apply_link_rule(cx);
2739        }
2740    }
2741
2742    fn replace_and_mark_text_in_range(
2743        &mut self,
2744        range16: Option<Range<usize>>,
2745        new_text: &str,
2746        new_selected16: Option<Range<usize>>,
2747        _: &mut Window,
2748        cx: &mut Context<Self>,
2749    ) {
2750        if self.readonly {
2751            return;
2752        }
2753        if new_text.is_empty() {
2754            // Cancelled composition: remove the marked text, close the group.
2755            if let Some(marked) = self.marked.clone() {
2756                self.select_marked_or(None);
2757                if !marked.range.is_empty() {
2758                    self.edit(EditOrigin::Ime, |m| m.delete(false), cx);
2759                }
2760            }
2761            self.end_composition();
2762            return;
2763        }
2764        self.select_marked_or(range16);
2765        let start = match self.model.doc.resolve_selection(self.model.selection()) {
2766            Ok(range) => range.start,
2767            Err(_) => return,
2768        };
2769        if !self.composition_open {
2770            // The whole composition is one undo step. Opened only once
2771            // something is about to be inserted; always closed by
2772            // `end_composition`.
2773            self.model.history.begin_transaction();
2774            self.composition_open = true;
2775        }
2776        let revision = self.model.revision();
2777        self.edit(
2778            EditOrigin::Ime,
2779            |m| m.insert_text(new_text, EditOrigin::Ime),
2780            cx,
2781        );
2782        let inserted = self.model.revision() != revision;
2783        let Some((_, head)) = self.head_anchor().filter(|_| inserted) else {
2784            // Nothing was inserted (e.g. into a code block): close the group.
2785            self.end_composition();
2786            return;
2787        };
2788        let block = head.block;
2789        let begin = if block == start.block {
2790            start.offset
2791        } else {
2792            head.offset.saturating_sub(new_text.len())
2793        };
2794        let range = begin..begin + new_text.len();
2795        self.marked = Some(Marked {
2796            block,
2797            range: range.clone(),
2798        });
2799        if let Some(sel16) = new_selected16 {
2800            let sel = utf16_to_byte(new_text, sel16.start)..utf16_to_byte(new_text, sel16.end);
2801            let _ = self.model.set_selection(BlockSelection::Text {
2802                anchor: BlockPosition::new(block, range.start + sel.start),
2803                head: BlockPosition::new(block, range.start + sel.end),
2804            });
2805        }
2806    }
2807
2808    fn bounds_for_range(
2809        &mut self,
2810        range16: Range<usize>,
2811        _: Bounds<Pixels>,
2812        _: &mut Window,
2813        _: &mut Context<Self>,
2814    ) -> Option<Bounds<Pixels>> {
2815        let block = self.ime_block()?;
2816        let text = self.ime_text();
2817        let start = utf16_to_byte(text, range16.start);
2818        let end = utf16_to_byte(text, range16.end);
2819        let frame = self.frame.borrow();
2820        let width = self.style.caret_width();
2821        let a = frame.caret_bounds(BlockPosition::new(block, start), width)?;
2822        let b = frame.caret_bounds(BlockPosition::new(block, end), width)?;
2823        Some(Bounds::from_corners(
2824            a.origin,
2825            gpui::point(
2826                b.origin.x.max(a.origin.x + width),
2827                a.origin.y + a.size.height,
2828            ),
2829        ))
2830    }
2831
2832    fn character_index_for_point(
2833        &mut self,
2834        at: Point<Pixels>,
2835        _: &mut Window,
2836        _: &mut Context<Self>,
2837    ) -> Option<usize> {
2838        let pos = self.frame.borrow().hit_test(at)?;
2839        if Some(pos.block) != self.ime_block() {
2840            return None;
2841        }
2842        Some(byte_to_utf16(self.ime_text(), pos.offset))
2843    }
2844}
2845
2846/// The byte range of the contiguous run linking to `url` around `offset`.
2847fn link_extent(
2848    inline: &super::inline::InlineText,
2849    offset: usize,
2850    url: &SharedString,
2851) -> Option<Range<usize>> {
2852    let pieces: Vec<_> = inline
2853        .pieces()
2854        .map(|(range, marks, _)| (range, marks.link.as_ref().map(|l| l.url.clone())))
2855        .collect();
2856    let ix = pieces
2857        .iter()
2858        .position(|(r, _)| r.start <= offset && offset < r.end)?;
2859    let same = |i: usize| pieces[i].1.as_ref() == Some(url);
2860    let (mut lo, mut hi) = (ix, ix);
2861    while lo > 0 && same(lo - 1) {
2862        lo -= 1;
2863    }
2864    while hi + 1 < pieces.len() && same(hi + 1) {
2865        hi += 1;
2866    }
2867    Some(pieces[lo].0.start..pieces[hi].0.end)
2868}
2869
2870/// `[text](url)` ending at `caret`: the `[` offset, the text and the url.
2871fn link_rule(text: &str, caret: usize) -> Option<(usize, String, String)> {
2872    let before = text.get(..caret)?.strip_suffix(')')?;
2873    let open_paren = before.rfind("](")?;
2874    let url = &before[open_paren + 2..];
2875    if url.is_empty() || url.contains(char::is_whitespace) {
2876        return None;
2877    }
2878    let head = &before[..open_paren];
2879    let open = head.rfind('[')?;
2880    let label = &head[open + 1..];
2881    if label.is_empty() || label.contains(['[', ']']) {
2882        return None;
2883    }
2884    Some((open, label.to_string(), url.to_string()))
2885}
2886
2887/// The language of a typed code fence (```` ``` ```` plus an optional word).
2888fn code_fence(text: &str) -> Option<String> {
2889    let lang = text.strip_prefix("```")?;
2890    lang.chars()
2891        .all(|c| c.is_alphanumeric() || matches!(c, '+' | '-' | '_' | '#' | '.'))
2892        .then(|| lang.to_string())
2893}
2894
2895/// The `(opening delimiter, closing delimiter, mark)` of an inline-mark input
2896/// rule whose closing delimiter ends at `caret`, if any. Nothing fires inside
2897/// inline code, across an atom, or around empty / space-padded content.
2898fn mark_rule(
2899    inline: &super::inline::InlineText,
2900    caret: usize,
2901) -> Option<(Range<usize>, Range<usize>, MarkKind)> {
2902    let text = inline.text();
2903    let prefix = text.get(..caret)?;
2904    let (delim, kind) = if prefix.ends_with('`') {
2905        ("`", MarkKind::Code)
2906    } else if prefix.ends_with("~~") {
2907        ("~~", MarkKind::Strikethrough)
2908    } else if prefix.ends_with("**") {
2909        ("**", MarkKind::Bold)
2910    } else if prefix.ends_with('*') {
2911        ("*", MarkKind::Italic)
2912    } else {
2913        return None;
2914    };
2915    let close = caret - delim.len()..caret;
2916    let before = &prefix[..close.start];
2917    let start = before.rfind(delim)?;
2918    // `*` must not be half of a `**`.
2919    if delim == "*" && (before[..start].ends_with('*') || before.ends_with('*')) {
2920        return None;
2921    }
2922    let open = start..start + delim.len();
2923    let content = &text[open.end..close.start];
2924    if content.is_empty()
2925        || content.contains(super::inline::ATOM_CHAR)
2926        || (kind != MarkKind::Code
2927            && (content.starts_with(char::is_whitespace) || content.ends_with(char::is_whitespace)))
2928    {
2929        return None;
2930    }
2931    let in_code = inline
2932        .pieces()
2933        .any(|(r, m, _)| m.code && r.start < close.end && r.end > open.start);
2934    (!in_code).then_some((open, close, kind))
2935}