Skip to main content

markdown_editor/
table.rs

1//! Table cell editing (Phase 4c): cell navigation, `BlockSelection::Cells`,
2//! and row/column commands. Cells are ordinary inline leaves; a table is
3//! `Table > TableRow > TableCell`, the first row being the header.
4
5use super::{
6    command::CommandError,
7    document::{BlockContent, EditorDocument},
8    editor::BlockEditorModel,
9    id::BlockId,
10    inline::InlineText,
11    kind::BlockKindData,
12    position::{BlockPosition, PositionError},
13    selection::BlockSelection,
14    step::DetachedBlock,
15    transaction::EditOrigin,
16};
17use gpui_kit::base::text::editor_host::ColumnAlign;
18
19/// A table's cells as rows.
20pub(crate) fn grid(doc: &EditorDocument, table: BlockId) -> Vec<Vec<BlockId>> {
21    doc.children(table)
22        .iter()
23        .map(|row| doc.children(*row).to_vec())
24        .collect()
25}
26
27pub(crate) fn table_of(doc: &EditorDocument, cell: BlockId) -> Option<BlockId> {
28    if doc.kind(cell) != Some(&BlockKindData::TableCell) {
29        return None;
30    }
31    doc.path(cell)
32        .into_iter()
33        .rev()
34        .find(|id| matches!(doc.kind(*id), Some(BlockKindData::Table { .. })))
35}
36
37/// `(row, column)` of a cell in its table's grid.
38pub(crate) fn cell_coords(
39    doc: &EditorDocument,
40    table: BlockId,
41    cell: BlockId,
42) -> Option<(usize, usize)> {
43    grid(doc, table)
44        .iter()
45        .enumerate()
46        .find_map(|(r, row)| row.iter().position(|c| *c == cell).map(|c| (r, c)))
47}
48
49fn empty_cell() -> DetachedBlock {
50    DetachedBlock::leaf(
51        BlockKindData::TableCell,
52        BlockContent::Inline(InlineText::new()),
53    )
54}
55
56fn empty_row(cols: usize) -> DetachedBlock {
57    DetachedBlock::container(
58        BlockKindData::TableRow,
59        (0..cols).map(|_| empty_cell()).collect(),
60    )
61}
62
63fn aligns(doc: &EditorDocument, table: BlockId) -> Vec<ColumnAlign> {
64    match doc.kind(table) {
65        Some(BlockKindData::Table { aligns }) => aligns.clone(),
66        _ => Vec::new(),
67    }
68}
69
70/// Which table edit to make at the caret's cell, or across the rows and
71/// columns of a cell selection (D20).
72#[derive(Clone, Copy, PartialEq, Debug)]
73pub enum TableEdit {
74    /// Insert an empty row above the topmost selected row.
75    AddRowAbove,
76    /// Insert an empty row below the bottommost selected row.
77    AddRowBelow,
78    /// Remove the selected row(s); removing every row removes the table.
79    RemoveRow,
80    /// Insert an empty column left of the leftmost selected column.
81    AddColumnLeft,
82    /// Insert an empty column right of the rightmost selected column.
83    AddColumnRight,
84    /// Remove the selected column(s); removing every column removes the table.
85    RemoveColumn,
86    /// Set the alignment of the selected column(s).
87    SetColumnAlign(ColumnAlign),
88}
89
90/// A table row or column, as a grip names it.
91#[derive(Clone, Copy, PartialEq, Eq, Debug)]
92pub enum TableLine {
93    Row(usize),
94    Column(usize),
95}
96
97impl BlockEditorModel {
98    /// The cell holding the caret / selection head, and its table.
99    pub(crate) fn current_cell(&self) -> Option<(BlockId, BlockId)> {
100        let cell = match self.selection() {
101            BlockSelection::Text { head, .. } => head.block,
102            BlockSelection::Cells { head, .. } => *head,
103            BlockSelection::Blocks { .. } => return None,
104        };
105        Some((table_of(&self.doc, cell)?, cell))
106    }
107
108    /// The cells of a `BlockSelection::Cells` rectangle, row by row.
109    pub fn selected_cells(&self) -> Option<Vec<Vec<BlockId>>> {
110        let BlockSelection::Cells { anchor, head } = self.selection() else {
111            return None;
112        };
113        let table = table_of(&self.doc, *anchor)?;
114        let (ar, ac) = cell_coords(&self.doc, table, *anchor)?;
115        let (hr, hc) = cell_coords(&self.doc, table, *head)?;
116        let grid = grid(&self.doc, table);
117        Some(
118            (ar.min(hr)..=ar.max(hr))
119                .map(|r| {
120                    (ac.min(hc)..=ac.max(hc))
121                        .filter_map(|c| grid[r].get(c).copied())
122                        .collect()
123                })
124                .collect(),
125        )
126    }
127
128    /// Clear the contents of the selected cells (one undo step); the caret
129    /// goes to the first of them.
130    pub(crate) fn clear_cells(&mut self, origin: EditOrigin) -> Result<bool, CommandError> {
131        let Some(cells) = self.selected_cells() else {
132            return Ok(false);
133        };
134        let first = cells.first().and_then(|r| r.first()).copied();
135        self.transact(origin, |tx| {
136            for cell in cells.iter().flatten() {
137                let len = tx.doc.leaf_len(*cell).unwrap_or(0);
138                if len > 0 {
139                    tx.replace_text(*cell, 0..len, InlineText::new())?;
140                }
141            }
142            Ok(first.map(|c| BlockSelection::caret(BlockPosition::new(c, 0))))
143        })?;
144        if let Some(first) = first {
145            // A no-op transaction leaves the selection: collapse it anyway.
146            let _ = self.set_selection(BlockSelection::caret(BlockPosition::new(first, 0)));
147        }
148        Ok(true)
149    }
150
151    /// The selected cells as a Markdown table (header = first selected row).
152    pub(crate) fn cells_markdown(&self) -> Option<String> {
153        let cells = self.selected_cells()?;
154        let cell_text = |id: &BlockId| {
155            let inline = self.doc.inline(*id).cloned().unwrap_or_default();
156            super::serialize::inline_markdown(&inline)
157        };
158        let mut out = String::new();
159        for (r, row) in cells.iter().enumerate() {
160            let line: Vec<String> = row.iter().map(cell_text).collect();
161            out.push_str(&format!("| {} |\n", line.join(" | ")));
162            if r == 0 {
163                let sep: Vec<&str> = row.iter().map(|_| "---").collect();
164                out.push_str(&format!("| {} |\n", sep.join(" | ")));
165            }
166        }
167        Some(out.trim_end().to_string())
168    }
169
170    /// Tab / Shift+Tab in a cell: move to the next / previous cell (all of
171    /// its text selected, like spreadsheets do). Tab in the last cell adds
172    /// a row. Returns `false` when the caret is not in a cell.
173    pub(crate) fn tab_cell(&mut self, back: bool) -> Result<bool, CommandError> {
174        let Some((table, cell)) = self.current_cell() else {
175            return Ok(false);
176        };
177        let cells: Vec<BlockId> = grid(&self.doc, table).into_iter().flatten().collect();
178        let Some(ix) = cells.iter().position(|c| *c == cell) else {
179            return Ok(false);
180        };
181        let target = if back {
182            match ix.checked_sub(1) {
183                Some(i) => cells[i],
184                None => return Ok(true),
185            }
186        } else if let Some(next) = cells.get(ix + 1) {
187            *next
188        } else {
189            let cols = self
190                .doc
191                .children(*self.doc.children(table).last().unwrap_or(&table))
192                .len();
193            let row = empty_row(cols.max(1));
194            let first = match &row.content {
195                super::step::DetachedContent::Children(c) => c[0].id,
196                _ => unreachable!(),
197            };
198            let rows = self.doc.children(table).len();
199            self.transact(EditOrigin::Command("table_add_row"), |tx| {
200                tx.insert(table, rows, vec![row])?;
201                Ok(Some(BlockSelection::caret(BlockPosition::new(first, 0))))
202            })?;
203            return Ok(true);
204        };
205        let len = self.doc.leaf_len(target).unwrap_or(0);
206        self.set_selection(BlockSelection::Text {
207            anchor: BlockPosition::new(target, 0),
208            head: BlockPosition::new(target, len),
209        })?;
210        Ok(true)
211    }
212
213    /// Insert an empty `rows` × `cols` table (header row included) after the
214    /// caret's top-level-sibling block, with the caret in its first cell.
215    pub(crate) fn insert_table(&mut self, rows: usize, cols: usize) -> Result<bool, CommandError> {
216        let pos = self
217            .caret()
218            .ok_or(CommandError::Unsupported("block selection"))?;
219        if table_of(&self.doc, pos.block).is_some() {
220            return Err(CommandError::Unsupported("table inside a table"));
221        }
222        let (rows, cols) = (rows.max(1), cols.max(1));
223        let table = DetachedBlock::container(
224            BlockKindData::Table {
225                aligns: vec![ColumnAlign::default(); cols],
226            },
227            (0..rows).map(|_| empty_row(cols)).collect(),
228        );
229        let mut ids = Vec::new();
230        table.ids(&mut ids);
231        let first = ids[2];
232        let empty = self.doc.leaf_len(pos.block) == Some(0)
233            && self.doc.kind(pos.block) == Some(&BlockKindData::Paragraph);
234        self.transact(EditOrigin::Command("insert_table"), |tx| {
235            let (parent, index) = tx
236                .doc
237                .index_in_parent(pos.block)
238                .ok_or(PositionError::UnknownBlock(pos.block))?;
239            tx.insert(parent, index + 1, vec![table])?;
240            // An empty paragraph is replaced by the table.
241            if empty {
242                tx.remove_block(pos.block)?;
243            }
244            Ok(Some(BlockSelection::caret(BlockPosition::new(first, 0))))
245        })?;
246        Ok(true)
247    }
248
249    /// The table and the rows / columns the selection spans: the cell
250    /// rectangle of a cell selection, or the caret's cell.
251    pub(crate) fn table_span(
252        &self,
253    ) -> Option<(
254        BlockId,
255        std::ops::RangeInclusive<usize>,
256        std::ops::RangeInclusive<usize>,
257    )> {
258        let (table, head) = self.current_cell()?;
259        let (hr, hc) = cell_coords(&self.doc, table, head)?;
260        let (ar, ac) = match self.selection() {
261            BlockSelection::Cells { anchor, .. } => cell_coords(&self.doc, table, *anchor)?,
262            BlockSelection::Text { anchor, .. } if anchor.block == head => (hr, hc),
263            BlockSelection::Text { anchor, .. } => {
264                cell_coords(&self.doc, table, anchor.block).unwrap_or((hr, hc))
265            }
266            BlockSelection::Blocks { .. } => return None,
267        };
268        Some((table, ar.min(hr)..=ar.max(hr), ac.min(hc)..=ac.max(hc)))
269    }
270
271    /// Whether the selection is whole column(s): a cell selection over
272    /// every row of its table.
273    pub fn whole_columns_selected(&self) -> bool {
274        matches!(self.selection(), BlockSelection::Cells { .. })
275            && self.table_span().is_some_and(|(table, rows, _)| {
276                *rows.start() == 0 && *rows.end() + 1 == self.doc.children(table).len()
277            })
278    }
279
280    /// The alignment of the column holding the selection head.
281    pub fn column_align_at_caret(&self) -> Option<ColumnAlign> {
282        let (table, _, cols) = self.table_span()?;
283        Some(
284            aligns(&self.doc, table)
285                .get(*cols.start())
286                .copied()
287                .unwrap_or_default(),
288        )
289    }
290
291    /// Select a whole row or column of `table` as `BlockSelection::Cells`.
292    pub fn select_table_line(
293        &mut self,
294        table: BlockId,
295        line: TableLine,
296    ) -> Result<bool, CommandError> {
297        let grid = grid(&self.doc, table);
298        let (anchor, head) = match line {
299            TableLine::Row(r) => {
300                let Some(row) = grid.get(r) else {
301                    return Ok(false);
302                };
303                match (row.first(), row.last()) {
304                    (Some(a), Some(h)) => (*a, *h),
305                    _ => return Ok(false),
306                }
307            }
308            TableLine::Column(c) => {
309                let (Some(a), Some(h)) = (
310                    grid.first().and_then(|r| r.get(c)),
311                    grid.last().and_then(|r| r.get(c)),
312                ) else {
313                    return Ok(false);
314                };
315                (*a, *h)
316            }
317        };
318        self.set_selection(BlockSelection::Cells { anchor, head })?;
319        Ok(true)
320    }
321
322    /// Add or remove rows or columns, or align columns, at the caret's cell
323    /// or across the cell selection (one undo step). Removing every row or
324    /// every column removes the table.
325    pub(crate) fn edit_table(&mut self, edit: TableEdit) -> Result<bool, CommandError> {
326        let Some((table, rsel, csel)) = self.table_span() else {
327            return Err(CommandError::Unsupported("caret is not in a table"));
328        };
329        let (r0, r1, c0, c1) = (*rsel.start(), *rsel.end(), *csel.start(), *csel.end());
330        let grid = grid(&self.doc, table);
331        let rows = grid.len();
332        let cols = grid.iter().map(Vec::len).max().unwrap_or(0);
333        let row_ids = self.doc.children(table).to_vec();
334        let mut aligns = aligns(&self.doc, table);
335        aligns.resize(cols, ColumnAlign::default());
336        let remove_table = matches!(edit, TableEdit::RemoveRow if r0 == 0 && r1 + 1 >= rows)
337            || matches!(edit, TableEdit::RemoveColumn if c0 == 0 && c1 + 1 >= cols);
338        if remove_table {
339            self.set_selection(BlockSelection::Blocks {
340                anchor: table,
341                head: table,
342            })?;
343            return self.run(super::command::EditorCommand::DeleteSelection);
344        }
345        let r = r0;
346        let c = c0;
347        let cell_at = |row: usize, col: usize| -> BlockId {
348            let row = &grid[row.min(rows - 1)];
349            row[col.min(row.len().saturating_sub(1))]
350        };
351        self.transact(EditOrigin::Command("table_edit"), |tx| {
352            let caret_cell = match edit {
353                TableEdit::AddRowAbove | TableEdit::AddRowBelow => {
354                    let at = if edit == TableEdit::AddRowBelow {
355                        r1 + 1
356                    } else {
357                        r0
358                    };
359                    let row = empty_row(cols);
360                    let mut ids = Vec::new();
361                    row.ids(&mut ids);
362                    tx.insert(table, at, vec![row])?;
363                    ids[1 + c.min(cols - 1)]
364                }
365                TableEdit::RemoveRow => {
366                    tx.remove(table, r0..r1 + 1)?;
367                    if r1 + 1 < rows {
368                        cell_at(r1 + 1, c)
369                    } else {
370                        cell_at(r0 - 1, c)
371                    }
372                }
373                TableEdit::AddColumnLeft | TableEdit::AddColumnRight => {
374                    let at = if edit == TableEdit::AddColumnRight {
375                        c1 + 1
376                    } else {
377                        c0
378                    };
379                    let mut caret_cell = cell_at(r, c);
380                    for (ri, row) in row_ids.iter().enumerate() {
381                        let new = empty_cell();
382                        if ri == r {
383                            caret_cell = new.id;
384                        }
385                        let at = at.min(tx.doc.children(*row).len());
386                        tx.insert(*row, at, vec![new])?;
387                    }
388                    aligns.insert(at.min(aligns.len()), ColumnAlign::default());
389                    tx.set_kind(
390                        table,
391                        BlockKindData::Table {
392                            aligns: aligns.clone(),
393                        },
394                    )?;
395                    caret_cell
396                }
397                TableEdit::RemoveColumn => {
398                    for row in &row_ids {
399                        let len = tx.doc.children(*row).len();
400                        if c0 < len {
401                            tx.remove(*row, c0..(c1 + 1).min(len))?;
402                        }
403                    }
404                    aligns.drain(c0..(c1 + 1).min(aligns.len()));
405                    tx.set_kind(
406                        table,
407                        BlockKindData::Table {
408                            aligns: aligns.clone(),
409                        },
410                    )?;
411                    if c1 + 1 < cols {
412                        cell_at(r, c1 + 1)
413                    } else {
414                        cell_at(r, c0 - 1)
415                    }
416                }
417                TableEdit::SetColumnAlign(align) => {
418                    for a in &mut aligns[c0..=c1.min(cols - 1)] {
419                        *a = align;
420                    }
421                    tx.set_kind(
422                        table,
423                        BlockKindData::Table {
424                            aligns: aligns.clone(),
425                        },
426                    )?;
427                    // Keep the selection (a column stays selected).
428                    return Ok(None);
429                }
430            };
431            Ok(Some(BlockSelection::caret(BlockPosition::new(
432                caret_cell, 0,
433            ))))
434        })?;
435        Ok(true)
436    }
437
438    /// Enter in a cell (D13). Returns `false` when the caret is not in a
439    /// cell. Each branch is one undo step:
440    /// - a body row whose cells are all empty: remove it and leave the
441    ///   table to a new paragraph after it;
442    /// - the last row: append an empty row, caret in its first cell;
443    /// - any other row: the same column of the next row.
444    pub(crate) fn table_enter(&mut self) -> Result<bool, CommandError> {
445        let Some((table, cell)) = self.current_cell() else {
446            return Ok(false);
447        };
448        let Some((r, c)) = cell_coords(&self.doc, table, cell) else {
449            return Ok(false);
450        };
451        let grid = grid(&self.doc, table);
452        let row_empty = grid[r]
453            .iter()
454            .all(|id| self.doc.leaf_len(*id).unwrap_or(0) == 0);
455        if r > 0 && row_empty {
456            let para = empty_paragraph();
457            let target = para.id;
458            self.transact(EditOrigin::Command("table_exit_row"), |tx| {
459                tx.remove(table, r..r + 1)?;
460                let (parent, index) = tx
461                    .doc
462                    .index_in_parent(table)
463                    .ok_or(PositionError::UnknownBlock(table))?;
464                tx.insert(parent, index + 1, vec![para])?;
465                Ok(Some(BlockSelection::caret(BlockPosition::new(target, 0))))
466            })?;
467            return Ok(true);
468        }
469        if r + 1 == grid.len() {
470            let cols = grid.iter().map(Vec::len).max().unwrap_or(1).max(1);
471            let row = empty_row(cols);
472            let mut ids = Vec::new();
473            row.ids(&mut ids);
474            let first = ids[1];
475            let rows = grid.len();
476            self.transact(EditOrigin::Command("table_add_row"), |tx| {
477                tx.insert(table, rows, vec![row])?;
478                Ok(Some(BlockSelection::caret(BlockPosition::new(first, 0))))
479            })?;
480            return Ok(true);
481        }
482        let next = &grid[r + 1];
483        let target = next[c.min(next.len().saturating_sub(1))];
484        self.set_selection(BlockSelection::caret(BlockPosition::new(target, 0)))?;
485        Ok(true)
486    }
487
488    /// Shift+Enter in a cell: a soft line break (`<br>` in Markdown).
489    pub(crate) fn cell_soft_break(&mut self) -> Result<bool, CommandError> {
490        let Some((_, cell)) = self.current_cell() else {
491            return Ok(false);
492        };
493        let BlockSelection::Text { anchor, head } = self.selection().clone() else {
494            return Ok(false);
495        };
496        if anchor.block != cell || head.block != cell {
497            return Ok(false);
498        }
499        let (lo, hi) = (
500            anchor.offset.min(head.offset),
501            anchor.offset.max(head.offset),
502        );
503        let marks = self
504            .doc
505            .inline(cell)
506            .and_then(|i| i.mark_before(lo).cloned())
507            .unwrap_or_default();
508        self.transact(EditOrigin::Command("cell_soft_break"), |tx| {
509            tx.replace_text(cell, lo..hi, InlineText::plain("\n", marks))?;
510            Ok(Some(BlockSelection::caret(BlockPosition::new(
511                cell,
512                lo + 1,
513            ))))
514        })?;
515        Ok(true)
516    }
517
518    /// Down past the last row or Cmd/Ctrl+Enter: the caret goes to the
519    /// text block after the table, creating an empty paragraph (one undo
520    /// step) when there is none. Returns `false` outside a table.
521    pub(crate) fn exit_table(&mut self) -> Result<bool, CommandError> {
522        let Some((table, _)) = self.current_cell() else {
523            return Ok(false);
524        };
525        let Some((parent, index)) = self.doc.index_in_parent(table) else {
526            return Ok(false);
527        };
528        if let Some(next) = self.doc.children(parent).get(index + 1).copied()
529            && self.doc.inline(next).is_some()
530        {
531            self.set_selection(BlockSelection::caret(BlockPosition::new(next, 0)))?;
532            return Ok(true);
533        }
534        let para = empty_paragraph();
535        let target = para.id;
536        self.transact(EditOrigin::Command("table_exit"), |tx| {
537            tx.insert(parent, index + 1, vec![para])?;
538            Ok(Some(BlockSelection::caret(BlockPosition::new(target, 0))))
539        })?;
540        Ok(true)
541    }
542
543    /// Whether the caret is in the last row of a table.
544    pub(crate) fn in_last_table_row(&self) -> bool {
545        self.current_cell().is_some_and(|(table, cell)| {
546            cell_coords(&self.doc, table, cell)
547                .is_some_and(|(r, _)| r + 1 == self.doc.children(table).len())
548        })
549    }
550
551    /// The table input rule (D13): a paragraph `| a | b |` becomes a table
552    /// with those cells as the header row and one empty body row, caret in
553    /// its first cell; no alignment. One undo step. Returns `false` when
554    /// `block` is not such a paragraph.
555    pub(crate) fn paragraph_to_table(&mut self, block: BlockId) -> Result<bool, CommandError> {
556        if self.doc.kind(block) != Some(&BlockKindData::Paragraph)
557            || table_of(&self.doc, block).is_some()
558        {
559            return Ok(false);
560        }
561        let Some(inline) = self.doc.inline(block).cloned() else {
562            return Ok(false);
563        };
564        let Some(ranges) = table_row_cells(inline.text()) else {
565            return Ok(false);
566        };
567        let cols = ranges.len();
568        let header = DetachedBlock::container(
569            BlockKindData::TableRow,
570            ranges
571                .into_iter()
572                .map(|r| {
573                    DetachedBlock::leaf(
574                        BlockKindData::TableCell,
575                        BlockContent::Inline(inline.slice(r)),
576                    )
577                })
578                .collect(),
579        );
580        let body = empty_row(cols);
581        let mut ids = Vec::new();
582        body.ids(&mut ids);
583        let first = ids[1];
584        let table = DetachedBlock::container(
585            BlockKindData::Table {
586                aligns: vec![ColumnAlign::None; cols],
587            },
588            vec![header, body],
589        );
590        self.transact(EditOrigin::InputRule("table"), |tx| {
591            let (parent, index) = tx
592                .doc
593                .index_in_parent(block)
594                .ok_or(PositionError::UnknownBlock(block))?;
595            tx.remove_block(block)?;
596            tx.insert(parent, index, vec![table])?;
597            Ok(Some(BlockSelection::caret(BlockPosition::new(first, 0))))
598        })?;
599        Ok(true)
600    }
601}
602
603fn empty_paragraph() -> DetachedBlock {
604    DetachedBlock::leaf(
605        BlockKindData::Paragraph,
606        BlockContent::Inline(InlineText::new()),
607    )
608}
609
610/// The trimmed byte ranges of the cells of a `| a | b |` line, if it is one
611/// (starts and ends with `|`, at least one cell; `\|` does not split).
612pub(crate) fn table_row_cells(text: &str) -> Option<Vec<std::ops::Range<usize>>> {
613    let start = text.len() - text.trim_start().len();
614    let end = text.trim_end().len();
615    let line = &text[start..end];
616    if line.len() < 2 || !line.starts_with('|') || !line.ends_with('|') || line.contains('\n') {
617        return None;
618    }
619    let mut bars = Vec::new();
620    let bytes = text.as_bytes();
621    for i in start..end {
622        if bytes[i] == b'|' && (i == 0 || bytes[i - 1] != b'\\') {
623            bars.push(i);
624        }
625    }
626    if bars.len() < 2 {
627        return None;
628    }
629    Some(
630        bars.windows(2)
631            .map(|w| {
632                let (a, b) = (w[0] + 1, w[1]);
633                let cell = &text[a..b];
634                let lead = cell.len() - cell.trim_start().len();
635                a + lead..a + cell.trim_end().len()
636            })
637            .map(|r| if r.start > r.end { r.start..r.start } else { r })
638            .collect(),
639    )
640}