Skip to main content

indigo_core/
window.rs

1use crate::{
2    buffer::{Buffer, BufferKey},
3    editor::Editor,
4    range::RangeState,
5    rope::{LINE_TYPE, RopeExt as _},
6    selection::{Selection, SelectionEdit, SelectionMove, SelectionState},
7};
8use indigo_wrap::{WMut, WRef, Wrap, WrapMut, WrapRef};
9use std::cmp::min;
10
11slotmap::new_key_type! {
12    #[must_use]
13    pub struct WindowKey;
14}
15
16#[derive(Clone)]
17pub struct WindowState {
18    pub buffer: BufferKey,
19    pub selection: SelectionState,
20    pub height: u16,
21    pub prev_vertical_scroll: usize,
22    // TODO(horizontal_scroll)
23}
24
25impl WindowState {
26    #[must_use]
27    pub fn new(buffer_key: BufferKey) -> Self {
28        let selection = SelectionState {
29            ranges: vec![RangeState::default()],
30            primary_range: 0,
31        };
32        Self {
33            buffer: buffer_key,
34            selection,
35            height: 0,
36            prev_vertical_scroll: 0,
37        }
38    }
39}
40
41#[must_use]
42pub struct WindowView<'a, W: Wrap> {
43    state: W::Wrap<'a, WindowState>,
44    buffer: W::Wrap<'a, Buffer>,
45}
46
47pub type Window<'a> = WindowView<'a, WRef>;
48
49pub type WindowMut<'a> = WindowView<'a, WMut>;
50
51#[expect(clippy::elidable_lifetime_names)]
52impl<'a, W: Wrap> WindowView<'a, W> {
53    //
54}
55
56impl<'a, W: WrapRef> WindowView<'a, W> {
57    pub fn new(buffer: W::WrapRef<'a, Buffer>, state: W::WrapRef<'a, WindowState>) -> Self {
58        Self { state, buffer }
59    }
60
61    #[must_use]
62    pub fn height(&self) -> u16 {
63        self.state.height
64    }
65
66    #[must_use]
67    pub fn vertical_scroll(&self) -> usize {
68        let last_line = self.buffer.text.rope().len_lines_unix().saturating_sub(1);
69        min(self.state.prev_vertical_scroll, last_line)
70    }
71
72    // TODO(horizontal_scroll)
73
74    #[must_use]
75    pub fn buffer(&self) -> &Buffer {
76        &self.buffer
77    }
78
79    pub fn selection(&self) -> Selection<'_> {
80        Selection::new(&self.buffer.text, &self.state.selection)
81            .expect("Window text and selection state are always kept valid")
82    }
83
84    pub fn assert_invariants(&self) -> anyhow::Result<()> {
85        let _ = Selection::new(&self.buffer.text, &self.state.selection)?;
86        Ok(())
87    }
88}
89
90impl<W: WrapMut> WindowView<'_, W> {
91    pub fn set_height(&mut self, height: u16) {
92        self.state.height = height;
93    }
94
95    pub fn scroll_to_line(&mut self, line: usize) {
96        let last_line = self.buffer.text.rope().len_lines_unix().saturating_sub(1);
97        self.state.prev_vertical_scroll = min(line, last_line);
98    }
99
100    // TODO: There's a bug where the cursor can move one line above the viewport without this
101    // function scrolling it into view. Try moving up line-by-line in a long file like `Cargo.lock`
102    // and watch it pop in and out from the top of the viewport. Moving down works correctly: the
103    // cursor never disappears below the bottom of the viewport.
104    pub fn scroll_to_selection(&mut self) {
105        let state = &self.state.selection;
106        let head_byte_index = state.ranges[state.primary_range].head.byte_index;
107        let line = self
108            .buffer
109            .text
110            .rope()
111            .byte_to_line_idx(head_byte_index, LINE_TYPE);
112        let top = self.vertical_scroll();
113        let bottom = top + usize::from(self.state.height).saturating_sub(1);
114        if line < top {
115            self.state.prev_vertical_scroll = line;
116        } else if line > bottom {
117            self.state.prev_vertical_scroll = top + (line - bottom);
118        }
119    }
120
121    pub fn scroll_center_selection(&mut self) {
122        let state = &self.state.selection;
123        let head_byte_index = state.ranges[state.primary_range].head.byte_index;
124        let line = self
125            .buffer
126            .text
127            .rope()
128            .byte_to_line_idx(head_byte_index, LINE_TYPE);
129        let half_height = usize::from(self.state.height) / 2;
130        self.state.prev_vertical_scroll = line.saturating_sub(half_height);
131    }
132
133    #[must_use]
134    pub fn buffer_mut(&mut self) -> &mut Buffer {
135        &mut self.buffer
136    }
137
138    pub fn selection_move(&mut self) -> SelectionMove<'_> {
139        SelectionMove::new(&self.buffer.text, &mut self.state.selection)
140            .expect("Window text and selection state are always kept valid")
141            .on_drop(|selection| selection.assert_invariants().unwrap())
142    }
143
144    pub fn selection_edit(&mut self) -> SelectionEdit<'_> {
145        SelectionEdit::new(&mut self.buffer.text, &mut self.state.selection)
146            .expect("Window text and selection state are always kept valid")
147            .on_drop(|selection| selection.assert_invariants().unwrap())
148    }
149
150    #[tracing::instrument(skip_all)]
151    pub fn undo(&mut self) -> anyhow::Result<bool> {
152        let version = self.buffer.text.version();
153        if self.buffer.text.undo()? {
154            if let Some(opss) = self.buffer.text.ops_since(version) {
155                for ops in opss {
156                    self.state.selection.transform(ops, &self.buffer.text);
157                }
158                self.state.selection.merge_overlapping();
159            }
160            Ok(true)
161        } else {
162            Ok(false)
163        }
164    }
165
166    #[tracing::instrument(skip_all)]
167    pub fn redo(&mut self) -> anyhow::Result<bool> {
168        let version = self.buffer.text.version();
169        if self.buffer.text.redo()? {
170            if let Some(opss) = self.buffer.text.ops_since(version) {
171                for ops in opss {
172                    self.state.selection.transform(ops, &self.buffer.text);
173                }
174                self.state.selection.merge_overlapping();
175            }
176            Ok(true)
177        } else {
178            Ok(false)
179        }
180    }
181}
182
183pub fn scroll_up(editor: &mut Editor) {
184    let mut window = editor.focused_window_mut();
185    let line = window.vertical_scroll().saturating_sub(3);
186    window.scroll_to_line(line);
187}
188
189pub fn scroll_down(editor: &mut Editor) {
190    let mut window = editor.focused_window_mut();
191    let line = window.vertical_scroll() + 3;
192    window.scroll_to_line(line);
193}
194
195// TODO(horizontal_scroll)
196
197pub fn scroll_half_page_up(editor: &mut Editor) {
198    let mut window = editor.focused_window_mut();
199    let line = window
200        .vertical_scroll()
201        .saturating_sub(usize::from(window.height()) / 2);
202    window.scroll_to_line(line);
203}
204
205pub fn scroll_half_page_down(editor: &mut Editor) {
206    let mut window = editor.focused_window_mut();
207    let line = window.vertical_scroll() + usize::from(window.height()) / 2;
208    window.scroll_to_line(line);
209}
210
211pub fn scroll_full_page_up(editor: &mut Editor) {
212    let mut window = editor.focused_window_mut();
213    let line = window
214        .vertical_scroll()
215        .saturating_sub(usize::from(window.height()));
216    window.scroll_to_line(line);
217}
218
219pub fn scroll_full_page_down(editor: &mut Editor) {
220    let mut window = editor.focused_window_mut();
221    let line = window.vertical_scroll() + usize::from(window.height());
222    window.scroll_to_line(line);
223}