documents: block commands as Typora's Paragraph menu
MarkdownDocument::block runs a BlockCommand at the caret: heading levels, paragraph, promote and demote; quote and list toggles; rule, code, math, toc, footnote, link definition and front matter inserts; empty paragraphs before and after; delete. Formats and block commands share one runner off the UI thread; FormatRefusal is now EditRefusal. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1d0354592b
commit
58e212d4ec
15 files changed
+1849
-188
No files matched your search
@@ -0,0 +1,165 @@
|
||||
//! Edits read off the UI thread: formats and block commands.
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::{AppContext as _, Context};
|
||||
|
||||
use super::{
|
||||
DocumentEvent, LARGEST_READ, MarkdownDocument,
|
||||
block::{self, BlockCommand},
|
||||
format::{Change, EditRefusal, InlineFormat, toggle},
|
||||
};
|
||||
use crate::documents::{MarkdownMode, modes::joint};
|
||||
|
||||
/// The text read, the open block's offset, the selection.
|
||||
type Snapshot = (String, usize, Range<usize>);
|
||||
|
||||
impl MarkdownDocument {
|
||||
/// `format` at the selection, read apart; refusals by event.
|
||||
pub fn format(
|
||||
&mut self,
|
||||
format: InlineFormat,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<(), EditRefusal> {
|
||||
self.apart(
|
||||
format!("format {format:?}"),
|
||||
move |full, selection| toggle(full, selection, format),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
/// `command` at the caret, read apart; refusals by event.
|
||||
pub fn block(
|
||||
&mut self,
|
||||
command: BlockCommand,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<(), EditRefusal> {
|
||||
self.apart(
|
||||
format!("block {command:?}"),
|
||||
move |full, selection| block::run(full, selection, command),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
/// Reads `read` off the UI thread, then writes its change.
|
||||
fn apart(
|
||||
&mut self,
|
||||
name: String,
|
||||
read: impl FnOnce(&str, Range<usize>) -> Result<Change, EditRefusal> + Send + 'static,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<(), EditRefusal> {
|
||||
if self.mode == MarkdownMode::Preview {
|
||||
return Err(EditRefusal::Preview);
|
||||
}
|
||||
let (full, offset, selection) = self.snapshot(cx);
|
||||
if full.len() > LARGEST_READ {
|
||||
log::info!("markdown document: {name} refused: {} bytes", full.len());
|
||||
return Err(EditRefusal::TooLong);
|
||||
}
|
||||
if self.editing {
|
||||
log::info!("markdown document: {name} refused: one runs");
|
||||
return Err(EditRefusal::Busy);
|
||||
}
|
||||
log::info!("markdown document: {name} reads {} bytes apart", full.len());
|
||||
let taken = (full.clone(), offset, selection.clone());
|
||||
self.editing = true;
|
||||
cx.spawn(async move |document, cx| {
|
||||
let change = cx
|
||||
.background_spawn(async move { read(&full, selection) })
|
||||
.await;
|
||||
document
|
||||
.update(cx, |document, cx| {
|
||||
document.editing = false;
|
||||
let done = if document.mode == MarkdownMode::Preview {
|
||||
Err(EditRefusal::Preview)
|
||||
} else if document.snapshot(cx) != taken {
|
||||
Err(EditRefusal::Changed)
|
||||
} else {
|
||||
change
|
||||
.and_then(|change| document.apply(&name, change, &taken.0, taken.1, cx))
|
||||
};
|
||||
if let Err(why) = done {
|
||||
log::info!("markdown document: {name} refused: {why:?}");
|
||||
cx.emit(DocumentEvent::EditRefused(why));
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The text an edit reads: the open block's in place.
|
||||
fn snapshot(&self, cx: &Context<Self>) -> Snapshot {
|
||||
let text = self.editor.read(cx).text();
|
||||
let Some(open) = &self.open else {
|
||||
return (text.to_string(), 0, self.editor.read(cx).primary().range());
|
||||
};
|
||||
let input = open.input.read(cx);
|
||||
let (before, after) = match open.append {
|
||||
true => (format!("{text}{}", joint(text)), ""),
|
||||
false => (
|
||||
text[..open.range.start].to_string(),
|
||||
&text[open.range.end..],
|
||||
),
|
||||
};
|
||||
let picked = input.selection();
|
||||
let offset = before.len();
|
||||
let full = format!("{before}{}{after}", input.text());
|
||||
(full, offset, offset + picked.start..offset + picked.end)
|
||||
}
|
||||
|
||||
/// Writes `change`: into the open block while it stays inside.
|
||||
fn apply(
|
||||
&mut self,
|
||||
name: &str,
|
||||
(edits, selected): Change,
|
||||
full: &str,
|
||||
offset: usize,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<(), EditRefusal> {
|
||||
log::info!("markdown document: {name}");
|
||||
if let Some(open) = &self.open {
|
||||
let end = offset + open.input.read(cx).text().len();
|
||||
let grown = (edits.iter().map(|(_, with)| with.len()).sum::<usize>() + end)
|
||||
.checked_sub(edits.iter().map(|(range, _)| range.len()).sum::<usize>());
|
||||
let inside = edits
|
||||
.iter()
|
||||
.all(|(range, _)| offset <= range.start && range.end <= end)
|
||||
&& grown.is_some_and(|grown| offset <= selected.start && selected.end <= grown);
|
||||
if inside {
|
||||
let (Some(from), Some(to)) = (
|
||||
edits.iter().map(|(range, _)| range.start).min(),
|
||||
edits.iter().map(|(range, _)| range.end).max(),
|
||||
) else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut written = String::new();
|
||||
let mut at = from;
|
||||
for (range, with) in &edits {
|
||||
written.push_str(&full[at..range.start]);
|
||||
written.push_str(with);
|
||||
at = range.end;
|
||||
}
|
||||
written.push_str(&full[at..to]);
|
||||
open.input.update(cx, |input, cx| {
|
||||
input.select(from - offset..to - offset, cx);
|
||||
input.insert(&written, cx);
|
||||
input.select(selected.start - offset..selected.end - offset, cx);
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
log::info!("markdown document: {name} reaches past the open block");
|
||||
self.commit(cx);
|
||||
if self.editor.read(cx).text() != full {
|
||||
log::error!("markdown document: {name}: the write-back is not the text read");
|
||||
return Err(EditRefusal::Changed);
|
||||
}
|
||||
}
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.edit(edits, cx);
|
||||
editor.select(std::iter::once(selected), cx);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user