1use {
2 super::{
3 menu_functions::{parse_selection_char, string_difference},
4 Menu, MenuEvent, MenuTextStyle,
5 },
6 crate::{
7 core_editor::Editor,
8 painting::{estimate_single_line_wraps, Painter},
9 Completer, Suggestion, UndoBehavior,
10 },
11 nu_ansi_term::{ansi::RESET, Style},
12 std::{fmt::Write, iter::Sum},
13 unicode_width::UnicodeWidthStr,
14};
15
16const SELECTION_CHAR: char = '!';
17
18struct Page {
19 size: usize,
20 full: bool,
21}
22
23impl<'a> Sum<&'a Page> for Page {
24 fn sum<I>(iter: I) -> Page
25 where
26 I: Iterator<Item = &'a Page>,
27 {
28 iter.fold(
29 Page {
30 size: 0,
31 full: false,
32 },
33 |acc, menu| Page {
34 size: acc.size + menu.size,
35 full: acc.full || menu.full,
36 },
37 )
38 }
39}
40
41pub struct ListMenu {
44 name: String,
46 color: MenuTextStyle,
48 page_size: usize,
50 marker: String,
52 active: bool,
54 values: Vec<Suggestion>,
60 row_position: u16,
62 query_size: Option<usize>,
64 max_lines: u16,
66 multiline_marker: String,
68 pages: Vec<Page>,
70 page: usize,
72 event: Option<MenuEvent>,
74 input: Option<String>,
76 only_buffer_difference: bool,
79}
80
81impl Default for ListMenu {
82 fn default() -> Self {
83 Self {
84 name: "search_menu".to_string(),
85 color: MenuTextStyle::default(),
86 page_size: 10,
87 active: false,
88 values: Vec::new(),
89 row_position: 0,
90 page: 0,
91 query_size: None,
92 marker: "? ".to_string(),
93 max_lines: 5,
94 multiline_marker: ":::".to_string(),
95 pages: Vec::new(),
96 event: None,
97 input: None,
98 only_buffer_difference: true,
99 }
100 }
101}
102
103impl ListMenu {
105 #[must_use]
107 pub fn with_name(mut self, name: &str) -> Self {
108 self.name = name.into();
109 self
110 }
111
112 #[must_use]
114 pub fn with_text_style(mut self, text_style: Style) -> Self {
115 self.color.text_style = text_style;
116 self
117 }
118
119 #[must_use]
121 pub fn with_selected_text_style(mut self, selected_text_style: Style) -> Self {
122 self.color.selected_text_style = selected_text_style;
123 self
124 }
125
126 #[must_use]
128 pub fn with_description_text_style(mut self, description_text_style: Style) -> Self {
129 self.color.description_style = description_text_style;
130 self
131 }
132
133 #[must_use]
135 pub fn with_page_size(mut self, page_size: usize) -> Self {
136 self.page_size = page_size;
137 self
138 }
139
140 #[must_use]
142 pub fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self {
143 self.only_buffer_difference = only_buffer_difference;
144 self
145 }
146}
147
148impl ListMenu {
150 #[must_use]
152 pub fn with_marker(mut self, marker: String) -> Self {
153 self.marker = marker;
154 self
155 }
156
157 #[must_use]
159 pub fn with_max_entry_lines(mut self, max_lines: u16) -> Self {
160 self.max_lines = max_lines;
161 self
162 }
163
164 fn update_row_pos(&mut self, new_pos: Option<usize>) {
165 if let (Some(row), Some(page)) = (new_pos, self.pages.get(self.page)) {
166 let values_before_page = self.pages.iter().take(self.page).sum::<Page>().size;
167 let row = row.saturating_sub(values_before_page);
168 if row < page.size {
169 self.row_position = row as u16;
170 }
171 }
172 }
173
174 fn number_of_lines(&self, entry: &str, terminal_columns: u16) -> u16 {
176 number_of_lines(entry, self.max_lines as usize, terminal_columns)
177 }
178
179 fn total_values(&self) -> usize {
180 self.query_size.unwrap_or(self.values.len())
181 }
182
183 fn values_until_current_page(&self) -> usize {
184 self.pages.iter().take(self.page + 1).sum::<Page>().size
185 }
186
187 fn set_actual_page_size(&mut self, printable_entries: usize) {
188 if let Some(page) = self.pages.get_mut(self.page) {
189 page.full = page.size > printable_entries || page.full;
190 page.size = printable_entries;
191 }
192 }
193
194 fn index(&self) -> usize {
196 self.row_position as usize
197 }
198
199 fn get_value(&self) -> Option<Suggestion> {
201 self.get_values().get(self.index()).cloned()
202 }
203
204 fn reset_position(&mut self) {
206 self.page = 0;
207 self.row_position = 0;
208 self.pages = Vec::new();
209 }
210
211 fn printable_entries(&self, painter: &Painter) -> usize {
212 let available_lines = painter.screen_height().saturating_sub(2);
215 let (printable_entries, _) =
216 self.get_values()
217 .iter()
218 .fold(
219 (0, Some(0)),
220 |(lines, total_lines), suggestion| match total_lines {
221 None => (lines, None),
222 Some(total_lines) => {
223 let new_total_lines = total_lines
224 + self.number_of_lines(
225 &suggestion.value,
226 painter.screen_width().saturating_sub(
228 self.indicator().width() as u16 + count_digits(lines),
229 ),
230 );
231
232 if new_total_lines < available_lines {
233 (lines + 1, Some(new_total_lines))
234 } else {
235 (lines, None)
236 }
237 }
238 },
239 );
240
241 printable_entries
242 }
243
244 fn no_page_msg(&self, use_ansi_coloring: bool) -> String {
245 let msg = "PAGE NOT FOUND";
246 if use_ansi_coloring {
247 format!(
248 "{}{}{}",
249 self.color.selected_text_style.prefix(),
250 msg,
251 RESET
252 )
253 } else {
254 msg.to_string()
255 }
256 }
257
258 fn banner_message(&self, page: &Page, use_ansi_coloring: bool) -> String {
259 let values_until = self.values_until_current_page().saturating_sub(1);
260 let value_before = if self.values.is_empty() || self.page == 0 {
261 0
262 } else {
263 let page_size = self.pages.get(self.page).map(|page| page.size).unwrap_or(0);
264 values_until.saturating_sub(page_size) + 1
265 };
266
267 let full_page = if page.full { "[FULL]" } else { "" };
268 let status_bar = format!(
269 "Page {}: records {} - {} total: {} {}",
270 self.page + 1,
271 value_before,
272 values_until,
273 self.total_values(),
274 full_page,
275 );
276
277 if use_ansi_coloring {
278 format!(
279 "{}{}{}",
280 self.color.selected_text_style.prefix(),
281 status_bar,
282 RESET,
283 )
284 } else {
285 status_bar
286 }
287 }
288
289 fn end_of_line() -> &'static str {
291 "\r\n"
292 }
293
294 fn text_style(&self, index: usize) -> String {
296 if index == self.index() {
297 self.color.selected_text_style.prefix().to_string()
298 } else {
299 self.color.text_style.prefix().to_string()
300 }
301 }
302
303 fn create_string(
305 &self,
306 line: &str,
307 description: Option<&str>,
308 index: usize,
309 row_number: &str,
310 use_ansi_coloring: bool,
311 ) -> String {
312 let description = description.map_or("".to_string(), |desc| {
313 if use_ansi_coloring {
314 format!(
315 "{}({}) {}",
316 self.color.description_style.prefix(),
317 desc,
318 RESET
319 )
320 } else {
321 format!("({desc}) ")
322 }
323 });
324
325 if use_ansi_coloring {
326 format!(
327 "{}{}{}{}{}{}",
328 row_number,
329 description,
330 self.text_style(index),
331 &line,
332 RESET,
333 Self::end_of_line(),
334 )
335 } else {
336 let line_str = if index == self.index() {
339 format!("{}{}>{}", row_number, description, line.to_uppercase())
340 } else {
341 format!("{row_number}{description}{line}")
342 };
343
344 format!("{}{}", line_str, Self::end_of_line())
346 }
347 }
348}
349
350impl Menu for ListMenu {
351 fn name(&self) -> &str {
352 self.name.as_str()
353 }
354
355 fn indicator(&self) -> &str {
357 self.marker.as_str()
358 }
359
360 fn is_active(&self) -> bool {
362 self.active
363 }
364
365 fn can_quick_complete(&self) -> bool {
367 false
368 }
369
370 fn can_partially_complete(
373 &mut self,
374 _values_updated: bool,
375 _editor: &mut Editor,
376 _completer: &mut dyn Completer,
377 ) -> bool {
378 false
379 }
380
381 fn menu_event(&mut self, event: MenuEvent) {
383 match &event {
384 MenuEvent::Activate(_) => self.active = true,
385 MenuEvent::Deactivate => {
386 self.active = false;
387 self.input = None;
388 }
389 _ => {}
390 }
391
392 self.event = Some(event);
393 }
394
395 fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) {
397 let line_buffer = editor.line_buffer();
398 let (start, input) = if self.only_buffer_difference {
399 match &self.input {
400 Some(old_string) => {
401 let (start, input) = string_difference(line_buffer.get_buffer(), old_string);
402 if input.is_empty() {
403 (line_buffer.insertion_point(), "")
404 } else {
405 (start, input)
406 }
407 }
408 None => (line_buffer.insertion_point(), ""),
409 }
410 } else {
411 (line_buffer.insertion_point(), line_buffer.get_buffer())
412 };
413
414 let parsed = parse_selection_char(input, SELECTION_CHAR);
415 self.update_row_pos(parsed.index);
416
417 if matches!(self.event, Some(MenuEvent::Edit(_))) && parsed.index.is_none() {
420 self.reset_position();
421 }
422
423 self.values = if parsed.remainder.is_empty() {
424 self.query_size = Some(completer.total_completions(parsed.remainder, start));
425
426 let skip = self.pages.iter().take(self.page).sum::<Page>().size;
427 let take = self
428 .pages
429 .get(self.page)
430 .map(|page| page.size)
431 .unwrap_or(self.page_size);
432
433 completer.partial_complete(input, start, skip, take)
434 } else {
435 self.query_size = None;
436 completer.complete(input, start)
437 }
438 }
439
440 fn get_values(&self) -> &[Suggestion] {
442 if self.query_size.is_some() {
443 &self.values
446 } else {
447 if self.values.is_empty() {
451 return &self.values;
452 }
453
454 let start = self.pages.iter().take(self.page).sum::<Page>().size;
455
456 let end: usize = if self.page >= self.pages.len() {
457 self.page_size + start
458 } else {
459 self.pages.iter().take(self.page + 1).sum::<Page>().size
460 };
461
462 let end = end.min(self.total_values());
463 &self.values[start..end]
464 }
465 }
466
467 fn replace_in_buffer(&self, editor: &mut Editor) {
469 if let Some(Suggestion {
470 mut value,
471 span,
472 append_whitespace,
473 ..
474 }) = self.get_value()
475 {
476 let buffer_len = editor.line_buffer().len();
477 let start = span.start.min(buffer_len);
478 let end = span.end.min(buffer_len);
479 if append_whitespace {
480 value.push(' ');
481 }
482 let mut line_buffer = editor.line_buffer().clone();
483 line_buffer.replace_range(start..end, &value);
484
485 let mut offset = line_buffer.insertion_point();
486 offset += value.len().saturating_sub(end.saturating_sub(start));
487 line_buffer.set_insertion_point(offset);
488 editor.set_line_buffer(line_buffer, UndoBehavior::CreateUndoPoint);
489 }
490 }
491
492 fn update_working_details(
493 &mut self,
494 editor: &mut Editor,
495 completer: &mut dyn Completer,
496 painter: &Painter,
497 ) {
498 if let Some(event) = self.event.clone() {
499 match event {
500 MenuEvent::Activate(_) => {
501 self.reset_position();
502
503 self.input = if self.only_buffer_difference {
504 Some(editor.get_buffer().to_string())
505 } else {
506 None
507 };
508
509 self.update_values(editor, completer);
510
511 self.pages.push(Page {
512 size: self.printable_entries(painter),
513 full: false,
514 });
515 }
516 MenuEvent::Deactivate => {
517 self.active = false;
518 self.input = None;
519 }
520 MenuEvent::Edit(_) => {
521 self.update_values(editor, completer);
522 self.pages.push(Page {
523 size: self.printable_entries(painter),
524 full: false,
525 });
526 }
527 MenuEvent::NextElement | MenuEvent::MoveDown | MenuEvent::MoveRight => {
528 let new_pos = self.row_position + 1;
529
530 if let Some(page) = self.pages.get(self.page) {
531 if new_pos >= page.size as u16 {
532 self.event = Some(MenuEvent::NextPage);
533 self.update_working_details(editor, completer, painter);
534 } else {
535 self.row_position = new_pos;
536 }
537 }
538 }
539 MenuEvent::PreviousElement | MenuEvent::MoveUp | MenuEvent::MoveLeft => {
540 if let Some(new_pos) = self.row_position.checked_sub(1) {
541 self.row_position = new_pos;
542 } else {
543 let page = if let Some(page) = self.page.checked_sub(1) {
544 self.pages.get(page)
545 } else {
546 self.pages.get(self.pages.len().saturating_sub(1))
547 };
548
549 if let Some(page) = page {
550 self.row_position = page.size.saturating_sub(1) as u16;
551 }
552
553 self.event = Some(MenuEvent::PreviousPage);
554 self.update_working_details(editor, completer, painter);
555 }
556 }
557 MenuEvent::NextPage => {
558 if self.values_until_current_page() <= self.total_values().saturating_sub(1) {
559 if let Some(page) = self.pages.get_mut(self.page) {
560 if page.full {
561 self.row_position = 0;
562 self.page += 1;
563 if self.page >= self.pages.len() {
564 self.pages.push(Page {
565 size: self.page_size,
566 full: false,
567 });
568 }
569 } else {
570 page.size += self.page_size;
571 }
572 }
573
574 self.update_values(editor, completer);
575 self.set_actual_page_size(self.printable_entries(painter));
576 } else {
577 self.row_position = 0;
578 self.page = 0;
579 self.update_values(editor, completer);
580 }
581 }
582 MenuEvent::PreviousPage => {
583 match self.page.checked_sub(1) {
584 Some(page_num) => self.page = page_num,
585 None => self.page = self.pages.len().saturating_sub(1),
586 }
587 self.update_values(editor, completer);
588 }
589 }
590
591 self.event = None;
592 }
593 }
594
595 fn menu_required_lines(&self, terminal_columns: u16) -> u16 {
598 let mut entry_index = 0;
599 self.get_values().iter().fold(0, |total_lines, suggestion| {
600 let ret = total_lines
602 + self.number_of_lines(
603 &suggestion.value,
604 terminal_columns.saturating_sub(
605 self.indicator().width() as u16 + count_digits(entry_index),
606 ),
607 );
608 entry_index += 1;
609 ret
610 }) + 1
611 }
612
613 fn menu_string(&self, _available_lines: u16, use_ansi_coloring: bool) -> String {
615 let values_before_page = self.pages.iter().take(self.page).sum::<Page>().size;
616 match self.pages.get(self.page) {
617 Some(page) => {
618 let lines_string = self
619 .get_values()
620 .iter()
621 .take(page.size)
622 .enumerate()
623 .map(|(index, suggestion)| {
624 let line = &suggestion.value;
626 let line = if line.lines().count() > self.max_lines as usize {
627 let lines = line.lines().take(self.max_lines as usize).fold(
628 String::new(),
629 |mut out_string, string| {
630 let _ = write!(
631 out_string,
632 "{}\r\n{}",
633 string, self.multiline_marker
634 );
635 out_string
636 },
637 );
638
639 lines + "..."
640 } else {
641 line.replace('\n', &format!("\r\n{}", self.multiline_marker))
642 };
643
644 let row_number = format!("{}: ", index + values_before_page);
645
646 self.create_string(
647 &line,
648 suggestion.description.as_deref(),
649 index,
650 &row_number,
651 use_ansi_coloring,
652 )
653 })
654 .collect::<String>();
655
656 format!(
657 "{}{}",
658 lines_string,
659 self.banner_message(page, use_ansi_coloring)
660 )
661 }
662 None => self.no_page_msg(use_ansi_coloring),
663 }
664 }
665
666 fn min_rows(&self) -> u16 {
668 self.max_lines + 1
669 }
670}
671
672fn number_of_lines(entry: &str, max_lines: usize, terminal_columns: u16) -> u16 {
673 let lines = if entry.contains('\n') {
674 let total_lines = entry.lines().count();
675 let printable_lines = if total_lines > max_lines {
676 max_lines + 1
679 } else {
680 total_lines
681 };
682
683 let wrap_lines = entry.lines().take(max_lines).fold(0, |acc, line| {
684 acc + estimate_single_line_wraps(line, terminal_columns)
685 });
686
687 (printable_lines + wrap_lines) as u16
688 } else {
689 1 + estimate_single_line_wraps(entry, terminal_columns) as u16
690 };
691
692 lines
693}
694
695fn count_digits(mut n: usize) -> u16 {
696 if n == 0 {
698 return 1;
699 }
700 let mut count = 0;
701 while n > 0 {
702 n /= 10;
703 count += 1;
704 }
705 count
706}
707
708#[cfg(test)]
709mod tests {
710 use super::*;
711
712 #[test]
713 fn number_of_lines_test() {
714 let input = "let a: another:\nsomething\nanother";
715 let res = number_of_lines(input, 5, 30);
716
717 assert_eq!(res, 3);
719 }
720
721 #[test]
722 fn number_one_line_test() {
723 let input = "let a: another";
724 let res = number_of_lines(input, 5, 30);
725
726 assert_eq!(res, 1);
727 }
728
729 #[test]
730 fn lines_with_wrap_test() {
731 let input = "let a= an1other ver2y large l3ine what 4should wr5ap";
732 let res = number_of_lines(input, 5, 10);
733
734 assert_eq!(res, 6);
735 }
736
737 #[test]
738 fn number_of_max_lines_test() {
739 let input = "let a\n: ano\nther:\nsomething\nanother\nmore\nanother\nasdf\nasdfa\n3123";
740 let res = number_of_lines(input, 3, 30);
741
742 assert_eq!(res, 4);
744 }
745}