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 }
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 }
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 #[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 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
195pub 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}