MarkdownDocument: one caret for every mode, pressed blocks kept across write-backs, preview read only, keyboard reach, history capped on every path
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
2b6f5513aa
commit
fc181b44c2
13 files changed
+590
-242
No files matched your search
@@ -0,0 +1,160 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::{AppContext as _, Context, Focusable as _, Window};
|
||||
|
||||
use super::{DocumentEvent, MarkdownDocument, Open};
|
||||
use crate::{
|
||||
documents::{
|
||||
markdown::markdown_highlights,
|
||||
modes::{at_edge, joint},
|
||||
},
|
||||
forms::{InputEvent, TextInput},
|
||||
};
|
||||
|
||||
/// Where byte `at` lies after `edit`, if it survives.
|
||||
fn moved(at: usize, edit: &Option<(Range<usize>, usize)>) -> Option<usize> {
|
||||
match edit {
|
||||
None => Some(at),
|
||||
Some((replaced, _)) if at < replaced.start => Some(at),
|
||||
Some((replaced, written)) if at >= replaced.end => Some(at - replaced.len() + written),
|
||||
Some(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl MarkdownDocument {
|
||||
/// Opens block `ix` as shown, or one past the end.
|
||||
pub(super) fn open(
|
||||
&mut self,
|
||||
ix: Option<usize>,
|
||||
at_start: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let pressed = match ix.map(|ix| self.parsed.blocks.get(ix)) {
|
||||
Some(None) => return log::warn!("markdown document: a press on a block read away"),
|
||||
pressed => pressed.flatten().map(|block| block.range.start),
|
||||
};
|
||||
let written = self.commit(cx);
|
||||
if written.is_some() {
|
||||
self.reparse(cx);
|
||||
}
|
||||
if !self.read_now() {
|
||||
return log::warn!("markdown document: pressed while the text is read again");
|
||||
}
|
||||
let text = self.editor.read(cx).text();
|
||||
let (range, append) = match pressed {
|
||||
None => (text.len()..text.len(), true),
|
||||
Some(start) => {
|
||||
let found = moved(start, &written).and_then(|start| {
|
||||
self.parsed
|
||||
.blocks
|
||||
.iter()
|
||||
.find(|block| block.range.start == start)
|
||||
});
|
||||
let Some(block) = found else {
|
||||
return log::warn!("markdown document: the pressed block changed as one wrote");
|
||||
};
|
||||
(block.range.clone(), false)
|
||||
}
|
||||
};
|
||||
let source = text[range.clone()].to_string();
|
||||
let input = cx.new(|cx| {
|
||||
TextInput::new(window, cx)
|
||||
.multi_line(1, usize::MAX)
|
||||
.highlighter(markdown_highlights)
|
||||
});
|
||||
input.update(cx, |input, cx| {
|
||||
input.set_text(source, cx);
|
||||
if at_start {
|
||||
input.select(0..0, cx);
|
||||
}
|
||||
});
|
||||
window.focus(&input.focus_handle(cx), cx);
|
||||
let subscriptions =
|
||||
vec![
|
||||
cx.subscribe(&input, |document, _, event: &InputEvent, cx| match event {
|
||||
InputEvent::Blur => {
|
||||
document.commit(cx);
|
||||
}
|
||||
InputEvent::Changed => cx.emit(DocumentEvent::Changed),
|
||||
_ => {}
|
||||
}),
|
||||
];
|
||||
log::info!("markdown document: block {range:?} opens");
|
||||
self.open = Some(Open {
|
||||
range,
|
||||
input,
|
||||
append,
|
||||
_subscriptions: subscriptions,
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Writes the open block as one step; the edit made.
|
||||
pub(super) fn commit(&mut self, cx: &mut Context<Self>) -> Option<(Range<usize>, usize)> {
|
||||
let open = self.open.take()?;
|
||||
cx.notify();
|
||||
let typed = open.input.read(cx).text().to_string();
|
||||
let caret = open.input.read(cx).cursor();
|
||||
let text = self.editor.read(cx).text();
|
||||
let (lead, written) = match open.append {
|
||||
true if typed.is_empty() => return None,
|
||||
true => (joint(text).len(), format!("{}{typed}", joint(text))),
|
||||
false => (0, typed),
|
||||
};
|
||||
let at = open.range.start + lead + caret;
|
||||
let edit = (text[open.range.clone()] != written).then(|| {
|
||||
log::info!("markdown document: block {:?} written back", open.range);
|
||||
(open.range.clone(), written.len())
|
||||
});
|
||||
if edit.is_some() {
|
||||
self.writes += 1;
|
||||
let start = open.range.start;
|
||||
let landed = self.editor.update(cx, |editor, cx| {
|
||||
editor.edit([(open.range, written.clone())], cx);
|
||||
editor.text().get(start..start + written.len()) == Some(written.as_str())
|
||||
});
|
||||
if !landed {
|
||||
self.writes -= 1;
|
||||
log::error!("markdown document: the editor refused a write-back");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
// The one caret follows the block's.
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.select(std::iter::once(at..at), cx));
|
||||
edit
|
||||
}
|
||||
|
||||
/// Up past the open block's top, or down past it.
|
||||
pub(super) fn step(&mut self, down: bool, window: &mut Window, cx: &mut Context<Self>) -> bool {
|
||||
let Some(open) = &self.open else {
|
||||
return false;
|
||||
};
|
||||
if open.append || !at_edge(open.input.read(cx), down) {
|
||||
return false;
|
||||
}
|
||||
let here = self.block_at(open.range.start);
|
||||
let next = match down {
|
||||
true => Some(here + 1).filter(|next| *next < self.parsed.blocks.len()),
|
||||
false => here.checked_sub(1),
|
||||
};
|
||||
let Some(next) = next else {
|
||||
return false;
|
||||
};
|
||||
self.open(Some(next), down, window, cx);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn a_place_moves_with_an_edit_before_it() {
|
||||
let edit = Some((2..6, 9));
|
||||
assert_eq!(super::moved(10, &edit), Some(15));
|
||||
assert_eq!(super::moved(1, &edit), Some(1), "before stays");
|
||||
assert_eq!(super::moved(3, &edit), None, "inside is gone");
|
||||
assert_eq!(super::moved(7, &None), Some(7));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user