Skip to main content

clankerdiff_ratatui/
markdown_layout.rs

1use 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}