Skip to main content

matchmaker/nucleo/
worker.rs

1// Original code from https://github.com/helix-editor/helix (MPL 2.0)
2// Modified by Squirreljetpack, 2025
3
4use super::{Line, Span, Style, Text};
5use bitflags::bitflags;
6use std::{
7    borrow::Cow,
8    sync::{
9        Arc,
10        atomic::{self, AtomicU32},
11    },
12};
13use unicode_segmentation::UnicodeSegmentation;
14use unicode_width::UnicodeWidthStr;
15
16use super::{injector::WorkerInjector, query::PickerQuery};
17use crate::{
18    SSS,
19    nucleo::Render,
20    utils::text::{hscroll_indicator, text_to_string, wrap_text, wrapping_indicator},
21};
22
23type ColumnFormatFn<T> = Box<dyn for<'a> Fn(&'a T) -> Text<'a> + Send + Sync>;
24pub struct Column<T> {
25    pub name: Arc<str>,
26    pub(super) format: ColumnFormatFn<T>,
27    /// Whether the column should be passed to nucleo for matching and filtering.
28    pub(super) filter: bool,
29}
30
31impl<T> Column<T> {
32    pub fn new_boxed(name: impl Into<Arc<str>>, format: ColumnFormatFn<T>) -> Self {
33        Self {
34            name: name.into(),
35            format,
36            filter: true,
37        }
38    }
39
40    pub fn new<F>(name: impl Into<Arc<str>>, f: F) -> Self
41    where
42        F: for<'a> Fn(&'a T) -> Text<'a> + SSS,
43    {
44        Self {
45            name: name.into(),
46            format: Box::new(f),
47            filter: true,
48        }
49    }
50
51    /// Disable filtering.
52    pub fn without_filtering(mut self) -> Self {
53        self.filter = false;
54        self
55    }
56
57    pub fn format<'a>(&self, item: &'a T) -> Text<'a> {
58        (self.format)(item)
59    }
60
61    // Note: the characters should match the output of [`Self::format`]
62    pub fn format_text<'a>(&self, item: &'a T) -> Cow<'a, str> {
63        Cow::Owned(text_to_string(&(self.format)(item)))
64    }
65}
66
67/// Worker: can instantiate, push, and get results. A view into computation.
68///
69/// Additionally, the worker can affect the computation via find and restart.
70pub struct Worker<T>
71where
72    T: SSS,
73{
74    /// The inner `Nucleo` fuzzy matcher.
75    pub nucleo: nucleo::Nucleo<T>,
76    /// The last pattern that was matched against.
77    pub query: PickerQuery,
78    /// A pre-allocated buffer used to collect match indices when fetching the results
79    /// from the matcher. This avoids having to re-allocate on each pass.
80    pub col_indices_buffer: Vec<u32>,
81    pub columns: Arc<[Column<T>]>,
82
83    // Background tasks which push to the injector check their version matches this or exit
84    pub(super) version: Arc<AtomicU32>,
85    // pub settings: WorkerSettings,
86    column_options: Vec<ColumnOptions>,
87}
88
89// #[derive(Debug, Default)]
90// pub struct WorkerSettings {
91//     pub stable: bool,
92// }
93
94bitflags! {
95    #[derive(Default, Clone, Debug)]
96    pub struct ColumnOptions: u8 {
97        const Optional = 1 << 0;
98        const OrUseDefault = 1 << 2;
99    }
100}
101
102impl<T> Worker<T>
103where
104    T: SSS,
105{
106    /// Column names must be distinct!
107    pub fn new(columns: impl IntoIterator<Item = Column<T>>, default_column: usize) -> Self {
108        let columns: Arc<[_]> = columns.into_iter().collect();
109        let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32;
110
111        let inner = nucleo::Nucleo::new(
112            nucleo::Config::DEFAULT,
113            Arc::new(|| {}),
114            None,
115            matcher_columns,
116        );
117
118        Self {
119            nucleo: inner,
120            col_indices_buffer: Vec::with_capacity(128),
121            query: PickerQuery::new(columns.iter().map(|col| &col.name).cloned(), default_column),
122            column_options: vec![ColumnOptions::default(); columns.len()],
123            columns,
124            version: Arc::new(AtomicU32::new(0)),
125        }
126    }
127
128    #[cfg(feature = "experimental")]
129    pub fn set_column_options(&mut self, index: usize, options: ColumnOptions) {
130        if options.contains(ColumnOptions::Optional) {
131            self.nucleo
132                .pattern
133                .configure_column(index, nucleo::pattern::Variant::Optional)
134        }
135
136        self.column_options[index] = options
137    }
138
139    #[cfg(feature = "experimental")]
140    pub fn reverse_items(&mut self, reverse_items: bool) {
141        self.nucleo.reverse_items(reverse_items);
142    }
143
144    pub fn injector(&self) -> WorkerInjector<T> {
145        WorkerInjector {
146            inner: self.nucleo.injector(),
147            columns: self.columns.clone(),
148            version: self.version.load(atomic::Ordering::Relaxed),
149            picker_version: self.version.clone(),
150        }
151    }
152
153    pub fn find(&mut self, line: &str) {
154        let old_query = self.query.parse(line);
155        if self.query == old_query {
156            return;
157        }
158        for (i, column) in self
159            .columns
160            .iter()
161            .filter(|column| column.filter)
162            .enumerate()
163        {
164            let pattern = self
165                .query
166                .get(&column.name)
167                .map(|s| &**s)
168                .unwrap_or_else(|| {
169                    self.column_options[i]
170                        .contains(ColumnOptions::OrUseDefault)
171                        .then(|| self.query.primary_column_query())
172                        .flatten()
173                        .unwrap_or_default()
174                });
175
176            let old_pattern = old_query
177                .get(&column.name)
178                .map(|s| &**s)
179                .unwrap_or_else(|| {
180                    self.column_options[i]
181                        .contains(ColumnOptions::OrUseDefault)
182                        .then(|| {
183                            let name = self.query.primary_column_name()?;
184                            old_query.get(name).map(|s| &**s)
185                        })
186                        .flatten()
187                        .unwrap_or_default()
188                });
189
190            // Fastlane: most columns will remain unchanged after each edit.
191            if pattern == old_pattern {
192                continue;
193            }
194            let is_append = pattern.starts_with(old_pattern);
195
196            self.nucleo.pattern.reparse(
197                i,
198                pattern,
199                nucleo::pattern::CaseMatching::Smart,
200                nucleo::pattern::Normalization::Smart,
201                is_append,
202            );
203        }
204    }
205
206    // --------- UTILS
207    pub fn get_nth(&self, n: u32) -> Option<&T> {
208        self.nucleo
209            .snapshot()
210            .get_matched_item(n)
211            .map(|item| item.data)
212    }
213
214    pub fn new_snapshot(nucleo: &mut nucleo::Nucleo<T>) -> (&nucleo::Snapshot<T>, Status) {
215        let nucleo::Status { changed, running } = nucleo.tick(10);
216        let snapshot = nucleo.snapshot();
217        (
218            snapshot,
219            Status {
220                item_count: snapshot.item_count(),
221                matched_count: snapshot.matched_item_count(),
222                running,
223                changed,
224            },
225        )
226    }
227
228    pub fn raw_results(&self) -> impl ExactSizeIterator<Item = &T> + DoubleEndedIterator + '_ {
229        let snapshot = self.nucleo.snapshot();
230        snapshot.matched_items(..).map(|item| item.data)
231    }
232
233    /// matched item count, total item count
234    pub fn counts(&self) -> (u32, u32) {
235        let snapshot = self.nucleo.snapshot();
236        (snapshot.matched_item_count(), snapshot.item_count())
237    }
238
239    #[cfg(feature = "experimental")]
240    pub fn set_stability(&mut self, threshold: u32) {
241        self.nucleo.set_stability(threshold);
242    }
243
244    #[cfg(feature = "experimental")]
245    pub fn get_stability(&self) -> u32 {
246        self.nucleo.get_stability()
247    }
248
249    pub fn restart(&mut self, clear_snapshot: bool) {
250        self.nucleo.restart(clear_snapshot);
251    }
252}
253
254#[derive(Debug, Default, Clone)]
255pub struct Status {
256    pub item_count: u32,
257    pub matched_count: u32,
258    pub running: bool,
259    pub changed: bool,
260}
261
262#[derive(Debug, thiserror::Error)]
263pub enum WorkerError {
264    #[error("the matcher injector has been shut down")]
265    InjectorShutdown,
266    #[error("{0}")]
267    Custom(&'static str),
268}
269
270/// A vec of ItemResult, each ItemResult being the Column Texts of the Item, and Item
271pub type WorkerResults<'a, T> = Vec<(Vec<Text<'a>>, &'a T)>;
272
273impl<T: SSS> Worker<T> {
274    /// Returns:
275    /// 1. Table of (Row, item, height)
276    /// 2. Final column widths
277    /// 3. Status
278    ///
279    /// # Notes
280    /// - Final column width is at least header width
281    pub fn results(
282        &mut self,
283        start: u32,
284        end: u32,
285        width_limits: &[u16],
286        highlight_style: Style,
287        matcher: &mut nucleo::Matcher,
288        autoscroll: Option<(usize, usize)>,
289        hscroll_offset: i8,
290    ) -> (WorkerResults<'_, T>, Vec<u16>, Status) {
291        let (snapshot, status) = Self::new_snapshot(&mut self.nucleo);
292
293        let mut widths = vec![0u16; self.columns.len()];
294
295        let iter =
296            snapshot.matched_items(start.min(status.matched_count)..end.min(status.matched_count));
297
298        let table = iter
299            .map(|item| {
300                let mut widths = widths.iter_mut();
301
302                let row = self
303                    .columns
304                    .iter()
305                    .enumerate()
306                    .zip(width_limits.iter().chain(std::iter::repeat(&u16::MAX)))
307                    .map(|((col_idx, column), &width_limit)| {
308                        let max_width = widths.next().unwrap();
309                        let cell = column.format(item.data);
310
311                        // 0 represents hide
312                        if width_limit == 0 {
313                            return Text::default();
314                        }
315
316                        let (cell, width) = if column.filter {
317                            render_cell(
318                                cell,
319                                col_idx,
320                                snapshot,
321                                &item,
322                                matcher,
323                                highlight_style,
324                                width_limit,
325                                &mut self.col_indices_buffer,
326                                autoscroll,
327                                hscroll_offset,
328                            )
329                        } else if width_limit != u16::MAX {
330                            let (cell, wrapped) = wrap_text(cell, width_limit - 1);
331
332                            let width = if wrapped {
333                                width_limit as usize
334                            } else {
335                                cell.width()
336                            };
337                            (cell, width)
338                        } else {
339                            let width = cell.width();
340                            (cell, width)
341                        };
342
343                        // update col width, row height
344                        if width as u16 > *max_width {
345                            *max_width = width as u16;
346                        }
347
348                        cell
349                    });
350
351                (row.collect(), item.data)
352            })
353            .collect();
354
355        // Nonempty columns should have width at least their header
356        for (w, c) in widths.iter_mut().zip(self.columns.iter()) {
357            let name_width = c.name.width() as u16;
358            if *w != 0 {
359                *w = (*w).max(name_width);
360            }
361        }
362
363        (table, widths, status)
364    }
365
366    pub fn exact_column_match(&mut self, column: &str) -> Option<&T> {
367        let (i, col) = self
368            .columns
369            .iter()
370            .enumerate()
371            .find(|(_, c)| column == &*c.name)?;
372
373        let query = self.query.get(column).map(|s| &**s).or_else(|| {
374            self.column_options[i]
375                .contains(ColumnOptions::OrUseDefault)
376                .then(|| self.query.primary_column_query())
377                .flatten()
378        })?;
379
380        let snapshot = self.nucleo.snapshot();
381        snapshot.matched_items(..).find_map(|item| {
382            let content = col.format_text(item.data);
383            if content.as_str() == query {
384                Some(item.data)
385            } else {
386                None
387            }
388        })
389    }
390
391    pub fn format_with<'a>(&'a self, item: &'a T, col: &str) -> Option<Cow<'a, str>> {
392        self.columns
393            .iter()
394            .find(|c| &*c.name == col)
395            .map(|c| c.format_text(item))
396    }
397}
398
399fn render_cell<T: SSS>(
400    // Assuming T implements the required SSS/Config trait
401    cell: Text<'_>,
402    col_idx: usize,
403    snapshot: &nucleo::Snapshot<T>,
404    item: &nucleo::Item<T>,
405    matcher: &mut nucleo::Matcher,
406    highlight_style: Style,
407    width_limit: u16,
408    col_indices_buffer: &mut Vec<u32>,
409    autoscroll: Option<(usize, usize)>,
410    hscroll_offset: i8,
411) -> (Text<'static>, usize) {
412    let mut cell_width = 0;
413    let mut wrapped = false;
414
415    // get indices
416    let indices_buffer = col_indices_buffer;
417    indices_buffer.clear();
418    snapshot.pattern().column_pattern(col_idx).indices(
419        item.matcher_columns[col_idx].slice(..),
420        matcher,
421        indices_buffer,
422    );
423    indices_buffer.sort_unstable();
424    indices_buffer.dedup();
425    let mut indices = indices_buffer.drain(..);
426
427    let mut lines = vec![];
428    let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX);
429    let mut grapheme_idx = 0u32;
430
431    for line in &cell {
432        // 1: Collect graphemes, compute styles, and find the first match on this line.
433        let mut line_graphemes = Vec::new();
434        let mut first_match_idx = None;
435
436        for span in line {
437            // this looks like a bug on first glance, we are iterating
438            // graphemes but treating them as char indices. The reason that
439            // this is correct is that nucleo will only ever consider the first char
440            // of a grapheme (and discard the rest of the grapheme) so the indices
441            // returned by nucleo are essentially grapheme indecies
442            let mut graphemes = span.content.graphemes(true).peekable();
443
444            while let Some(grapheme) = graphemes.next() {
445                let is_match = grapheme_idx == next_highlight_idx;
446
447                let style = if is_match {
448                    next_highlight_idx = indices.next().unwrap_or(u32::MAX);
449                    span.style.patch(highlight_style)
450                } else {
451                    span.style
452                };
453
454                if is_match && first_match_idx.is_none() {
455                    first_match_idx = Some(line_graphemes.len());
456                }
457
458                line_graphemes.push((grapheme, style));
459                grapheme_idx += 1;
460            }
461        }
462
463        // 2: Calculate where to start rendering this line
464        let mut start_idx;
465        let mut preserved_prefix = vec![];
466
467        if let Some((preserved, context)) = autoscroll {
468            let first_idx = first_match_idx.unwrap_or(0);
469            start_idx = (first_idx as i32 + hscroll_offset as i32 - context as i32).max(0) as usize;
470
471            if width_limit != u16::MAX {
472                let mut tail_width: usize = line_graphemes[start_idx..]
473                    .iter()
474                    .map(|(g, _)| g.width())
475                    .sum();
476
477                let preserved_width = line_graphemes[..preserved.min(line_graphemes.len())]
478                    .iter()
479                    .map(|(g, _)| g.width())
480                    .sum::<usize>();
481
482                let gap_indicator_width = 1;
483
484                // Expand leftwards as long as the total rendered width <= width_limit
485                while start_idx > preserved {
486                    let prev_width = line_graphemes[start_idx - 1].0.width();
487                    if tail_width + preserved_width + gap_indicator_width + prev_width
488                        <= width_limit as usize
489                    {
490                        start_idx -= 1;
491                        tail_width += prev_width;
492                    } else {
493                        break;
494                    }
495                }
496            }
497
498            if start_idx <= preserved + 1 {
499                start_idx = 0;
500            } else {
501                preserved_prefix = line_graphemes[..preserved].to_vec();
502            }
503        } else {
504            start_idx = hscroll_offset.max(0) as usize;
505        }
506
507        // 3: Apply the standard wrapping and Span generation logic to the visible slice
508        let mut current_spans = Vec::new();
509        let mut current_span = String::new();
510        let mut current_style = Style::default();
511        let mut current_width = 0;
512
513        // Add preserved prefix and ellipsis if needed
514        if start_idx > 0 && autoscroll.is_some() {
515            if !preserved_prefix.is_empty() {
516                for (g, s) in preserved_prefix {
517                    if s != current_style {
518                        if !current_span.is_empty() {
519                            current_spans.push(Span::styled(current_span, current_style));
520                        }
521                        current_span = String::new();
522                        current_style = s;
523                    }
524                    current_span.push_str(g);
525                    current_width += g.width();
526                }
527                if !current_span.is_empty() {
528                    current_spans.push(Span::styled(current_span, current_style));
529                }
530            }
531            current_spans.push(hscroll_indicator());
532            current_width += 1;
533
534            current_span = String::new();
535            current_style = Style::default();
536        }
537
538        let mut graphemes = line_graphemes[start_idx..].iter().peekable();
539
540        while let Some(&(grapheme, style)) = graphemes.next() {
541            let grapheme_width = grapheme.width();
542
543            if width_limit != u16::MAX {
544                if current_width + grapheme_width > (width_limit - 1) as usize && {
545                    grapheme_width > 1 || graphemes.peek().is_some()
546                } {
547                    if !current_span.is_empty() {
548                        current_spans.push(Span::styled(current_span, current_style));
549                    }
550                    current_spans.push(wrapping_indicator());
551                    lines.push(Line::from(current_spans));
552
553                    current_spans = Vec::new();
554                    current_span = String::new();
555                    current_width = 0;
556                    wrapped = true;
557                }
558            }
559
560            if style != current_style {
561                if !current_span.is_empty() {
562                    current_spans.push(Span::styled(current_span, current_style))
563                }
564                current_span = String::new();
565                current_style = style;
566            }
567            current_span.push_str(grapheme);
568            current_width += grapheme_width;
569        }
570
571        current_spans.push(Span::styled(current_span, current_style));
572        lines.push(Line::from(current_spans));
573        cell_width = cell_width.max(current_width);
574
575        grapheme_idx += 1; // newline
576    }
577
578    (
579        Text::from(lines),
580        if wrapped {
581            width_limit as usize
582        } else {
583            cell_width
584        },
585    )
586}
587
588#[cfg(test)]
589mod tests {
590    use super::*;
591    use nucleo::{Matcher, Nucleo};
592    use ratatui::style::{Color, Style};
593    use ratatui::text::Text;
594    use std::sync::Arc;
595
596    /// Sets up the necessary Nucleo state to trigger a match
597    fn setup_nucleo_mocks(
598        search_query: &str,
599        item_text: &str,
600    ) -> (Nucleo<String>, Matcher, Vec<u32>) {
601        let mut nucleo = Nucleo::<String>::new(nucleo::Config::DEFAULT, Arc::new(|| {}), None, 1);
602
603        let injector = nucleo.injector();
604        injector.push(item_text.to_string(), |item, columns| {
605            columns[0] = item.clone().into();
606        });
607
608        nucleo.pattern.reparse(
609            0,
610            search_query,
611            nucleo::pattern::CaseMatching::Ignore,
612            nucleo::pattern::Normalization::Smart,
613            false,
614        );
615
616        nucleo.tick(10); // Process the item
617
618        let matcher = Matcher::default();
619        let buffer = Vec::new();
620
621        (nucleo, matcher, buffer)
622    }
623
624    #[test]
625    fn test_no_scroll_context_renders_normally() {
626        let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "hello match world");
627        let snapshot = nucleo.snapshot();
628        let item = snapshot.get_item(0).unwrap();
629
630        let cell = Text::from("hello match world");
631        let highlight = Style::default().fg(Color::Red);
632
633        let (result_text, width) = render_cell(
634            cell,
635            0,
636            &snapshot,
637            &item,
638            &mut matcher,
639            highlight,
640            u16::MAX,
641            &mut buffer,
642            None,
643            0,
644        );
645
646        let output_str = text_to_string(&result_text);
647        assert_eq!(output_str, "hello match world");
648        assert_eq!(width, 17);
649    }
650
651    #[test]
652    fn test_scroll_context_cuts_prefix_correctly() {
653        // Match starts at index 6 ("match"). Context is 2.
654        // autoscroll = Some((preserved=0, context=2))
655        // initial_start_idx = 6 + 0 - 2 = 4.
656        // start_idx = 4.
657        // start_idx > preserved + 1 (4 > 1) -> preserved_prefix is empty, start_idx=4.
658        // "o match world"
659        let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "hello match world");
660        let snapshot = nucleo.snapshot();
661        let item = snapshot.get_item(0).unwrap();
662
663        let cell = Text::from("hello match world");
664        let highlight = Style::default().fg(Color::Red);
665
666        // Width limit MAX so no backfill constraint triggers
667        let (result_text, _) = render_cell(
668            cell,
669            0,
670            &snapshot,
671            &item,
672            &mut matcher,
673            highlight,
674            u16::MAX,
675            &mut buffer,
676            Some((0, 2)),
677            0,
678        );
679
680        let output_str = text_to_string(&result_text);
681        assert_eq!(output_str, "…o match world");
682    }
683
684    #[test]
685    fn test_scroll_context_backfills_to_fill_width_limit() {
686        // Query "match". Starts at index 10.
687        // "abcdefghijmatch"
688        // autoscroll = Some((preserved=0, context=1))
689        // initial_start_idx = 10 + 0 - 1 = 9 ("jmatch").
690        // width_limit = 10.
691        // tail_width ("jmatch") = 6.
692        // Try to decrease start_idx.
693        // start_idx=8 ("ijmatch"), tail_width=7.
694        // start_idx=7 ("hijmatch"), tail_width=8.
695        // start_idx=6 ("ghijmatch"), tail_width=9.
696        // start_idx=5 ("fghijmatch"), tail_width=10.
697        // start_idx=4 ("efghijmatch"), tail_width=11 > 10 (STOP).
698        // Result start_idx = 5. Output: "fghijmatch"
699
700        let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefghijmatch");
701        let snapshot = nucleo.snapshot();
702        let item = snapshot.get_item(0).unwrap();
703
704        let cell = Text::from("abcdefghijmatch");
705        let highlight = Style::default().fg(Color::Red);
706
707        let (result_text, width) = render_cell(
708            cell,
709            0,
710            &snapshot,
711            &item,
712            &mut matcher,
713            highlight,
714            10,
715            &mut buffer,
716            Some((0, 1)),
717            0,
718        );
719
720        let output_str = text_to_string(&result_text);
721        assert_eq!(output_str, "…ghijmatch");
722        assert_eq!(width, 10);
723    }
724
725    #[test]
726    fn test_preserved_prefix_and_ellipsis() {
727        // Query "match". Starts at index 10.
728        // "abcdefghijmatch"
729        // autoscroll = Some((preserved=3, context=1))
730        // initial_start_idx = 10 + 0 - 1 = 9.
731        // start_idx = 9.
732        // width_limit = 10.
733        // preserved_width ("abc") = 3.
734        // gap_indicator_width ("…") = 1.
735        // tail_width ("jmatch") = 6.
736        // total = 3 + 1 + 6 = 10.
737        // start_idx=9, preserved=3. 9 > 3 + 1 (9 > 4) -> preserved_prefix = "abc", output: "abc…jmatch"
738
739        let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefghijmatch");
740        let snapshot = nucleo.snapshot();
741        let item = snapshot.get_item(0).unwrap();
742
743        let cell = Text::from("abcdefghijmatch");
744        let highlight = Style::default().fg(Color::Red);
745
746        let (result_text, width) = render_cell(
747            cell,
748            0,
749            &snapshot,
750            &item,
751            &mut matcher,
752            highlight,
753            10,
754            &mut buffer,
755            Some((3, 1)),
756            0,
757        );
758
759        let output_str = text_to_string(&result_text);
760        assert_eq!(output_str, "abc…jmatch");
761        assert_eq!(width, 10);
762    }
763
764    #[test]
765    fn test_wrap() {
766        let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefmatch");
767        let snapshot = nucleo.snapshot();
768        let item = snapshot.get_item(0).unwrap();
769
770        let cell = Text::from("abcdefmatch");
771        let highlight = Style::default().fg(Color::Red);
772
773        let (result_text, width) = render_cell(
774            cell,
775            0,
776            &snapshot,
777            &item,
778            &mut matcher,
779            highlight,
780            10,
781            &mut buffer,
782            Some((3, 1)),
783            -2,
784        );
785
786        let output_str = text_to_string(&result_text);
787        assert_eq!(output_str, "abcdefmat↵\nch");
788        assert_eq!(width, 10);
789    }
790}