use std::ops::Range;
use unicode_segmentation::GraphemeCursor;
use gpui_kit::base::text::{
MarkdownNode,
editor_host::{ImageNode, TextMark},
};
pub(crate) const ATOM_CHAR: char = '\u{FFFC}';
pub(crate) const ATOM_LEN: usize = ATOM_CHAR.len_utf8();
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum InlineAtom {
Image(ImageNode),
Custom(MarkdownNode),
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct MarkRun {
pub(crate) len: usize,
pub(crate) marks: TextMark,
}
#[derive(Clone, Debug, PartialEq, Default)]
pub(crate) struct InlineText {
text: String,
runs: Vec<MarkRun>,
atoms: Vec<(usize, InlineAtom)>,
}
impl InlineText {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn plain(text: &str, marks: TextMark) -> Self {
let text: String = text.chars().filter(|ch| *ch != ATOM_CHAR).collect();
let mut out = Self {
runs: vec![MarkRun {
len: text.len(),
marks,
}],
text,
atoms: Vec::new(),
};
out.normalize();
out
}
pub(crate) fn atom(atom: InlineAtom, marks: TextMark) -> Self {
Self {
text: ATOM_CHAR.to_string(),
runs: vec![MarkRun {
len: ATOM_LEN,
marks,
}],
atoms: vec![(0, atom)],
}
}
pub(crate) fn text(&self) -> &str {
&self.text
}
pub(crate) fn len(&self) -> usize {
self.text.len()
}
pub(crate) fn is_empty(&self) -> bool {
self.text.is_empty()
}
#[cfg(test)]
pub(crate) fn runs(&self) -> &[MarkRun] {
&self.runs
}
#[cfg(test)]
pub(crate) fn atoms(&self) -> &[(usize, InlineAtom)] {
&self.atoms
}
pub(crate) fn pieces(
&self,
) -> impl Iterator<Item = (Range<usize>, &TextMark, Option<&InlineAtom>)> {
let mut start = 0;
self.runs.iter().map(move |run| {
let range = start..start + run.len;
start += run.len;
let atom = self
.atoms
.iter()
.find(|(offset, _)| *offset == range.start && run.len == ATOM_LEN)
.map(|(_, atom)| atom);
(range, &run.marks, atom)
})
}
pub(crate) fn push(&mut self, other: InlineText) {
let shift = self.text.len();
self.text.push_str(&other.text);
self.runs.extend(other.runs);
self.atoms.extend(
other
.atoms
.into_iter()
.map(|(offset, atom)| (offset + shift, atom)),
);
self.normalize();
}
pub(crate) fn slice(&self, range: Range<usize>) -> InlineText {
let mut out = InlineText {
text: self.text[range.clone()].to_string(),
runs: Vec::new(),
atoms: Vec::new(),
};
for (run_range, marks, _) in self.pieces() {
let lo = run_range.start.max(range.start);
let hi = run_range.end.min(range.end);
if lo < hi {
out.runs.push(MarkRun {
len: hi - lo,
marks: marks.clone(),
});
}
}
out.atoms = self
.atoms
.iter()
.filter(|(offset, _)| *offset >= range.start && *offset + ATOM_LEN <= range.end)
.map(|(offset, atom)| (offset - range.start, atom.clone()))
.collect();
out.normalize();
out
}
pub(crate) fn replace(&mut self, range: Range<usize>, insert: InlineText) -> InlineText {
let removed = self.slice(range.clone());
let mut out = self.slice(0..range.start);
out.push(insert);
out.push(self.slice(range.end..self.len()));
*self = out;
removed
}
pub(crate) fn map_marks(&mut self, range: Range<usize>, f: impl Fn(&TextMark) -> TextMark) {
let mut runs = Vec::new();
for (run_range, marks, _) in self.pieces() {
for (lo, hi, inside) in [
(run_range.start, run_range.end.min(range.start), false),
(
run_range.start.max(range.start),
run_range.end.min(range.end),
true,
),
(run_range.start.max(range.end), run_range.end, false),
] {
if lo < hi {
runs.push(MarkRun {
len: hi - lo,
marks: if inside { f(marks) } else { marks.clone() },
});
}
}
}
self.runs = runs;
self.normalize();
}
pub(crate) fn mark_before(&self, offset: usize) -> Option<&TextMark> {
self.pieces()
.find(|(range, _, _)| range.start < offset && offset <= range.end)
.map(|(_, marks, _)| marks)
}
pub(crate) fn mark_after(&self, offset: usize) -> Option<&TextMark> {
self.pieces()
.find(|(range, _, _)| range.start <= offset && offset < range.end)
.map(|(_, marks, _)| marks)
}
pub(crate) fn is_grapheme_boundary(&self, offset: usize) -> bool {
if offset > self.text.len() || !self.text.is_char_boundary(offset) {
return false;
}
GraphemeCursor::new(offset, self.text.len(), true)
.is_boundary(&self.text, 0)
.unwrap_or(false)
}
pub(crate) fn prev_grapheme(&self, offset: usize) -> Option<usize> {
GraphemeCursor::new(offset, self.text.len(), true)
.prev_boundary(&self.text, 0)
.ok()
.flatten()
}
pub(crate) fn next_grapheme(&self, offset: usize) -> Option<usize> {
GraphemeCursor::new(offset, self.text.len(), true)
.next_boundary(&self.text, 0)
.ok()
.flatten()
}
pub(crate) fn normalize(&mut self) {
let mut runs: Vec<MarkRun> = Vec::with_capacity(self.runs.len());
let mut last_is_atom = false;
let mut start = 0;
for run in std::mem::take(&mut self.runs) {
let mut cuts = vec![start, start + run.len];
for (offset, _) in &self.atoms {
for cut in [*offset, *offset + ATOM_LEN] {
if cut > start && cut < start + run.len {
cuts.push(cut);
}
}
}
cuts.sort_unstable();
cuts.dedup();
for pair in cuts.windows(2) {
let (lo, hi) = (pair[0], pair[1]);
if lo == hi {
continue;
}
let is_atom = self.atoms.iter().any(|(offset, _)| *offset == lo);
match runs.last_mut() {
Some(prev) if !is_atom && !last_is_atom && prev.marks == run.marks => {
prev.len += hi - lo
}
_ => runs.push(MarkRun {
len: hi - lo,
marks: run.marks.clone(),
}),
}
last_is_atom = is_atom;
}
start += run.len;
}
if start < self.text.len() {
runs.push(MarkRun {
len: self.text.len() - start,
marks: TextMark::default(),
});
}
self.runs = runs;
self.atoms.sort_by_key(|(offset, _)| *offset);
debug_assert!(self.is_normalized(), "inline text not normalized: {self:?}");
}
pub(crate) fn is_normalized(&self) -> bool {
let total: usize = self.runs.iter().map(|run| run.len).sum();
if total != self.text.len() || self.runs.iter().any(|run| run.len == 0) {
return false;
}
let mut start = 0;
let mut starts = Vec::with_capacity(self.runs.len());
for run in &self.runs {
starts.push(start);
start += run.len;
}
let is_atom_run = |ix: usize| {
self.atoms
.iter()
.any(|(offset, _)| *offset == starts[ix] && self.runs[ix].len == ATOM_LEN)
};
for (offset, _) in &self.atoms {
match starts.iter().position(|s| s == offset) {
Some(ix) if self.runs[ix].len == ATOM_LEN => {}
_ => return false,
}
if self.text.get(*offset..*offset + ATOM_LEN) != Some("\u{FFFC}") {
return false;
}
}
if self.text.matches(ATOM_CHAR).count() != self.atoms.len() {
return false;
}
for ix in 1..self.runs.len() {
if self.runs[ix].marks == self.runs[ix - 1].marks
&& !is_atom_run(ix)
&& !is_atom_run(ix - 1)
{
return false;
}
}
true
}
}