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