T21a round 1 fixes

- Enter over a selection drops it and splits at its start, one undo step
- undo and redo keep where the caret was and put it back
- a format unwraps only its own markers, so italic inside bold keeps the bold
- keys into a merged table cell land on the cell that shows
- undo rebuilds a block whose kind needs other fields, so styling comes back
- tests activate their window, as focus events need
- undo bookkeeping moves to blocks/edit.rs
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 03:49:27 -04:00
1 parent 6a9ce8a2c3
commit f3e2c6c27f
10 files changed
+330 -81

No files matched your search

+58 -2
View File
@@ -2,7 +2,7 @@ use std::ops::Range;
use gpui::{App, Entity};
use super::markdown::{Mark, marks};
use super::markdown::{Mark, edge, marks};
use crate::forms::TextInput;
/// A format the toolbars apply to markdown.
@@ -32,6 +32,17 @@ impl Format {
}
}
/// The mark an inline format writes.
fn mark(self) -> Option<Mark> {
match self {
Self::Bold => Some(Mark::Strong),
Self::Italic => Some(Mark::Emphasis),
Self::Strike => Some(Mark::Strike),
Self::Code => Some(Mark::Code),
_ => None,
}
}
/// The prefix a line format starts its lines with; numbered lines count up from one.
fn prefix(self, nth: usize) -> String {
match self {
@@ -81,6 +92,28 @@ fn line_prefix(line: &str) -> usize {
number_prefix(line)
}
/// The span of `format` that `selection` fills, but for other styles' markers: where it is written, and where its text sits.
fn own(
text: &str,
selection: &Range<usize>,
format: Format,
) -> Option<(Range<usize>, Range<usize>)> {
let wanted = format.mark().expect("an inline format has a mark");
let only_markers = |part: &str| part.chars().all(|ch| matches!(ch, '*' | '_' | '~' | '`'));
marks(text).into_iter().find_map(|(range, mark)| {
if mark != wanted || selection.start < range.start || range.end < selection.end {
return None;
}
let edge = edge(&text[range.clone()], mark);
let inner = range.start + edge..range.end - edge;
let fills = inner.start <= selection.start
&& selection.end <= inner.end
&& only_markers(&text[inner.start..selection.start])
&& only_markers(&text[selection.end..inner.end]);
fills.then_some((range, inner))
})
}
/// `text` with `format` applied over `selection`, and where the selection lands. An inline format wraps the selection, or unwraps it when already wrapped; an empty one leaves the caret between its markers. A line format prefixes each touched line, or takes the prefix off when every line has it.
pub fn apply(text: &str, selection: Range<usize>, format: Format) -> (String, Range<usize>) {
assert!(
@@ -97,11 +130,21 @@ pub fn apply(text: &str, selection: Range<usize>, format: Format) -> (String, Ra
if let Some(marker) = format.marker() {
let (before, after) = (&text[..selection.start], &text[selection.end..]);
let inner = &text[selection.clone()];
if before.ends_with(marker) && after.starts_with(marker) {
if selection.is_empty() && before.ends_with(marker) && after.starts_with(marker) {
let start = selection.start - marker.len();
let unwrapped = format!("{}{inner}{}", &before[..start], &after[marker.len()..]);
return (unwrapped, start..selection.end - marker.len());
}
if let Some((range, text_range)) = own(text, &selection, format) {
let shift = text_range.start - range.start;
let unwrapped = format!(
"{}{}{}",
&text[..range.start],
&text[text_range],
&text[range.end..]
);
return (unwrapped, selection.start - shift..selection.end - shift);
}
let shift = marker.len();
let wrapped = format!("{before}{marker}{inner}{marker}{after}");
return (wrapped, selection.start + shift..selection.end + shift);
@@ -204,6 +247,19 @@ mod tests {
);
}
#[test]
fn a_format_unwraps_only_its_own_markers() {
assert_eq!(
apply("**bold**", 2..6, Format::Italic),
("***bold***".into(), 3..7)
);
assert_eq!(
apply("a *it* b", 3..5, Format::Italic),
("a it b".into(), 2..4)
);
assert_eq!(apply("**bold**", 2..6, Format::Bold), ("bold".into(), 0..4));
}
#[test]
fn line_formats_prefix_every_touched_line_or_take_it_off() {
let text = "one\ntwo\nthree";