Skip to main content

editor_core/
render_grid.rs

1//! Viewport and render-grid snapshot helpers.
2
3use super::*;
4
5impl EditorCore {
6    /// Get styled headless grid snapshot (by visual line).
7    ///
8    /// - Supportsoft wrapping (based `layout_engine`)
9    /// - `Cell.styles` will `interval_tree` + `style_layers` merged from
10    /// - Supportcode folding (based `folding_manager`)
11    ///
12    /// Note: This API is not responsible for mapping `StyleId` to specific colors.
13    pub fn get_headless_grid_styled(&self, start_visual_row: usize, count: usize) -> HeadlessGrid {
14        self.with_visual_row_index(|index| {
15            let mut grid = HeadlessGrid::new(start_visual_row, count);
16            if count == 0 {
17                return grid;
18            }
19
20            let total_visual = index.total_visual_lines();
21            if start_visual_row >= total_visual {
22                return grid;
23            }
24
25            let tab_width = self.layout_engine.tab_width();
26            let end_visual = start_visual_row.saturating_add(count).min(total_visual);
27            let regions = self.folding_manager.regions();
28
29            let Some((mut span, mut visual_in_line)) = index.span_for_visual_row(start_visual_row)
30            else {
31                return grid;
32            };
33            let mut current_visual = start_visual_row;
34
35            while current_visual < end_visual {
36                let logical_line = span.logical_line;
37
38                let Some(layout) = self.layout_engine.get_line_layout(logical_line) else {
39                    let remaining_in_span = span.visual_line_count.saturating_sub(visual_in_line);
40                    current_visual = current_visual.saturating_add(remaining_in_span);
41                    let Some(next_span) = index.next_span_after_logical_line(logical_line) else {
42                        break;
43                    };
44                    span = next_span;
45                    visual_in_line = 0;
46                    continue;
47                };
48
49                let line_text = self
50                    .line_index
51                    .get_line_text(logical_line)
52                    .unwrap_or_default();
53                let line_char_len = line_text.chars().count();
54                let line_start_offset = self.line_index.position_to_char_offset(logical_line, 0);
55
56                let segment_start_col = if visual_in_line == 0 {
57                    0
58                } else {
59                    layout
60                        .wrap_points
61                        .get(visual_in_line - 1)
62                        .map(|wp| wp.char_index)
63                        .unwrap_or(0)
64                        .min(line_char_len)
65                };
66
67                let segment_end_col = if visual_in_line < layout.wrap_points.len() {
68                    layout.wrap_points[visual_in_line]
69                        .char_index
70                        .min(line_char_len)
71                } else {
72                    line_char_len
73                };
74
75                let mut headless_line = HeadlessLine::new(logical_line, visual_in_line > 0);
76                let mut segment_x_start_cells = 0usize;
77                if visual_in_line > 0 {
78                    let indent_cells = wrap_indent_cells_for_line_text(
79                        &line_text,
80                        self.layout_engine.wrap_indent(),
81                        self.viewport_width,
82                        tab_width,
83                    );
84                    segment_x_start_cells = indent_cells;
85                    for _ in 0..indent_cells {
86                        headless_line.add_cell(Cell::new(' ', 1));
87                    }
88                }
89                let mut x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
90
91                for (col, ch) in line_text
92                    .chars()
93                    .enumerate()
94                    .skip(segment_start_col)
95                    .take(segment_end_col.saturating_sub(segment_start_col))
96                {
97                    let offset = line_start_offset + col;
98                    let styles = self.styles_at_offset(offset);
99                    let w = cell_width_at(ch, x_in_line, tab_width);
100                    x_in_line = x_in_line.saturating_add(w);
101                    headless_line.add_cell(Cell::with_styles(ch, w, styles));
102                }
103
104                headless_line.set_visual_metadata(
105                    visual_in_line,
106                    line_start_offset.saturating_add(segment_start_col),
107                    line_start_offset.saturating_add(segment_end_col),
108                    segment_x_start_cells,
109                );
110                headless_line.set_fold_placeholder_appended(false);
111
112                // For collapsed folding start line, append placeholder to the last segment.
113                if visual_in_line + 1 == layout.visual_line_count
114                    && let Some(region) = Self::collapsed_region_starting_at(regions, logical_line)
115                    && !region.placeholder.is_empty()
116                {
117                    if !headless_line.cells.is_empty() {
118                        x_in_line = x_in_line.saturating_add(char_width(' '));
119                        headless_line.add_cell(Cell::with_styles(
120                            ' ',
121                            char_width(' '),
122                            vec![FOLD_PLACEHOLDER_STYLE_ID],
123                        ));
124                    }
125                    for ch in region.placeholder.chars() {
126                        let w = cell_width_at(ch, x_in_line, tab_width);
127                        x_in_line = x_in_line.saturating_add(w);
128                        headless_line.add_cell(Cell::with_styles(
129                            ch,
130                            w,
131                            vec![FOLD_PLACEHOLDER_STYLE_ID],
132                        ));
133                    }
134                    if let Some(right_bracket) = self.fold_right_boundary_bracket_char(region) {
135                        x_in_line = x_in_line.saturating_add(char_width(' '));
136                        headless_line.add_cell(Cell::with_styles(
137                            ' ',
138                            char_width(' '),
139                            vec![FOLD_PLACEHOLDER_STYLE_ID],
140                        ));
141
142                        let w = cell_width_at(right_bracket, x_in_line, tab_width);
143                        x_in_line = x_in_line.saturating_add(w);
144                        headless_line.add_cell(Cell::with_styles(
145                            right_bracket,
146                            w,
147                            vec![FOLD_PLACEHOLDER_STYLE_ID],
148                        ));
149                    }
150                    headless_line.set_fold_placeholder_appended(true);
151                }
152
153                grid.add_line(headless_line);
154                current_visual = current_visual.saturating_add(1);
155                visual_in_line = visual_in_line.saturating_add(1);
156                if visual_in_line >= span.visual_line_count {
157                    let Some(next_span) = index.next_span_after_logical_line(logical_line) else {
158                        break;
159                    };
160                    span = next_span;
161                    visual_in_line = 0;
162                }
163            }
164
165            grid
166        })
167    }
168
169    /// Get a lightweight minimap snapshot (by visual line).
170    ///
171    /// Compared with [`Self::get_headless_grid_styled`], this API returns aggregated per-line
172    /// summaries instead of per-character cells, which is intended for minimap-like overviews.
173    pub fn get_minimap_grid(&self, start_visual_row: usize, count: usize) -> MinimapGrid {
174        self.with_visual_row_index(|index| {
175            let mut grid = MinimapGrid::new(start_visual_row, count);
176            if count == 0 {
177                return grid;
178            }
179
180            let total_visual = index.total_visual_lines();
181            if start_visual_row >= total_visual {
182                return grid;
183            }
184
185            let tab_width = self.layout_engine.tab_width();
186            let end_visual = start_visual_row.saturating_add(count).min(total_visual);
187            let regions = self.folding_manager.regions();
188
189            let Some((mut span, mut visual_in_line)) = index.span_for_visual_row(start_visual_row)
190            else {
191                return grid;
192            };
193            let mut current_visual = start_visual_row;
194
195            while current_visual < end_visual {
196                let logical_line = span.logical_line;
197
198                let Some(layout) = self.layout_engine.get_line_layout(logical_line) else {
199                    let remaining_in_span = span.visual_line_count.saturating_sub(visual_in_line);
200                    current_visual = current_visual.saturating_add(remaining_in_span);
201                    let Some(next_span) = index.next_span_after_logical_line(logical_line) else {
202                        break;
203                    };
204                    span = next_span;
205                    visual_in_line = 0;
206                    continue;
207                };
208
209                let line_text = self
210                    .line_index
211                    .get_line_text(logical_line)
212                    .unwrap_or_default();
213                let line_char_len = line_text.chars().count();
214                let line_start_offset = self.line_index.position_to_char_offset(logical_line, 0);
215
216                let segment_start_col = if visual_in_line == 0 {
217                    0
218                } else {
219                    layout
220                        .wrap_points
221                        .get(visual_in_line - 1)
222                        .map(|wp| wp.char_index)
223                        .unwrap_or(0)
224                        .min(line_char_len)
225                };
226
227                let segment_end_col = if visual_in_line < layout.wrap_points.len() {
228                    layout.wrap_points[visual_in_line]
229                        .char_index
230                        .min(line_char_len)
231                } else {
232                    line_char_len
233                };
234
235                let mut total_cells = 0usize;
236                let mut non_whitespace_cells = 0usize;
237                let mut dominant_style_counts: HashMap<StyleId, usize> = HashMap::new();
238                if visual_in_line > 0 {
239                    let indent_cells = wrap_indent_cells_for_line_text(
240                        &line_text,
241                        self.layout_engine.wrap_indent(),
242                        self.viewport_width,
243                        tab_width,
244                    );
245                    total_cells = total_cells.saturating_add(indent_cells);
246                }
247                let mut x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
248
249                for (col, ch) in line_text
250                    .chars()
251                    .enumerate()
252                    .skip(segment_start_col)
253                    .take(segment_end_col.saturating_sub(segment_start_col))
254                {
255                    let offset = line_start_offset + col;
256                    let styles = self.styles_at_offset(offset);
257                    let w = cell_width_at(ch, x_in_line, tab_width);
258                    x_in_line = x_in_line.saturating_add(w);
259                    total_cells = total_cells.saturating_add(w);
260                    if !ch.is_whitespace() {
261                        non_whitespace_cells = non_whitespace_cells.saturating_add(w);
262                    }
263                    if let Some(style) = styles.first().copied() {
264                        let entry = dominant_style_counts.entry(style).or_insert(0);
265                        *entry = entry.saturating_add(w);
266                    }
267                }
268
269                let mut placeholder_appended = false;
270                if visual_in_line + 1 == layout.visual_line_count
271                    && let Some(region) = Self::collapsed_region_starting_at(regions, logical_line)
272                    && !region.placeholder.is_empty()
273                {
274                    placeholder_appended = true;
275                    if total_cells > 0 {
276                        x_in_line = x_in_line.saturating_add(char_width(' '));
277                        total_cells = total_cells.saturating_add(char_width(' '));
278                    }
279                    for ch in region.placeholder.chars() {
280                        let w = cell_width_at(ch, x_in_line, tab_width);
281                        x_in_line = x_in_line.saturating_add(w);
282                        total_cells = total_cells.saturating_add(w);
283                        if !ch.is_whitespace() {
284                            non_whitespace_cells = non_whitespace_cells.saturating_add(w);
285                        }
286                        let entry = dominant_style_counts
287                            .entry(FOLD_PLACEHOLDER_STYLE_ID)
288                            .or_insert(0);
289                        *entry = entry.saturating_add(w);
290                    }
291                    if let Some(right_bracket) = self.fold_right_boundary_bracket_char(region) {
292                        x_in_line = x_in_line.saturating_add(char_width(' '));
293                        total_cells = total_cells.saturating_add(char_width(' '));
294                        let entry = dominant_style_counts
295                            .entry(FOLD_PLACEHOLDER_STYLE_ID)
296                            .or_insert(0);
297                        *entry = entry.saturating_add(char_width(' '));
298
299                        let w = cell_width_at(right_bracket, x_in_line, tab_width);
300                        x_in_line = x_in_line.saturating_add(w);
301                        total_cells = total_cells.saturating_add(w);
302                        non_whitespace_cells = non_whitespace_cells.saturating_add(w);
303                        let entry = dominant_style_counts
304                            .entry(FOLD_PLACEHOLDER_STYLE_ID)
305                            .or_insert(0);
306                        *entry = entry.saturating_add(w);
307                    }
308                }
309
310                let dominant_style = dominant_style_counts
311                    .into_iter()
312                    .max_by(|a, b| a.1.cmp(&b.1).then_with(|| b.0.cmp(&a.0)))
313                    .map(|(style, _)| style);
314
315                grid.lines.push(MinimapLine {
316                    logical_line_index: logical_line,
317                    visual_in_logical: visual_in_line,
318                    char_offset_start: line_start_offset.saturating_add(segment_start_col),
319                    char_offset_end: line_start_offset.saturating_add(segment_end_col),
320                    total_cells,
321                    non_whitespace_cells,
322                    dominant_style,
323                    is_fold_placeholder_appended: placeholder_appended,
324                });
325
326                current_visual = current_visual.saturating_add(1);
327                visual_in_line = visual_in_line.saturating_add(1);
328                if visual_in_line >= span.visual_line_count {
329                    let Some(next_span) = index.next_span_after_logical_line(logical_line) else {
330                        break;
331                    };
332                    span = next_span;
333                    visual_in_line = 0;
334                }
335            }
336
337            grid
338        })
339    }
340
341    /// Get a decoration-aware composed grid snapshot (by composed visual line).
342    ///
343    /// This is an **optional** snapshot path that injects:
344    /// - inline virtual text (`DecorationPlacement::{Before,After}`), e.g. inlay hints
345    /// - above-line virtual text (`DecorationPlacement::AboveLine`), e.g. code lens
346    ///
347    /// Notes:
348    /// - Wrapping is still computed from the underlying document text only.
349    /// - Virtual text can therefore extend past the viewport width; hosts may clip.
350    /// - Each [`ComposedCell`] carries its origin (`Document` vs `Virtual`) so hosts can map
351    ///   interactions back to document offsets without re-implementing layout.
352    pub fn get_headless_grid_composed(
353        &self,
354        start_visual_row: usize,
355        count: usize,
356    ) -> ComposedGrid {
357        let mut grid = ComposedGrid::new(start_visual_row, count);
358        if count == 0 {
359            return grid;
360        }
361
362        #[derive(Debug, Clone)]
363        struct VirtualText {
364            anchor: usize,
365            text: String,
366            styles: Vec<StyleId>,
367        }
368
369        // Collect virtual text decorations from all layers.
370        let mut inline_before: HashMap<usize, Vec<VirtualText>> = HashMap::new();
371        let mut inline_after: HashMap<usize, Vec<VirtualText>> = HashMap::new();
372        let mut above_by_line: BTreeMap<usize, Vec<VirtualText>> = BTreeMap::new();
373
374        for decorations in self.decorations.values() {
375            for deco in decorations {
376                let Some(text) = deco.text.as_ref() else {
377                    continue;
378                };
379                if text.is_empty() {
380                    continue;
381                }
382
383                let anchor = match deco.placement {
384                    DecorationPlacement::After => deco.range.end,
385                    DecorationPlacement::Before | DecorationPlacement::AboveLine => {
386                        deco.range.start
387                    }
388                };
389                let vt = VirtualText {
390                    anchor,
391                    text: text.clone(),
392                    styles: deco.styles.clone(),
393                };
394
395                match deco.placement {
396                    DecorationPlacement::Before => {
397                        inline_before.entry(anchor).or_default().push(vt);
398                    }
399                    DecorationPlacement::After => {
400                        inline_after.entry(anchor).or_default().push(vt);
401                    }
402                    DecorationPlacement::AboveLine => {
403                        let line = self.line_index.char_offset_to_position(anchor).0;
404                        above_by_line.entry(line).or_default().push(vt);
405                    }
406                }
407            }
408        }
409
410        let regions = self.folding_manager.regions();
411        let line_count = self.layout_engine.logical_line_count();
412        let mut above_lines = Vec::new();
413        let mut above_prefix_counts = vec![0usize];
414        let mut total_above = 0usize;
415        for (&line, above) in &above_by_line {
416            if line >= line_count || Self::is_logical_line_hidden(regions, line) {
417                continue;
418            }
419            above_lines.push(line);
420            total_above = total_above.saturating_add(above.len());
421            above_prefix_counts.push(total_above);
422        }
423
424        let above_count_before_line = |line: usize| -> usize {
425            let idx = above_lines.partition_point(|candidate| *candidate < line);
426            above_prefix_counts[idx]
427        };
428
429        let (total_composed, start_logical_line, mut current_visual) =
430            self.with_visual_row_index(|index| {
431                let total_composed = index.total_visual_lines().saturating_add(total_above);
432                if line_count == 0 || start_visual_row >= total_composed {
433                    return (total_composed, line_count, total_composed);
434                }
435
436                let composed_rows_before_line = |line: usize| -> usize {
437                    index
438                        .visual_rows_before_logical_line(line)
439                        .saturating_add(above_count_before_line(line))
440                };
441
442                let mut low = 0usize;
443                let mut high = line_count;
444                while low < high {
445                    let mid = (low + high).div_ceil(2);
446                    if composed_rows_before_line(mid) <= start_visual_row {
447                        low = mid;
448                    } else {
449                        high = mid.saturating_sub(1);
450                    }
451                }
452
453                let mut start_line = low.min(line_count.saturating_sub(1));
454                if index.span_for_logical_line(start_line).is_none() {
455                    let doc_visual = index.visual_rows_before_logical_line(start_line);
456                    let Some((span, _)) = index.span_for_visual_row(doc_visual) else {
457                        return (total_composed, line_count, total_composed);
458                    };
459                    start_line = span.logical_line;
460                }
461
462                (
463                    total_composed,
464                    start_line,
465                    composed_rows_before_line(start_line),
466                )
467            });
468
469        if start_visual_row >= total_composed {
470            return grid;
471        }
472
473        let end_visual = start_visual_row.saturating_add(count).min(total_composed);
474        let tab_width = self.layout_engine.tab_width();
475
476        for logical_line in start_logical_line..line_count {
477            if Self::is_logical_line_hidden(regions, logical_line) {
478                continue;
479            }
480
481            // Above-line virtual text (e.g. code lens).
482            if let Some(above) = above_by_line.get(&logical_line) {
483                for vt in above {
484                    if current_visual >= end_visual {
485                        return grid;
486                    }
487
488                    if current_visual >= start_visual_row {
489                        let mut x_render = 0usize;
490                        let mut cells: Vec<ComposedCell> = Vec::new();
491                        for ch in vt.text.chars() {
492                            let w = cell_width_at(ch, x_render, tab_width);
493                            x_render = x_render.saturating_add(w);
494                            cells.push(ComposedCell {
495                                ch,
496                                width: w,
497                                styles: vt.styles.clone(),
498                                source: ComposedCellSource::Virtual {
499                                    anchor_offset: vt.anchor,
500                                },
501                            });
502                        }
503
504                        grid.lines.push(ComposedLine {
505                            kind: ComposedLineKind::VirtualAboveLine { logical_line },
506                            char_offset_start: vt.anchor,
507                            char_offset_end: vt.anchor,
508                            cells,
509                        });
510                    }
511
512                    current_visual = current_visual.saturating_add(1);
513                }
514            }
515
516            let Some(layout) = self.layout_engine.get_line_layout(logical_line) else {
517                continue;
518            };
519
520            let line_text = self
521                .line_index
522                .get_line_text(logical_line)
523                .unwrap_or_default();
524            let line_char_len = line_text.chars().count();
525            let line_start_offset = self.line_index.position_to_char_offset(logical_line, 0);
526
527            for visual_in_line in 0..layout.visual_line_count {
528                if current_visual >= end_visual {
529                    return grid;
530                }
531
532                if current_visual < start_visual_row {
533                    current_visual = current_visual.saturating_add(1);
534                    continue;
535                }
536
537                let segment_start_col = if visual_in_line == 0 {
538                    0
539                } else {
540                    layout
541                        .wrap_points
542                        .get(visual_in_line - 1)
543                        .map(|wp| wp.char_index)
544                        .unwrap_or(0)
545                        .min(line_char_len)
546                };
547
548                let segment_end_col = if visual_in_line < layout.wrap_points.len() {
549                    layout.wrap_points[visual_in_line]
550                        .char_index
551                        .min(line_char_len)
552                } else {
553                    line_char_len
554                };
555
556                let segment_start_offset = line_start_offset + segment_start_col;
557
558                let mut cells: Vec<ComposedCell> = Vec::new();
559
560                let mut x_render = 0usize;
561                if visual_in_line > 0 {
562                    let indent_cells = wrap_indent_cells_for_line_text(
563                        &line_text,
564                        self.layout_engine.wrap_indent(),
565                        self.viewport_width,
566                        tab_width,
567                    );
568                    x_render = x_render.saturating_add(indent_cells);
569                    for _ in 0..indent_cells {
570                        cells.push(ComposedCell {
571                            ch: ' ',
572                            width: 1,
573                            styles: Vec::new(),
574                            source: ComposedCellSource::Virtual {
575                                anchor_offset: segment_start_offset,
576                            },
577                        });
578                    }
579                }
580
581                let mut x_in_line = visual_x_for_column(&line_text, segment_start_col, tab_width);
582
583                let push_virtual = |anchor: usize,
584                                    list: &[VirtualText],
585                                    cells: &mut Vec<ComposedCell>,
586                                    x_render: &mut usize| {
587                    for vt in list {
588                        for ch in vt.text.chars() {
589                            let w = cell_width_at(ch, *x_render, tab_width);
590                            *x_render = x_render.saturating_add(w);
591                            cells.push(ComposedCell {
592                                ch,
593                                width: w,
594                                styles: vt.styles.clone(),
595                                source: ComposedCellSource::Virtual {
596                                    anchor_offset: anchor,
597                                },
598                            });
599                        }
600                    }
601                };
602
603                for (col, ch) in line_text
604                    .chars()
605                    .enumerate()
606                    .skip(segment_start_col)
607                    .take(segment_end_col.saturating_sub(segment_start_col))
608                {
609                    let offset = line_start_offset + col;
610
611                    if let Some(list) = inline_before.get(&offset) {
612                        push_virtual(offset, list, &mut cells, &mut x_render);
613                    }
614                    if let Some(list) = inline_after.get(&offset) {
615                        push_virtual(offset, list, &mut cells, &mut x_render);
616                    }
617
618                    let styles = self.styles_at_offset(offset);
619                    let w = cell_width_at(ch, x_in_line, tab_width);
620                    x_in_line = x_in_line.saturating_add(w);
621                    x_render = x_render.saturating_add(w);
622                    cells.push(ComposedCell {
623                        ch,
624                        width: w,
625                        styles,
626                        source: ComposedCellSource::Document { offset },
627                    });
628                }
629
630                // End-of-line inline virtual text (only on the last visual segment).
631                if visual_in_line + 1 == layout.visual_line_count {
632                    let eol_offset = line_start_offset + line_char_len;
633                    if let Some(list) = inline_before.get(&eol_offset) {
634                        push_virtual(eol_offset, list, &mut cells, &mut x_render);
635                    }
636                    if let Some(list) = inline_after.get(&eol_offset) {
637                        push_virtual(eol_offset, list, &mut cells, &mut x_render);
638                    }
639
640                    // For collapsed folding start line, append placeholder to the last segment.
641                    if let Some(region) = Self::collapsed_region_starting_at(regions, logical_line)
642                        && !region.placeholder.is_empty()
643                    {
644                        if !cells.is_empty() {
645                            x_render = x_render.saturating_add(char_width(' '));
646                            cells.push(ComposedCell {
647                                ch: ' ',
648                                width: char_width(' '),
649                                styles: vec![FOLD_PLACEHOLDER_STYLE_ID],
650                                source: ComposedCellSource::Virtual {
651                                    anchor_offset: eol_offset,
652                                },
653                            });
654                        }
655                        for ch in region.placeholder.chars() {
656                            let w = cell_width_at(ch, x_render, tab_width);
657                            x_render = x_render.saturating_add(w);
658                            cells.push(ComposedCell {
659                                ch,
660                                width: w,
661                                styles: vec![FOLD_PLACEHOLDER_STYLE_ID],
662                                source: ComposedCellSource::Virtual {
663                                    anchor_offset: eol_offset,
664                                },
665                            });
666                        }
667                        if let Some(right_bracket) = self.fold_right_boundary_bracket_char(region) {
668                            x_render = x_render.saturating_add(char_width(' '));
669                            cells.push(ComposedCell {
670                                ch: ' ',
671                                width: char_width(' '),
672                                styles: vec![FOLD_PLACEHOLDER_STYLE_ID],
673                                source: ComposedCellSource::Virtual {
674                                    anchor_offset: eol_offset,
675                                },
676                            });
677
678                            let w = cell_width_at(right_bracket, x_render, tab_width);
679                            cells.push(ComposedCell {
680                                ch: right_bracket,
681                                width: w,
682                                styles: vec![FOLD_PLACEHOLDER_STYLE_ID],
683                                source: ComposedCellSource::Virtual {
684                                    anchor_offset: eol_offset,
685                                },
686                            });
687                        }
688                    }
689                }
690
691                grid.lines.push(ComposedLine {
692                    kind: ComposedLineKind::Document {
693                        logical_line,
694                        visual_in_logical: visual_in_line,
695                    },
696                    char_offset_start: segment_start_offset,
697                    char_offset_end: line_start_offset + segment_end_col,
698                    cells,
699                });
700
701                current_visual = current_visual.saturating_add(1);
702            }
703        }
704
705        grid
706    }
707}