Skip to main content

clankerdiff_ratatui/
markdown_layout.rs

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}