Skip to main content

strop_engine/editor/
block.rs

1//! Visual rectangles share streamed cell geometry with rendering. Partial wide
2//! clusters become spaces; complete tabs retain their bytes in the register.
3use super::transact::ChangeSet;
4use super::{BlockRect, Editor, Mode, Register};
5use strop_core::id::DisplayColumn;
6use strop_core::layout::RopeGraphemes;
7use strop_core::{Range, Replacement};
8
9pub(crate) struct BlockInsertState {
10    rows: Vec<usize>,
11    column: DisplayColumn,
12    pad: bool,
13}
14struct RowPart {
15    range: Option<Range>,
16    selected: String,
17    remaining: String,
18}
19
20impl Editor {
21    pub fn enter_block_pub(&mut self) {
22        if self.buf().readonly {
23            self.message = "readonly buffer".into();
24            return;
25        }
26        let head = self.head();
27        self.sels_mut().stretch_primary(head, head);
28        self.view_mut().desired_column = None;
29        self.mode = Mode::VisualBlock;
30    }
31    pub fn block_rect_pub(&self) -> Option<BlockRect> {
32        self.block_rect()
33    }
34
35    fn cluster_cells(&self, byte: usize) -> (DisplayColumn, DisplayColumn) {
36        let line = self.buf().line_of(byte);
37        let start = self.buf().line_start(line);
38        let slice = self
39            .buf()
40            .text()
41            .byte_slice(start..self.buf().line_end(line));
42        let relative = byte.saturating_sub(start);
43        let mut end = DisplayColumn::new(0);
44        for (span, cluster) in RopeGraphemes::new(slice, self.config.tab_size) {
45            end = span.cell + span.width;
46            if relative < span.byte + cluster.len() {
47                return (span.cell, span.cell + span.width.max(1));
48            }
49        }
50        (end, end + 1)
51    }
52
53    pub(crate) fn block_rect(&self) -> Option<BlockRect> {
54        if self.mode != Mode::VisualBlock {
55            return None;
56        }
57        let (anchor, head) = (self.anchor(), self.head());
58        let (a, a_end) = self.cluster_cells(anchor);
59        let (h, h_end) = self.cluster_cells(head);
60        Some(BlockRect {
61            first_line: self.buf().line_of(anchor).min(self.buf().line_of(head)),
62            last_line: self.buf().line_of(anchor).max(self.buf().line_of(head)),
63            left_cell: a.min(h),
64            right_cell: a_end.max(h_end) - 1,
65        })
66    }
67
68    /// Block vertical movement preserves the requested display column through
69    /// short rows and clusters; byte columns cannot express that intent.
70    pub(crate) fn block_vertical(&mut self, command: &strop_grammar::Command) -> bool {
71        if self.mode != Mode::VisualBlock {
72            return false;
73        }
74        let down = match command.target {
75            strop_grammar::Target::Motion(strop_grammar::Motion::Down) => true,
76            strop_grammar::Target::Motion(strop_grammar::Motion::Up) => false,
77            _ => return false,
78        };
79        let desired = self
80            .view()
81            .desired_column
82            .unwrap_or_else(|| self.cluster_cells(self.head()).1 - 1);
83        let line = self.buf().line_of(self.head());
84        let count = command.count.unwrap_or(1).max(1);
85        let target = if down {
86            line.saturating_add(count)
87                .min(self.buf().last_content_line())
88        } else {
89            line.saturating_sub(count)
90        };
91        self.land_at_cell(target, desired);
92        self.view_mut().desired_column = Some(desired);
93        true
94    }
95
96    fn row_part(&self, line: usize, left: DisplayColumn, right: DisplayColumn) -> RowPart {
97        let start = self.buf().line_start(line);
98        let slice = self
99            .buf()
100            .text()
101            .byte_slice(start..self.buf().line_end(line));
102        let mut first = None;
103        let mut last = start;
104        let mut selected = String::new();
105        let mut remaining = String::new();
106        for (span, text) in RopeGraphemes::new(slice, self.config.tab_size) {
107            let end = span.cell + span.width;
108            if end <= left || span.cell >= right || span.width == 0 {
109                continue;
110            }
111            first.get_or_insert(start + span.byte);
112            last = start + span.byte + text.len();
113            let lo = span.cell.max(left);
114            let hi = end.min(right);
115            if lo == span.cell && hi == end {
116                selected.push_str(&text);
117            } else {
118                selected.push_str(&" ".repeat(hi - lo));
119                remaining.push_str(&" ".repeat(lo - span.cell));
120                remaining.push_str(&" ".repeat(end - hi));
121            }
122        }
123        if first.is_none() {
124            selected = " ".repeat(right - left);
125        }
126        RowPart {
127            range: first.map(|first| Range::charwise(first, last)),
128            selected,
129            remaining,
130        }
131    }
132
133    fn land_at_cell(&mut self, line: usize, column: DisplayColumn) {
134        let start = self.buf().line_start(line);
135        let end = self.buf().line_end(line);
136        let byte = RopeGraphemes::new(
137            self.buf().text().byte_slice(start..end),
138            self.config.tab_size,
139        )
140        .find_map(|(span, _)| (span.cell + span.width > column).then_some(start + span.byte))
141        .unwrap_or(end);
142        self.set_head(byte);
143        self.clamp_cursor();
144    }
145
146    pub(crate) fn block_yank(&mut self) {
147        let Some(rect) = self.block_rect() else {
148            return;
149        };
150        let rows: Vec<_> = (rect.first_line..=rect.last_line)
151            .map(|line| {
152                self.row_part(line, rect.left_cell, rect.right_cell + 1)
153                    .selected
154            })
155            .collect();
156        self.set_register(
157            None,
158            Register::blockwise(
159                rows.join("\n"),
160                DisplayColumn::new(rect.right_cell - rect.left_cell + 1),
161            ),
162        );
163        self.mode = Mode::Normal;
164        self.land_at_cell(rect.first_line, rect.left_cell);
165    }
166    pub(crate) fn block_delete(&mut self) {
167        self.block_edit(false);
168    }
169    pub(crate) fn block_change(&mut self) {
170        self.block_edit(true);
171    }
172    fn block_edit(&mut self, insert: bool) {
173        let Some(rect) = self.block_rect() else {
174            return;
175        };
176        let mut rows = Vec::new();
177        let mut selected = Vec::new();
178        let mut edits = Vec::new();
179        for line in rect.first_line..=rect.last_line {
180            let part = self.row_part(line, rect.left_cell, rect.right_cell + 1);
181            selected.push(part.selected);
182            if let Some(range) = part.range {
183                if line != rect.first_line {
184                    rows.push(line);
185                }
186                edits.push(Replacement::new(range, part.remaining));
187            }
188        }
189        if let Err(error) = self.apply(
190            self.current(),
191            self.buf().revision(),
192            ChangeSet {
193                edits,
194                undo_open: insert,
195            },
196        ) {
197            self.message = error.to_string();
198            return;
199        }
200        self.set_register(
201            None,
202            Register::blockwise(
203                selected.join("\n"),
204                DisplayColumn::new(rect.right_cell - rect.left_cell + 1),
205            ),
206        );
207        self.mode = if insert { Mode::Insert } else { Mode::Normal };
208        self.land_at_cell(rect.first_line, rect.left_cell);
209        if insert {
210            self.block_insert_state = Some(BlockInsertState {
211                rows,
212                column: rect.left_cell,
213                pad: false,
214            });
215            self.enter_insert_from("<c-v>c");
216        }
217    }
218
219    /// Inserting within a tab/wide cluster preserves both outside portions as
220    /// cells. A pads short lines; I and c skip rows that never reached the column.
221    fn insertion(
222        &self,
223        line: usize,
224        column: DisplayColumn,
225        text: &str,
226        pad: bool,
227    ) -> Option<Replacement> {
228        let start = self.buf().line_start(line);
229        let end = self.buf().line_end(line);
230        let mut width = DisplayColumn::new(0);
231        for (span, cluster) in RopeGraphemes::new(
232            self.buf().text().byte_slice(start..end),
233            self.config.tab_size,
234        ) {
235            if span.cell == column {
236                return Some(Replacement::new(
237                    Range::charwise(start + span.byte, start + span.byte),
238                    text,
239                ));
240            }
241            width = span.cell + span.width;
242            if span.cell < column && column < width {
243                return Some(Replacement::new(
244                    Range::charwise(start + span.byte, start + span.byte + cluster.len()),
245                    format!(
246                        "{}{}{}",
247                        " ".repeat(column - span.cell),
248                        text,
249                        " ".repeat(width - column)
250                    ),
251                ));
252            }
253        }
254        if column > width && !pad {
255            return None;
256        }
257        Some(Replacement::new(
258            Range::charwise(end, end),
259            format!(
260                "{}{text}",
261                " ".repeat(column.get().saturating_sub(width.get()))
262            ),
263        ))
264    }
265
266    pub(crate) fn block_insert(&mut self, right: bool) {
267        let Some(rect) = self.block_rect() else {
268            return;
269        };
270        let column = if right {
271            rect.right_cell + 1
272        } else {
273            rect.left_cell
274        };
275        let rows = (rect.first_line + 1..=rect.last_line)
276            .filter(|&line| self.insertion(line, column, "", right).is_some())
277            .collect();
278        if let Some(edit) = self.insertion(rect.first_line, column, "", true) {
279            if let Err(error) = self.apply(
280                self.current(),
281                self.buf().revision(),
282                ChangeSet {
283                    edits: vec![edit],
284                    undo_open: true,
285                },
286            ) {
287                self.message = error.to_string();
288                return;
289            }
290        }
291        self.mode = Mode::Insert;
292        self.land_at_cell(rect.first_line, column);
293        self.block_insert_state = Some(BlockInsertState {
294            rows,
295            column,
296            pad: right,
297        });
298        self.enter_insert_from("<c-v>I");
299    }
300
301    pub(crate) fn block_replicate(&mut self, typed: &str) {
302        let Some(state) = self.block_insert_state.take() else {
303            return;
304        };
305        if typed.is_empty() {
306            return;
307        }
308        let edits = state
309            .rows
310            .into_iter()
311            .filter_map(|line| {
312                if line > self.buf().last_content_line() {
313                    return None;
314                }
315                self.insertion(line, state.column, typed, state.pad)
316            })
317            .collect();
318        if let Err(error) = self.apply(
319            self.current(),
320            self.buf().revision(),
321            ChangeSet {
322                edits,
323                undo_open: true,
324            },
325        ) {
326            self.message = error.to_string();
327        }
328    }
329}