1use crate::text::FitPosition;
2use clankerdiff_markdown::{MarkdownTargetId, SourceRange};
3use imbl::Vector;
4use ratatui::text::Line;
5use std::{
6 ops::Range,
7 sync::{Arc, OnceLock},
8};
9
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
11pub enum MarkdownPresentation {
12 #[default]
13 Rendered,
14 SourceLines,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[allow(clippy::struct_excessive_bools)]
19pub struct MarkdownLayoutOptions {
20 pub width: u16,
21 pub block_spacing: bool,
22 pub presentation: MarkdownPresentation,
23 pub wrap: bool,
24 pub heading_markers: bool,
25 pub preserve_source_gaps: bool,
26 pub tab_width: u16,
27}
28impl Default for MarkdownLayoutOptions {
29 fn default() -> Self {
30 Self {
31 width: 80,
32 block_spacing: true,
33 presentation: MarkdownPresentation::Rendered,
34 wrap: true,
35 heading_markers: true,
36 preserve_source_gaps: false,
37 tab_width: 4,
38 }
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct MarkdownRow {
44 pub line: Line<'static>,
45 pub source: Option<SourceRange>,
46 pub target: Option<MarkdownTargetId>,
47 pub(crate) checkpoint: Option<RowCheckpoint>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub(crate) struct RowCheckpoint {
52 pub source: SourceRange,
53 pub position: FitPosition,
54 pub rendered: Arc<str>,
55}
56
57pub(crate) type RowChunk = Arc<[Arc<MarkdownRow>]>;
58pub(crate) type TargetIndex = imbl::HashMap<MarkdownTargetId, SourceRange>;
59#[derive(Debug, Clone, Default)]
60pub(crate) struct RowStore {
61 rows: Vector<Arc<MarkdownRow>>,
62 updates: usize,
63}
64impl RowStore {
65 pub(crate) fn push(&mut self, chunk: &RowChunk) {
66 self.updates += chunk.len();
67 self.rows.extend(chunk.iter().cloned());
68 }
69
70 pub(crate) fn extend(&mut self, rows: &MarkdownRows) {
71 self.append(&rows.store, rows.range.clone());
72 }
73
74 pub(crate) fn append(&mut self, store: &Self, range: Range<usize>) {
75 let mut rows = store.rows.clone();
76 rows.truncate(range.end);
77 let rows = rows.split_off(range.start);
78 self.rows.append(rows);
79 self.updates += 1;
80 }
81
82 pub(crate) fn truncate(&mut self, len: usize) {
83 if len < self.rows.len() {
84 self.rows.truncate(len);
85 self.updates += 1;
86 }
87 }
88
89 pub(crate) fn take_updates(&mut self) -> usize {
90 std::mem::take(&mut self.updates)
91 }
92
93 pub(crate) fn len(&self) -> usize {
94 self.rows.len()
95 }
96}
97
98#[derive(Debug, Clone, Default)]
99pub struct MarkdownRows {
100 store: Arc<RowStore>,
101 range: Range<usize>,
102}
103impl MarkdownRows {
104 #[must_use]
105 pub fn len(&self) -> usize {
106 self.range.len()
107 }
108 #[must_use]
109 pub fn is_empty(&self) -> bool {
110 self.range.is_empty()
111 }
112 #[must_use]
113 pub fn get(&self, index: usize) -> Option<&Arc<MarkdownRow>> {
114 if index >= self.len() {
115 return None;
116 }
117 self.store.rows.get(self.range.start + index)
118 }
119 #[must_use]
120 pub fn slice(&self, range: Range<usize>) -> Self {
121 assert!(range.start <= range.end && range.end <= self.len());
122 Self {
123 store: Arc::clone(&self.store),
124 range: self.range.start + range.start..self.range.start + range.end,
125 }
126 }
127 #[must_use]
128 pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Arc<MarkdownRow>> {
129 self.range.clone().map(move |index| &self.store.rows[index])
130 }
131}
132
133#[derive(Debug, Clone, Default)]
134pub struct MarkdownLayout {
135 rows: MarkdownRows,
136 flat: Arc<OnceLock<Arc<[Line<'static>]>>>,
137 targets: TargetIndex,
138}
139impl MarkdownLayout {
140 pub(crate) fn new(store: RowStore, targets: TargetIndex) -> Self {
141 let range = 0..store.len();
142 Self {
143 rows: MarkdownRows {
144 store: Arc::new(store),
145 range,
146 },
147 flat: Arc::default(),
148 targets,
149 }
150 }
151 #[must_use]
152 pub fn row_count(&self) -> usize {
153 self.rows.len()
154 }
155 #[must_use]
156 pub fn row(&self, index: usize) -> Option<&Arc<MarkdownRow>> {
157 self.rows.get(index)
158 }
159 #[must_use]
160 pub const fn rows(&self) -> &MarkdownRows {
161 &self.rows
162 }
163 #[must_use]
164 pub fn rows_for_source_line(&self, line: usize) -> Option<Range<usize>> {
165 self.matching_rows(|row| {
166 row.source
167 .as_ref()
168 .is_some_and(|source| source.lines.start <= line && source.lines.end >= line)
169 })
170 }
171 #[must_use]
172 pub fn rows_for_target(&self, target: MarkdownTargetId) -> Option<Range<usize>> {
173 self.matching_rows(|row| row.target == Some(target))
174 .or_else(|| {
175 let target = self.targets.get(&target)?;
176 self.matching_rows(|row| {
177 row.source.as_ref().is_some_and(|source| {
178 source.bytes.start >= target.bytes.start
179 && source.bytes.end <= target.bytes.end
180 })
181 })
182 })
183 }
184 fn matching_rows(&self, matches: impl Fn(&MarkdownRow) -> bool) -> Option<Range<usize>> {
185 let mut matching = self.rows.iter().enumerate().filter(|(_, row)| matches(row));
186 let first = matching.next()?.0;
187 let last = matching.last().map_or(first, |(index, _)| index);
188 Some(first..last + 1)
189 }
190 #[must_use]
191 pub fn materialize(&self) -> Arc<[Line<'static>]> {
192 Arc::clone(
193 self.flat
194 .get_or_init(|| self.rows.iter().map(|row| row.line.clone()).collect()),
195 )
196 }
197 pub(crate) fn is_materialized(&self) -> bool {
198 self.flat.get().is_some()
199 }
200}
201
202#[derive(Debug, Clone)]
203pub struct MarkdownRowUpdate {
204 pub base_revision: u64,
205 pub revision: u64,
206 pub first_changed_row: usize,
207 pub replacement: MarkdownRows,
208 pub reset: bool,
209}
210impl MarkdownRowUpdate {
211 #[must_use]
212 pub fn total_rows(&self) -> usize {
213 self.first_changed_row + self.replacement.len()
214 }
215}