1use lattice_core::BufferId;
29use lattice_grammar::registry::{MotionContext, MotionResult};
30use lattice_grammar::{
31 CheckCancelled, CommandError, CommandRegistry, GrammarResult, MotionId, MotionSpec,
32};
33use lattice_protocol::position::Position;
34use std::sync::Arc;
35
36use crate::Excerpt;
37use crate::registry::MultibufferRegistryHandle;
38
39#[derive(Debug, Clone, Copy)]
43pub struct MultibufferMotionIds {
44 pub next_excerpt_start: MotionId,
45 pub prev_excerpt_start: MotionId,
46 pub next_file_boundary: MotionId,
47 pub prev_file_boundary: MotionId,
48}
49
50pub fn register_multibuffer_motions(
59 registry: &mut CommandRegistry,
60 mb_registry: MultibufferRegistryHandle,
61) -> MultibufferMotionIds {
62 let mb_a = mb_registry.clone();
63 let next_excerpt_start = registry.register_motion(
64 "multibuffer.next-excerpt-start",
65 "Move cursor to the first row of the next excerpt (`]e`).",
66 MotionSpec {
67 curswant: lattice_grammar::CurswantEffect::default(),
68 jump: true,
69 exclusive: false,
70 apply: Arc::new(move |ctx| handle_next_excerpt_start(ctx, &mb_a)),
71 args_schema: Vec::new(),
72 },
73 );
74
75 let mb_b = mb_registry.clone();
76 let prev_excerpt_start = registry.register_motion(
77 "multibuffer.prev-excerpt-start",
78 "Move cursor to the first row of the previous excerpt (`[e`).",
79 MotionSpec {
80 curswant: lattice_grammar::CurswantEffect::default(),
81 jump: true,
82 exclusive: false,
83 apply: Arc::new(move |ctx| handle_prev_excerpt_start(ctx, &mb_b)),
84 args_schema: Vec::new(),
85 },
86 );
87
88 let mb_c = mb_registry.clone();
89 let next_file_boundary = registry.register_motion(
90 "multibuffer.next-file-boundary",
91 "Move cursor to the next excerpt whose `source` BufferId differs from the current excerpt's (`]E`).",
92 MotionSpec {
93 curswant: lattice_grammar::CurswantEffect::default(),
94 jump: true,
95 exclusive: false,
96 apply: Arc::new(move |ctx| handle_next_file_boundary(ctx, &mb_c)),
97 args_schema: Vec::new(),
98 },
99 );
100
101 let mb_d = mb_registry;
102 let prev_file_boundary = registry.register_motion(
103 "multibuffer.prev-file-boundary",
104 "Move cursor to the previous excerpt whose `source` BufferId differs from the current excerpt's (`[E`).",
105 MotionSpec {
106 curswant: lattice_grammar::CurswantEffect::default(),
107 jump: true,
108 exclusive: false,
109 apply: Arc::new(move |ctx| handle_prev_file_boundary(ctx, &mb_d)),
110 args_schema: Vec::new(),
111 },
112 );
113
114 MultibufferMotionIds {
115 next_excerpt_start,
116 prev_excerpt_start,
117 next_file_boundary,
118 prev_file_boundary,
119 }
120}
121
122fn handle_next_excerpt_start(
128 ctx: &MotionContext,
129 mb: &MultibufferRegistryHandle,
130) -> GrammarResult<MotionResult> {
131 let excerpts = excerpts_for_buffer(mb, ctx)?;
132 let count = ctx.count.get().max(1);
133 let target_row =
134 next_excerpt_start_row(&excerpts, ctx.from.line, count).unwrap_or(ctx.from.line);
135 Ok(MotionResult {
136 curswant: None,
137 target: Position::new(target_row, 0),
138 linewise: false,
139 exclusive: None,
140 notice: None,
141 })
142}
143
144fn handle_prev_excerpt_start(
145 ctx: &MotionContext,
146 mb: &MultibufferRegistryHandle,
147) -> GrammarResult<MotionResult> {
148 let excerpts = excerpts_for_buffer(mb, ctx)?;
149 let count = ctx.count.get().max(1);
150 let target_row =
151 prev_excerpt_start_row(&excerpts, ctx.from.line, count).unwrap_or(ctx.from.line);
152 Ok(MotionResult {
153 curswant: None,
154 target: Position::new(target_row, 0),
155 linewise: false,
156 exclusive: None,
157 notice: None,
158 })
159}
160
161fn handle_next_file_boundary(
162 ctx: &MotionContext,
163 mb: &MultibufferRegistryHandle,
164) -> GrammarResult<MotionResult> {
165 let excerpts = excerpts_for_buffer(mb, ctx)?;
166 let count = ctx.count.get().max(1);
167 let target_row =
168 next_file_boundary_row(&excerpts, ctx.from.line, count).unwrap_or(ctx.from.line);
169 Ok(MotionResult {
170 curswant: None,
171 target: Position::new(target_row, 0),
172 linewise: false,
173 exclusive: None,
174 notice: None,
175 })
176}
177
178fn handle_prev_file_boundary(
179 ctx: &MotionContext,
180 mb: &MultibufferRegistryHandle,
181) -> GrammarResult<MotionResult> {
182 let excerpts = excerpts_for_buffer(mb, ctx)?;
183 let count = ctx.count.get().max(1);
184 let target_row =
185 prev_file_boundary_row(&excerpts, ctx.from.line, count).unwrap_or(ctx.from.line);
186 Ok(MotionResult {
187 curswant: None,
188 target: Position::new(target_row, 0),
189 linewise: false,
190 exclusive: None,
191 notice: None,
192 })
193}
194
195fn excerpts_for_buffer(
196 mb: &MultibufferRegistryHandle,
197 ctx: &MotionContext,
198) -> Result<Vec<Excerpt>, CommandError> {
199 ctx.cancel.check()?;
200 Ok(mb
201 .handle(ctx.buffer_id)
202 .map(|h| h.excerpts())
203 .unwrap_or_default())
204}
205
206pub fn excerpt_start_rows(excerpts: &[Excerpt]) -> Vec<u32> {
213 let mut starts = Vec::with_capacity(excerpts.len());
214 let mut cursor: u32 = 0;
215 for e in excerpts {
216 starts.push(cursor);
217 cursor = cursor.saturating_add(e.line_count());
218 }
219 starts
220}
221
222pub fn containing_excerpt_index(excerpts: &[Excerpt], cursor_row: u32) -> Option<usize> {
227 if excerpts.is_empty() {
228 return None;
229 }
230 let starts = excerpt_start_rows(excerpts);
231 let mut found: Option<usize> = None;
232 for (i, &start) in starts.iter().enumerate() {
233 if start <= cursor_row {
234 found = Some(i);
235 } else {
236 break;
237 }
238 }
239 found
240}
241
242pub fn next_excerpt_start_row(excerpts: &[Excerpt], cursor_row: u32, count: u32) -> Option<u32> {
245 if excerpts.is_empty() || count == 0 {
246 return None;
247 }
248 let starts = excerpt_start_rows(excerpts);
249 let current = containing_excerpt_index(excerpts, cursor_row).unwrap_or(0);
250 let target_idx = current.checked_add(count as usize)?;
251 starts.get(target_idx).copied()
252}
253
254pub fn prev_excerpt_start_row(excerpts: &[Excerpt], cursor_row: u32, count: u32) -> Option<u32> {
257 if excerpts.is_empty() || count == 0 {
258 return None;
259 }
260 let starts = excerpt_start_rows(excerpts);
261 let current = containing_excerpt_index(excerpts, cursor_row)?;
262 let target_idx = current.checked_sub(count as usize)?;
263 starts.get(target_idx).copied()
264}
265
266fn file_boundary_indices(excerpts: &[Excerpt]) -> Vec<usize> {
270 let mut bounds = Vec::new();
271 let mut prev_source: Option<BufferId> = None;
272 for (i, e) in excerpts.iter().enumerate() {
273 if prev_source != Some(e.source) {
274 bounds.push(i);
275 prev_source = Some(e.source);
276 }
277 }
278 bounds
279}
280
281pub fn next_file_boundary_row(excerpts: &[Excerpt], cursor_row: u32, count: u32) -> Option<u32> {
287 if excerpts.is_empty() || count == 0 {
288 return None;
289 }
290 let starts = excerpt_start_rows(excerpts);
291 let bounds = file_boundary_indices(excerpts);
292 let current = containing_excerpt_index(excerpts, cursor_row).unwrap_or(0);
293 let cur_pos = bounds.iter().rposition(|&b| b <= current)?;
295 let target_pos = cur_pos.checked_add(count as usize)?;
296 let target_idx = *bounds.get(target_pos)?;
297 starts.get(target_idx).copied()
298}
299
300pub fn prev_file_boundary_row(excerpts: &[Excerpt], cursor_row: u32, count: u32) -> Option<u32> {
305 if excerpts.is_empty() || count == 0 {
306 return None;
307 }
308 let starts = excerpt_start_rows(excerpts);
309 let bounds = file_boundary_indices(excerpts);
310 let current = containing_excerpt_index(excerpts, cursor_row)?;
311 let cur_pos = bounds.iter().rposition(|&b| b <= current)?;
312 let target_pos = cur_pos.checked_sub(count as usize)?;
313 let target_idx = bounds[target_pos];
314 starts.get(target_idx).copied()
315}
316
317#[cfg(test)]
318mod tests {
319 #![allow(clippy::unwrap_used)]
320 use super::*;
321 use lattice_core::BufferId;
322
323 use crate::{Excerpt, ExcerptHeader};
324
325 fn ex(source: BufferId, start: u32, end: u32) -> Excerpt {
326 Excerpt::new(source, start, end).with_header(ExcerptHeader::default())
327 }
328
329 #[test]
330 fn start_rows_are_prefix_sums_of_line_counts() {
331 let s = BufferId::next();
332 let excerpts = vec![ex(s, 0, 2), ex(s, 5, 7), ex(s, 10, 10)];
333 assert_eq!(excerpt_start_rows(&excerpts), vec![0, 3, 6]);
335 }
336
337 #[test]
338 fn containing_excerpt_walks_the_starts() {
339 let s = BufferId::next();
340 let excerpts = vec![ex(s, 0, 2), ex(s, 0, 2), ex(s, 0, 1)];
341 assert_eq!(containing_excerpt_index(&excerpts, 0), Some(0));
343 assert_eq!(containing_excerpt_index(&excerpts, 2), Some(0));
344 assert_eq!(containing_excerpt_index(&excerpts, 3), Some(1));
345 assert_eq!(containing_excerpt_index(&excerpts, 5), Some(1));
346 assert_eq!(containing_excerpt_index(&excerpts, 6), Some(2));
347 assert_eq!(containing_excerpt_index(&excerpts, 100), Some(2));
348 }
349
350 #[test]
351 fn next_excerpt_start_advances_count_excerpts() {
352 let s = BufferId::next();
353 let excerpts = vec![ex(s, 0, 1), ex(s, 0, 0), ex(s, 0, 2)];
354 assert_eq!(next_excerpt_start_row(&excerpts, 0, 1), Some(2));
356 assert_eq!(next_excerpt_start_row(&excerpts, 0, 2), Some(3));
357 assert_eq!(next_excerpt_start_row(&excerpts, 0, 3), None);
358 assert_eq!(next_excerpt_start_row(&excerpts, 2, 1), Some(3));
359 assert_eq!(next_excerpt_start_row(&excerpts, 3, 1), None);
361 }
362
363 #[test]
364 fn prev_excerpt_start_backs_off_count_excerpts() {
365 let s = BufferId::next();
366 let excerpts = vec![ex(s, 0, 1), ex(s, 0, 0), ex(s, 0, 2)];
367 assert_eq!(prev_excerpt_start_row(&excerpts, 3, 1), Some(2));
369 assert_eq!(prev_excerpt_start_row(&excerpts, 3, 2), Some(0));
370 assert_eq!(prev_excerpt_start_row(&excerpts, 3, 3), None);
371 assert_eq!(prev_excerpt_start_row(&excerpts, 2, 1), Some(0));
372 assert_eq!(prev_excerpt_start_row(&excerpts, 0, 1), None);
374 }
375
376 #[test]
377 fn next_file_boundary_skips_excerpts_with_same_source() {
378 let a = BufferId::next();
379 let b = BufferId::next();
380 let c = BufferId::next();
381 let excerpts = vec![
382 ex(a, 0, 1), ex(a, 0, 0), ex(b, 0, 2), ex(b, 0, 0), ex(c, 0, 1), ];
388 assert_eq!(next_file_boundary_row(&excerpts, 0, 1), Some(3));
389 assert_eq!(next_file_boundary_row(&excerpts, 0, 2), Some(7));
390 assert_eq!(next_file_boundary_row(&excerpts, 0, 3), None);
391 assert_eq!(next_file_boundary_row(&excerpts, 3, 1), Some(7));
393 assert_eq!(next_file_boundary_row(&excerpts, 7, 1), None);
395 }
396
397 #[test]
398 fn prev_file_boundary_skips_excerpts_with_same_source() {
399 let a = BufferId::next();
400 let b = BufferId::next();
401 let c = BufferId::next();
402 let excerpts = vec![
403 ex(a, 0, 1), ex(b, 0, 2), ex(b, 0, 0), ex(c, 0, 1), ];
408 assert_eq!(prev_file_boundary_row(&excerpts, 6, 1), Some(2));
410 assert_eq!(prev_file_boundary_row(&excerpts, 6, 2), Some(0));
411 assert_eq!(prev_file_boundary_row(&excerpts, 6, 3), None);
412 }
413
414 #[test]
415 fn empty_excerpts_returns_none() {
416 let excerpts: Vec<Excerpt> = Vec::new();
417 assert_eq!(next_excerpt_start_row(&excerpts, 0, 1), None);
418 assert_eq!(prev_excerpt_start_row(&excerpts, 0, 1), None);
419 assert_eq!(next_file_boundary_row(&excerpts, 0, 1), None);
420 assert_eq!(prev_file_boundary_row(&excerpts, 0, 1), None);
421 }
422
423 #[test]
424 fn zero_count_returns_none() {
425 let s = BufferId::next();
426 let excerpts = vec![ex(s, 0, 1)];
427 assert_eq!(next_excerpt_start_row(&excerpts, 0, 0), None);
428 assert_eq!(prev_excerpt_start_row(&excerpts, 0, 0), None);
429 assert_eq!(next_file_boundary_row(&excerpts, 0, 0), None);
430 assert_eq!(prev_file_boundary_row(&excerpts, 0, 0), None);
431 }
432}