Skip to main content

editor/editor/
mode.rs

1//! Source mode and the switch between it and blocks.
2
3use super::*;
4
5impl Editor {
6    /// The document as markdown. In [`Mode::Blocks`] it is the text the
7    /// document was read from, with every block edited since written again
8    /// in normalized form. In [`Mode::Source`] it is the text being edited,
9    /// exactly as it stands.
10    pub fn source(&self) -> String {
11        source(&self.doc, self.mode, &self.synced)
12    }
13
14    /// Which form the document is being edited in.
15    pub fn mode(&self) -> Mode {
16        self.mode
17    }
18
19    /// What a toolbar needs to light itself. See [`Formatting`].
20    pub fn formatting(&self) -> Formatting {
21        let at = self.cursor();
22        let marks = if self.blocks() {
23            let mut marks = self.doc.marks(self.selection);
24            // A stored mark is one cmd-B has already taken and nothing has
25            // spent yet, so the button that took it stays lit.
26            for mark in &self.stored {
27                if !marks.contains(mark) {
28                    marks.push(mark.clone());
29                }
30            }
31            marks
32        } else {
33            Vec::new()
34        };
35        Formatting {
36            mode: self.mode,
37            marks,
38            block: self
39                .doc
40                .blocks
41                .get(at.block)
42                .and_then(|block| crate::slash::label(&block.kind)),
43            quote: self.doc.blocks.get(at.block).and_then(|block| block.quote),
44            fenceable: self.blocks() && fenceable(&self.doc, self.selection),
45        }
46    }
47
48    /// Switch between the document and its markdown, carrying the caret across.
49    /// A caret with no place in the other form lands at the end of the source,
50    /// or at the start of the document.
51    ///
52    /// One undo step, and one the history knows the mode of: stepping back
53    /// over a switch puts the document back in the form it was edited in.
54    ///
55    /// Anchors cross with it, into the coordinates of the form switched to. One
56    /// with an end that cannot be placed there comes out detached.
57    pub fn set_mode(&mut self, mode: Mode, cx: &mut Context<Self>) {
58        if mode == self.mode {
59            return;
60        }
61        let (was, selection) = (self.mode, self.selection);
62        self.document.update(cx, |document, _| {
63            document
64                .history
65                .record(false, was, &self.doc, selection, &self.anchors)
66        });
67        self.dismiss_menus();
68        self.switch(mode);
69        self.run.landed(EditKind::Structure, self.selection);
70        self.reveal = Some(Reveal::Nearest);
71        self.caret_moved();
72        self.publish(Change::Replaced, cx);
73        cx.emit(EditorEvent::ModeChanged(mode));
74        cx.emit(EditorEvent::Changed);
75        cx.notify();
76    }
77
78    /// Turn the document into the other form, caret and all. The half of
79    /// [`Self::set_mode`] that needs no window.
80    pub(super) fn switch(&mut self, mode: Mode) {
81        let caret = self.cursor();
82        let caret = switch(
83            &mut self.doc,
84            &mut self.synced.borrow_mut(),
85            &mut self.anchors,
86            mode,
87            caret,
88        );
89        self.selection = Selection::at(caret);
90        self.mode = mode;
91    }
92
93    /// [`Mode::Source`] if the document is in blocks, and back again — what a
94    /// toggle in the app's own chrome calls.
95    pub fn toggle_source(&mut self, cx: &mut Context<Self>) {
96        self.set_mode(
97            match self.mode {
98                Mode::Blocks => Mode::Source,
99                Mode::Source => Mode::Blocks,
100            },
101            cx,
102        );
103    }
104
105    /// Whether the document is the thing being edited, rather than its source.
106    ///
107    /// Every operation that acts on *blocks* asks this: in source mode there is
108    /// one block, it is a fence holding a string, and turning it into a heading
109    /// or dragging it somewhere would edit the markup rather than the document
110    /// the markup spells.
111    pub(super) fn blocks(&self) -> bool {
112        self.mode == Mode::Blocks
113    }
114
115    /// Bring the source view's text up to the document. A first build
116    /// highlights in place; an edit after it is highlighted on the background
117    /// executor, and the view shows the spans from before it until then.
118    /// A highlighter installed since the last highlight is run the same way.
119    pub(super) fn refresh_source_view(&mut self, cx: &mut Context<Self>) {
120        let revision = self.doc.revision();
121        let job = match &mut self.source_view {
122            Some((built, view)) => {
123                let edited = match *built == revision {
124                    true => None,
125                    false => {
126                        *built = revision;
127                        view.edit(source_of(&self.doc), cx)
128                    }
129                };
130                edited.or_else(|| view.rehighlight(cx))
131            }
132            None => {
133                let view = markdown::SourceText::new(source_of(&self.doc), cx);
134                self.source_view = Some((revision, view));
135                None
136            }
137        };
138        let Some(job) = job else { return };
139        self.source_spans = Some(cx.spawn(async move |editor, cx| {
140            let spans = cx
141                .background_executor()
142                .spawn(async move { job.run() })
143                .await;
144            editor
145                .update(cx, |editor, cx| {
146                    if let Some((_, view)) = &mut editor.source_view
147                        && view.set_spans(spans)
148                    {
149                        cx.notify();
150                    }
151                })
152                .ok();
153        }));
154    }
155
156    /// Drop the source view and any highlighting under way for it.
157    pub(super) fn drop_source_view(&mut self) {
158        self.source_view = None;
159        self.source_spans = None;
160    }
161
162    /// The source view's text. Current only after [`Self::refresh_source_view`].
163    pub(super) fn source_view(&self) -> &markdown::SourceText {
164        &self
165            .source_view
166            .as_ref()
167            .expect("render refreshes the source view before reading it")
168            .1
169    }
170
171    /// Lines in the source, counting the empty one after a trailing newline.
172    /// Only meaningful in [`Mode::Source`], as [`Self::source_text`] is.
173    pub fn source_lines(&self) -> usize {
174        match &self.source_view {
175            Some((built, view)) if *built == self.doc.revision() => view.lines(),
176            _ => {
177                self.source_text()
178                    .bytes()
179                    .filter(|byte| *byte == b'\n')
180                    .count()
181                    + 1
182            }
183        }
184    }
185
186    /// The text of the fence the source is held in — what is being edited in
187    /// [`Mode::Source`]. Only meaningful there; in [`Mode::Blocks`] the
188    /// document is the truth and this is whatever block zero happens to be.
189    pub(super) fn source_text(&self) -> &str {
190        source_of(&self.doc)
191    }
192
193    /// Put the source back in the one fence it is edited as.
194    ///
195    /// Backspace at the start of an empty fence is a merge, and a merge with
196    /// nothing above it leaves a paragraph — a block the source view does not
197    /// paint and the caret would be stranded in. The text survives either way,
198    /// so this is a change of container and never of content.
199    ///
200    /// Whether it had to.
201    pub(super) fn ensure_source(&mut self) -> bool {
202        if self.doc.blocks.len() == 1 && matches!(self.doc.blocks[0].kind, BlockKind::Code { .. }) {
203            return false;
204        }
205        let source = self
206            .doc
207            .blocks
208            .iter()
209            .filter_map(|block| block.text_at(*block.parts().first()?))
210            .map(|text| text.text.clone())
211            .collect::<Vec<_>>()
212            .join("\n");
213        let offset = self.selection.head.offset.min(source.len());
214        self.doc.set(source_doc(&source));
215        self.selection = Selection::at(Cursor::new(0, Part::Code, offset));
216        for anchor in &mut self.anchors {
217            anchor.range = anchor.range.clamp(&self.doc);
218        }
219        true
220    }
221
222    /// Shut everything floating. A switch of mode is a new document as far as
223    /// a menu anchored to a block is concerned.
224    pub(super) fn dismiss_menus(&mut self) {
225        self.slash = None;
226        self.pasted = None;
227        self.url_prompt = None;
228        self.hovered = None;
229        self.lifted = None;
230        self.table_drag = None;
231        self.dropping = None;
232    }
233}
234
235/// See [`Editor::source_text`].
236pub(crate) fn source_of(doc: &Doc) -> &str {
237    doc.blocks
238        .first()
239        .and_then(|block| block.text_at(Part::Code))
240        .map_or("", |text| text.text.as_str())
241}
242
243/// `doc` as markdown — see [`Editor::source`].
244pub(crate) fn source(doc: &Tracked, mode: Mode, synced: &RefCell<Synced>) -> String {
245    match mode {
246        Mode::Blocks => {
247            let mut synced = synced.borrow_mut();
248            synced.set_doc(doc.shared());
249            synced.text().to_owned()
250        }
251        Mode::Source => source_of(doc).to_string(),
252    }
253}
254
255/// Turn `doc` into `mode`, carrying `anchors` into the coordinates of the
256/// form switched to, and hand back where `caret` lands. An anchor with an end
257/// that cannot be placed there comes out detached.
258///
259/// `synced` is brought up to `doc` first. The markdown it holds is then the
260/// source, and its blocks the document, so neither is converted whole.
261pub(crate) fn switch(
262    doc: &mut Tracked,
263    synced: &mut Synced,
264    anchors: &mut [Anchor],
265    mode: Mode,
266    caret: Cursor,
267) -> Cursor {
268    let ends: Vec<(Cursor, Cursor)> = anchors
269        .iter()
270        .map(|anchor| anchor.range.ordered())
271        .collect();
272    match mode {
273        Mode::Source => {
274            synced.set_doc(doc.shared());
275            let mut at = vec![caret];
276            at.extend(ends.iter().flat_map(|(start, end)| [*start, *end]));
277            let offsets = synced.offsets(&at);
278            let source = synced.text();
279            doc.set(source_doc(source));
280            let place = |ix: usize| offsets[ix].map(|offset| Cursor::new(0, Part::Code, offset));
281            place_anchors(anchors, |ix| place(ix + 1));
282            place(0).unwrap_or(Cursor::new(0, Part::Code, source.len()))
283        }
284        Mode::Blocks => {
285            let mut offsets = vec![caret.offset];
286            offsets.extend(
287                ends.iter()
288                    .flat_map(|(start, end)| [start.offset, end.offset]),
289            );
290            synced.set_text(source_of(doc));
291            let at = synced.cursors(&offsets);
292            doc.share(synced.doc().clone());
293            doc.ensure_block();
294            let place = |ix: usize| at[ix].map(|at: Cursor| at.clamp(doc));
295            let caret = place(0).unwrap_or_else(|| Cursor::default().clamp(doc));
296            let ends: Vec<_> = (1..at.len()).map(place).collect();
297            place_anchors(anchors, |ix| ends[ix]);
298            caret
299        }
300    }
301}
302
303/// Put each anchor at the ends `place` gives for it, indexed start, end,
304/// start, end. An anchor with an end `place` cannot give collapses onto the
305/// other, detached.
306fn place_anchors(anchors: &mut [Anchor], place: impl Fn(usize) -> Option<Cursor>) {
307    for (ix, anchor) in anchors.iter_mut().enumerate() {
308        anchor.range = match (place(2 * ix), place(2 * ix + 1)) {
309            (Some(start), Some(end)) => Selection::new(start, end),
310            (surviving, other) => Selection::at(surviving.or(other).unwrap_or_default()),
311        };
312    }
313}