documents: table moves and tidy read apart; a code press names its text
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
0558f0c890
commit
bcee87c08f
12 files changed
+321
-106
No files matched your search
@@ -13,7 +13,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
||||
- Web views: `wry` 0.57 (Apache-2.0 OR MIT, default features off, macOS only) lays a child WKWebView over the window.
|
||||
- Terminal: `alacritty_terminal` 0.26 (Apache-2.0, default features off, native only) for the grid, its parser and the pseudo-terminal; `futures` carries its events. `vte` 0.15 (Apache-2.0 OR MIT), the parser it re-exports, reads `AnsiText`'s codes alone, so colored output builds for wasm32.
|
||||
- Diffs: `similar` 3.2 (Apache-2.0) for line and word diffs and three-way merges; its `unicode` feature splits words at punctuation.
|
||||
- Markdown: `markdown` 1.0.0 (markdown-rs, MIT) reads GitHub's flavor and math into a syntax tree with byte positions for `MarkdownRenderer`, `MarkdownEditor`, the highlights, block ranges and outline. `MarkdownRenderer` draws display math through `typography::Latex`, a ```` ```mermaid ```` block through `documents::Diagram`, and pictures from the owner's folder (`base`) or `data:` addresses (`base64` 0.22, MIT OR Apache-2.0); web pictures stay unloaded. Inline math shows as TeX: gpui text carries no inline pictures. Inline it reads as Typora's defaults: `:shortcode:` turns to its emoji as GitHub reads it (the `emojis` crate's names, GitHub's today, so `:cricket:` is the insect; read after escapes and references, so `\:smile:` turns too; inside an autolink or `<code>` it stays text; within one DOM text node, which filtered tags do not split, the first `:name:`-shaped run decides for all: blank text or a space before it turns every shortcode, else none, so `x:smile: :smile:` stays), inline HTML's `u`, `b`/`strong`, `i`/`em`, `s`/`del`/`strike`, `code`/`kbd`, `mark`, `br` and `a href` draw as what they mean GFM's filtered tags (`script`, `style`, `title`, `textarea`, `iframe`, `xmp`, `noembed`, `noframes`, `plaintext`) show as text, and other tags, hyphenated names too, vanish, leaving their text; a tag's name runs to white space or `/`, as HTML5 reads it; attribute values split on ASCII whitespace alone and decode as HTML decodes them; each Markdown container is the element GitHub writes (`em`, `strong`, `del`, `a`), on one stack with the tags and repaired as HTML5 repairs: an end tag closes the innermost element of its name and all above it, of which only formatting elements (`a`, `b`, `code`, `em`, `i`, `s`, `strike`, `strong`, `u`) reopen, a new `a` ends the open one and an `a` without `href` links nothing; other tags are not tracked, and `</br>` breaks as `<br>` does; and a footnote reference is its number in superscript, numbered by first reference, as its definition is; one `~` still strikes. At the root, blocks read as GitHub shows them: a quote whose first line is plain text reading `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]` or `[!CAUTION]` in any case, after escapes and references, spaces and tabs around it, with more after it, is an alert under its title (Important in the palette's violet, `syntax.keyword`, read at 4.5:1); a paragraph that is `[toc]` alone, as Typora reads it, lists every heading by depth, and a press on one reveals it as a link press does (`MarkdownDocument`'s `reveal`), the heading found where `CodeEditor` anchors carried it through each edit since the read, gone if an edit or an undo took it, and a secondary press acts on press, since a block writing back moves the list; front matter shows in a box without its fences; an HTML block shows its text with the inline tags above, white space collapsed and owed as a browser lays it, block elements breaking lines and closing what opened inside them, HTML5's start tags ending an open `p`, `fieldset` and `menu` too, and an `li` start an `li`, a `dd` or `dt` start either, as HTML5 ends them, comments dropped, the filtered tags shown as text, and an unclosed tag dropping the rest; a rule is a 2px `border_strong` line; bullets go •, ◦, ▪ by list depth, ordered lists counted. Exports read a tree as the drawing does through `MarkdownReader` (`block` gives its `MarkdownBlock`s, `inline` its text, `note` and `label` its footnotes, `headings` the `[toc]` list), one fresh stack per paragraph, heading or cell. `MarkdownDocument` holds a whole file in one `CodeEditor` (text and undo, colored by `CodeEditor::highlighter` with `markdown_highlights`): visual draws the file's top-level blocks in a gpui `list`, a pressed block opening as its source in a `TextInput` whose typing is the document's text at once and whose write-back is one undo step; source is the editor; preview draws without presses. A written-back block, a mode switch or a new text is read at once up to 128 KiB, longer ones on the background executor; typing in source or split reads 300 ms after the last key, off the main thread (markdown-rs grows with the square of its nodes, not its bytes: in release, 128 KiB of one-letter paragraphs takes 1.7 s, 16 KiB 42 ms, while 128 KiB of prose takes 20 ms; a block-heavy text under the cutoff can still hold the UI thread past 100 ms, a known limit). Texts past `LARGEST_READ` (2 MiB) are never read: no blocks, no outline. A new read keeps the list's blocks alike at both ends. One caret serves every mode: the editor's selection, which a written-back block moves; `set_caret` and `set_top_line` set caret and view apart; a top line asked (numbered, so a late frame callback finishes only its own ask; new text places it again, clamped to the text as it then is) waits for the read, lands on its block, then a frame later at its share of the block's height; `reveal` is such an ask with the caret at its line; a switch between source and the listLine truncated
|
||||
- Markdown: `markdown` 1.0.0 (markdown-rs, MIT) reads GitHub's flavor and math into a syntax tree with byte positions for `MarkdownRenderer`, `MarkdownEditor`, the highlights, block ranges and outline. `MarkdownRenderer` draws display math through `typography::Latex`, a ```` ```mermaid ```` block through `documents::Diagram`, and pictures from the owner's folder (`base`) or `data:` addresses (`base64` 0.22, MIT OR Apache-2.0); web pictures stay unloaded. Inline math shows as TeX: gpui text carries no inline pictures. Inline it reads as Typora's defaults: `:shortcode:` turns to its emoji as GitHub reads it (the `emojis` crate's names, GitHub's today, so `:cricket:` is the insect; read after escapes and references, so `\:smile:` turns too; inside an autolink or `<code>` it stays text; within one DOM text node, which filtered tags do not split, the first `:name:`-shaped run decides for all: blank text or a space before it turns every shortcode, else none, so `x:smile: :smile:` stays), inline HTML's `u`, `b`/`strong`, `i`/`em`, `s`/`del`/`strike`, `code`/`kbd`, `mark`, `br` and `a href` draw as what they mean GFM's filtered tags (`script`, `style`, `title`, `textarea`, `iframe`, `xmp`, `noembed`, `noframes`, `plaintext`) show as text, and other tags, hyphenated names too, vanish, leaving their text; a tag's name runs to white space or `/`, as HTML5 reads it; attribute values split on ASCII whitespace alone and decode as HTML decodes them; each Markdown container is the element GitHub writes (`em`, `strong`, `del`, `a`), on one stack with the tags and repaired as HTML5 repairs: an end tag closes the innermost element of its name and all above it, of which only formatting elements (`a`, `b`, `code`, `em`, `i`, `s`, `strike`, `strong`, `u`) reopen, a new `a` ends the open one and an `a` without `href` links nothing; other tags are not tracked, and `</br>` breaks as `<br>` does; and a footnote reference is its number in superscript, numbered by first reference, as its definition is; one `~` still strikes. At the root, blocks read as GitHub shows them: a quote whose first line is plain text reading `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]` or `[!CAUTION]` in any case, after escapes and references, spaces and tabs around it, with more after it, is an alert under its title (Important in the palette's violet, `syntax.keyword`, read at 4.5:1); a paragraph that is `[toc]` alone, as Typora reads it, lists every heading by depth, and a press on one reveals it as a link press does (`MarkdownDocument`'s `reveal`), the heading found where `CodeEditor` anchors carried it through each edit since the read, gone if an edit or an undo took it, and a secondary press acts on press, since a block writing back moves the list; front matter shows in a box without its fences; an HTML block shows its text with the inline tags above, white space collapsed and owed as a browser lays it, block elements breaking lines and closing what opened inside them, HTML5's start tags ending an open `p`, `fieldset` and `menu` too, and an `li` start an `li`, a `dd` or `dt` start either, as HTML5 ends them, comments dropped, the filtered tags shown as text, and an unclosed tag dropping the rest; a rule is a 2px `border_strong` line; bullets go •, ◦, ▪ by list depth, ordered lists counted. Exports read a tree as the drawing does through `MarkdownReader` (`block` gives its `MarkdownBlock`s, `inline` its text, `note` and `label` its footnotes, `headings` the `[toc]` list), one fresh stack per paragraph, heading or cell. `MarkdownDocument` holds a whole file in one `CodeEditor` (text and undo, colored by `CodeEditor::highlighter` with `markdown_highlights`): visual draws the file's top-level blocks in a gpui `list`, a pressed block opening as its source in a `TextInput` whose typing is the document's text at once and whose write-back is one undo step; source is the editor; preview draws without presses. A written-back block, a mode switch or a new text is read at once up to 128 KiB, longer ones on the background executor; typing in source or split reads 300 ms after the last key, off the main thread (markdown-rs grows with the square of its nodes, not its bytes: in release, 128 KiB of one-letter paragraphs takes 1.7 s, 16 KiB 42 ms, while 128 KiB of prose takes 20 ms; a block-heavy text under the cutoff can still hold the UI thread past 100 ms, a known limit). Texts past `LARGEST_READ` (2 MiB) are never read: no blocks, no outline. A new read keeps the list's blocks alike at both ends. One caret serves every mode: the editor's selection, which a written-back block moves; `set_caret` and `set_top_line` set caret and view apart; a top line asked (numbered, so a late frame callback finishes only its own ask; new text places it again, clamped to the text as it then is) waits for the read, lands on its block, then a frame later at its share of the block's height; `reveal` is such an ask with the caret at its line; a switch between source and the listLine truncated
|
||||
- Pictures: `image` 0.25 (MIT OR Apache-2.0), the crate gpui decodes with, default features off; it turns decoded frames and rims map tiles.
|
||||
- Math and diagrams: RaTeX 0.1.14 (`ratex-parser`, `ratex-layout`, `ratex-svg` with `embed-fonts`, `ratex-types`; MIT, KaTeX's fonts OFL) typesets `Latex` to SVG with outlined glyphs; `mermaid-rs-renderer` 0.3.1 (MIT, default features off) draws `Diagram`. Both go through gpui's `SvgRenderer` at the window's scale, loaded as gpui assets off the main thread; `usvg` 0.46, gpui's own, reads a diagram's size. A RaTeX or Mermaid panic on parsed input becomes the element's error. Each element holds its picture through `primitives::held`: a new source, or the element leaving the window, removes the old asset from gpui's cache and drops its image from the atlas after the frame.
|
||||
- Maps: Natural Earth's 110m countries (public domain). `assets/maps/countries.geojson`, trimmed by `scripts/countries.py` to an ISO code, a name and positions to 0.01°, backs `maps::WorldMap`; the gallery's tiles are cut from the same file, z0 to z3, by `scripts/tiles.py` with Pillow.
|
||||
|
||||
Generated
+1
@@ -1685,6 +1685,7 @@ dependencies = [
|
||||
"similar",
|
||||
"smallvec",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
"usvg",
|
||||
"vte",
|
||||
"wasm-bindgen",
|
||||
|
||||
@@ -43,6 +43,7 @@ serde_json = { version = "1", features = ["preserve_order"] }
|
||||
similar = { version = "3.2", default-features = false, features = ["std", "text", "inline", "unicode"] }
|
||||
smallvec = "1"
|
||||
unicode-segmentation = "1"
|
||||
unicode-width = "0.2"
|
||||
usvg = "0.46"
|
||||
vte = { version = "0.15", default-features = false, features = ["std", "ansi"] }
|
||||
web-time = "1.1"
|
||||
|
||||
@@ -8,6 +8,7 @@ use super::{
|
||||
DocumentEvent, LARGEST_READ, MarkdownDocument,
|
||||
block::{self, BlockCommand},
|
||||
format::{Change, EditRefusal, InlineFormat, toggle},
|
||||
tables::{self, TableEdit},
|
||||
};
|
||||
use crate::documents::{MarkdownMode, modes::joint};
|
||||
|
||||
@@ -41,6 +42,21 @@ impl MarkdownDocument {
|
||||
)
|
||||
}
|
||||
|
||||
/// `edit` on the table at the caret, read apart.
|
||||
pub fn edit_table(
|
||||
&mut self,
|
||||
edit: TableEdit,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<(), EditRefusal> {
|
||||
self.apart(
|
||||
format!("table edit {edit:?}"),
|
||||
move |full, selection| {
|
||||
tables::edit_table(full, selection.start, edit).map_err(EditRefusal::Cells)
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
/// Reads `read` off the UI thread, then writes its change.
|
||||
fn apart(
|
||||
&mut self,
|
||||
|
||||
@@ -6,6 +6,8 @@ use std::ops::Range;
|
||||
|
||||
use markdown::mdast::Node;
|
||||
|
||||
use super::TableRefusal;
|
||||
|
||||
use crate::documents::{
|
||||
markdown::{file_tree, span, walk},
|
||||
tree::inside,
|
||||
@@ -50,6 +52,8 @@ pub enum EditRefusal {
|
||||
Changed,
|
||||
/// The result would read as other text or blocks.
|
||||
Misread,
|
||||
/// A table edit refused, and why.
|
||||
Cells(TableRefusal),
|
||||
}
|
||||
|
||||
/// The edits and the selection after them.
|
||||
|
||||
@@ -61,7 +61,8 @@ pub struct DocumentTarget {
|
||||
pub link: Option<String>,
|
||||
pub image: Option<String>,
|
||||
pub in_table: bool,
|
||||
pub in_code: bool,
|
||||
/// The code block's text, pressed in one.
|
||||
pub code: Option<String>,
|
||||
}
|
||||
|
||||
impl MarkdownDocument {
|
||||
@@ -115,6 +116,12 @@ impl MarkdownDocument {
|
||||
};
|
||||
let kind = block.map(|block| block.kind);
|
||||
let pressed = block.map(|block| self.drawn(block));
|
||||
let code = (pointed.code.as_deref().map(str::to_string)).or_else(|| {
|
||||
match block.and_then(|block| block.nodes.as_deref()) {
|
||||
Some([Node::Code { text, .. }]) => Some(text.clone()),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
let at = at.or(block.map(|block| block.range.start));
|
||||
let written = self.commit(cx);
|
||||
if written.is_some() {
|
||||
@@ -149,7 +156,7 @@ impl MarkdownDocument {
|
||||
link: pointed.link,
|
||||
image: pointed.image,
|
||||
in_table: pointed.table || kind == Some(TargetBlock::Table),
|
||||
in_code: pointed.code || kind == Some(TargetBlock::Code),
|
||||
code,
|
||||
}));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
//! Tables change by rows and columns (MARK-06).
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::Context;
|
||||
use markdown::mdast::{self, AlignKind};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use super::MarkdownDocument;
|
||||
use super::{
|
||||
MarkdownDocument,
|
||||
format::{Change, EditRefusal},
|
||||
};
|
||||
use crate::documents::{
|
||||
MarkdownMode,
|
||||
markdown::{span, tree, walk},
|
||||
markdown::{file_tree, span, walk},
|
||||
};
|
||||
|
||||
/// What a table edit does at the caret.
|
||||
@@ -20,11 +22,14 @@ pub enum TableEdit {
|
||||
ColumnRight,
|
||||
DeleteRow,
|
||||
DeleteColumn,
|
||||
MoveRowUp,
|
||||
MoveRowDown,
|
||||
MoveColumnLeft,
|
||||
MoveColumnRight,
|
||||
/// Every cell padded to its column, as Typora tidies.
|
||||
Tidy,
|
||||
}
|
||||
|
||||
/// A new table: three columns, a head and two rows.
|
||||
const NEW: &str = "| | | |\n| --- | --- | --- |\n| | | |\n| | | |";
|
||||
|
||||
/// Why a table edit did not happen.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TableRefusal {
|
||||
@@ -36,17 +41,13 @@ pub enum TableRefusal {
|
||||
Beyond,
|
||||
/// Its lines carry different quote or list marks.
|
||||
Mixed,
|
||||
/// Preview takes no edits.
|
||||
Preview,
|
||||
/// Nothing lies past the caret's row or column that way.
|
||||
Edge,
|
||||
}
|
||||
|
||||
/// The table around `at` after `edit`: its range, text, caret.
|
||||
pub(super) fn edit_table(
|
||||
text: &str,
|
||||
at: usize,
|
||||
edit: TableEdit,
|
||||
) -> Result<(Range<usize>, String, usize), TableRefusal> {
|
||||
let root = tree(text);
|
||||
/// The table around `at` after `edit`, and the caret.
|
||||
pub(super) fn edit_table(text: &str, at: usize, edit: TableEdit) -> Result<Change, TableRefusal> {
|
||||
let root = file_tree(text);
|
||||
let mut nodes = Vec::new();
|
||||
walk(&root, &mut nodes);
|
||||
let (range, table) = nodes
|
||||
@@ -72,10 +73,12 @@ pub(super) fn edit_table(
|
||||
.rposition(|row| span(row).start <= at)
|
||||
.unwrap_or(0);
|
||||
let cells_of = |row: &mdast::Node| row.children().expect("a row holds cells").clone();
|
||||
let column = cells_of(rows[row])
|
||||
let cells = cells_of(rows[row]);
|
||||
let column = cells
|
||||
.iter()
|
||||
.rposition(|cell| span(cell).start <= at)
|
||||
.unwrap_or(0);
|
||||
let offset = cells.get(column).map_or(0, |cell| within(cell, at));
|
||||
let width = table.align.len();
|
||||
// Cells past the head stay as written.
|
||||
let mut grid: Vec<Vec<String>> = rows
|
||||
@@ -94,40 +97,69 @@ pub(super) fn edit_table(
|
||||
let mut aligns = table.align.clone();
|
||||
let column_edit = matches!(
|
||||
edit,
|
||||
TableEdit::ColumnLeft | TableEdit::ColumnRight | TableEdit::DeleteColumn
|
||||
TableEdit::ColumnLeft
|
||||
| TableEdit::ColumnRight
|
||||
| TableEdit::DeleteColumn
|
||||
| TableEdit::MoveColumnLeft
|
||||
| TableEdit::MoveColumnRight
|
||||
);
|
||||
if column_edit && column >= width {
|
||||
return Err(TableRefusal::Beyond);
|
||||
}
|
||||
let (row, column) = match edit {
|
||||
let caret = match edit {
|
||||
TableEdit::RowAbove | TableEdit::RowBelow => {
|
||||
let below = edit == TableEdit::RowBelow || row == 0;
|
||||
let at = if below { row + 1 } else { row };
|
||||
grid.insert(at, vec![String::new(); width]);
|
||||
(at, column.min(width - 1))
|
||||
(at, column.min(width - 1), 0)
|
||||
}
|
||||
TableEdit::DeleteRow if row > 0 => {
|
||||
grid.remove(row);
|
||||
(row.min(grid.len() - 1), column.min(width - 1))
|
||||
(row.min(grid.len() - 1), column.min(width - 1), 0)
|
||||
}
|
||||
TableEdit::ColumnLeft | TableEdit::ColumnRight => {
|
||||
let at = column + usize::from(edit == TableEdit::ColumnRight);
|
||||
grid.iter_mut()
|
||||
.for_each(|cells| cells.insert(at, String::new()));
|
||||
aligns.insert(at, AlignKind::None);
|
||||
(row, at)
|
||||
(row, at, 0)
|
||||
}
|
||||
TableEdit::DeleteColumn if width > 1 => {
|
||||
grid.iter_mut().for_each(|cells| {
|
||||
cells.remove(column);
|
||||
});
|
||||
aligns.remove(column);
|
||||
(row, column.min(width - 2))
|
||||
(row, column.min(width - 2), 0)
|
||||
}
|
||||
TableEdit::MoveRowUp | TableEdit::MoveRowDown => {
|
||||
let to = match edit {
|
||||
TableEdit::MoveRowUp => row.checked_sub(1),
|
||||
_ => Some(row + 1).filter(|to| *to < grid.len()),
|
||||
}
|
||||
.ok_or(TableRefusal::Edge)?;
|
||||
// GFM reads a head of other width as no table.
|
||||
if row.min(to) == 0 && grid[row.max(to)].len() > width {
|
||||
return Err(TableRefusal::Beyond);
|
||||
}
|
||||
grid.swap(row, to);
|
||||
(to, column, offset)
|
||||
}
|
||||
TableEdit::MoveColumnLeft | TableEdit::MoveColumnRight => {
|
||||
let to = match edit {
|
||||
TableEdit::MoveColumnLeft => column.checked_sub(1),
|
||||
_ => Some(column + 1).filter(|to| *to < width),
|
||||
}
|
||||
.ok_or(TableRefusal::Edge)?;
|
||||
grid.iter_mut().for_each(|cells| cells.swap(column, to));
|
||||
aligns.swap(column, to);
|
||||
(row, to, offset)
|
||||
}
|
||||
TableEdit::Tidy => (row, column, offset),
|
||||
_ => return Err(TableRefusal::Kept),
|
||||
};
|
||||
let (out, caret) = table_text(&grid, &aligns, (row, column), prefix);
|
||||
Ok((range, out, caret))
|
||||
let (out, caret) = table_text(&grid, &aligns, caret, prefix);
|
||||
let caret = range.start + caret;
|
||||
Ok((vec![(range, out)], caret..caret))
|
||||
}
|
||||
|
||||
/// A cell's source between its pipes.
|
||||
@@ -139,37 +171,69 @@ fn written(text: &str, cell: &mdast::Node) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// The table's text, each line after the first under `prefix`.
|
||||
/// `at`'s byte in `cell`'s written text.
|
||||
fn within(cell: &mdast::Node, at: usize) -> usize {
|
||||
let inside = cell.children().expect("a cell holds inlines");
|
||||
match (inside.first(), inside.last()) {
|
||||
(Some(first), Some(last)) => {
|
||||
let start = span(first).start;
|
||||
at.clamp(start, span(last).end) - start
|
||||
}
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// The table's text and the caret at row, column, byte.
|
||||
fn table_text(
|
||||
grid: &[Vec<String>],
|
||||
aligns: &[AlignKind],
|
||||
at: (usize, usize),
|
||||
at: (usize, usize, usize),
|
||||
prefix: &str,
|
||||
) -> (String, usize) {
|
||||
// Typora pads columns 4 to 60 display columns wide.
|
||||
let widths: Vec<usize> = (0..aligns.len())
|
||||
.map(|column| {
|
||||
(grid.iter())
|
||||
.map(|cells| cells[column].width())
|
||||
.fold(4, usize::max)
|
||||
.min(60)
|
||||
})
|
||||
.collect();
|
||||
let mut out = String::new();
|
||||
let mut caret = 0;
|
||||
for (row, cells) in grid.iter().enumerate() {
|
||||
out.push('|');
|
||||
for (column, cell) in cells.iter().enumerate() {
|
||||
out.push_str("| ");
|
||||
if (row, column) == at {
|
||||
caret = out.len();
|
||||
let room = widths
|
||||
.get(column)
|
||||
.map_or(0, |width| width.saturating_sub(cell.width()));
|
||||
let before = match aligns.get(column) {
|
||||
Some(AlignKind::Right) => room,
|
||||
Some(AlignKind::Center) => room / 2,
|
||||
_ => 0,
|
||||
};
|
||||
out.push(' ');
|
||||
out.push_str(&" ".repeat(before));
|
||||
if (row, column) == (at.0, at.1) {
|
||||
caret = out.len() + at.2;
|
||||
}
|
||||
out.push_str(cell);
|
||||
out.push(' ');
|
||||
out.push_str(&" ".repeat(room - before));
|
||||
out.push_str(" |");
|
||||
}
|
||||
out.push('|');
|
||||
if row == 0 {
|
||||
out.push('\n');
|
||||
out.push_str(prefix);
|
||||
for align in aligns {
|
||||
out.push_str(match align {
|
||||
AlignKind::Left => "| :-- ",
|
||||
AlignKind::Right => "| --: ",
|
||||
AlignKind::Center => "| :-: ",
|
||||
AlignKind::None => "| --- ",
|
||||
});
|
||||
}
|
||||
out.push('|');
|
||||
for (align, width) in aligns.iter().zip(&widths) {
|
||||
let dashes = match align {
|
||||
AlignKind::Left => format!(":{}", "-".repeat(width - 1)),
|
||||
AlignKind::Right => format!("{}:", "-".repeat(width - 1)),
|
||||
AlignKind::Center => format!(":{}:", "-".repeat(width - 2)),
|
||||
AlignKind::None => "-".repeat(*width),
|
||||
};
|
||||
out.push_str(&format!(" {dashes} |"));
|
||||
}
|
||||
}
|
||||
if row + 1 < grid.len() {
|
||||
out.push('\n');
|
||||
@@ -180,34 +244,10 @@ fn table_text(
|
||||
}
|
||||
|
||||
impl MarkdownDocument {
|
||||
/// `edit` on the table at the caret, or why not.
|
||||
pub fn edit_table(
|
||||
&mut self,
|
||||
edit: TableEdit,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<(), TableRefusal> {
|
||||
if self.mode == MarkdownMode::Preview {
|
||||
return Err(TableRefusal::Preview);
|
||||
}
|
||||
self.commit(cx);
|
||||
let at = self.editor.read(cx).primary().range().start;
|
||||
let text = self.editor.read(cx).text().to_string();
|
||||
let (range, table, caret) = edit_table(&text, at, edit).inspect_err(|why| {
|
||||
log::info!("markdown document: table edit {edit:?} refused: {why:?}");
|
||||
})?;
|
||||
log::info!("markdown document: table edit {edit:?}");
|
||||
let caret = range.start + caret;
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.edit([(range, table)], cx);
|
||||
editor.select(std::iter::once(caret..caret), cx);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A new table after the caret's line, unless in preview.
|
||||
pub fn insert_table(&mut self, cx: &mut Context<Self>) -> Result<(), TableRefusal> {
|
||||
pub fn insert_table(&mut self, cx: &mut Context<Self>) -> Result<(), EditRefusal> {
|
||||
if self.mode == MarkdownMode::Preview {
|
||||
return Err(TableRefusal::Preview);
|
||||
return Err(EditRefusal::Preview);
|
||||
}
|
||||
self.commit(cx);
|
||||
let text = self.editor.read(cx).text().to_string();
|
||||
@@ -230,11 +270,12 @@ impl MarkdownDocument {
|
||||
} else {
|
||||
"\n"
|
||||
};
|
||||
let written = format!("{before}{NEW}{after}");
|
||||
let caret = from + before.len() + 2;
|
||||
let grid = vec![vec![String::new(); 3]; 3];
|
||||
let (table, caret) = table_text(&grid, &[AlignKind::None; 3], (0, 0, 0), "");
|
||||
let caret = from + before.len() + caret;
|
||||
log::info!("markdown document: a table goes in");
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.edit([(from..end, written)], cx);
|
||||
editor.edit([(from..end, format!("{before}{table}{after}"))], cx);
|
||||
editor.select(std::iter::once(caret..caret), cx);
|
||||
});
|
||||
Ok(())
|
||||
@@ -247,42 +288,93 @@ mod tests {
|
||||
|
||||
const TABLE: &str = "Intro\n\n| A | B |\n|:--|--:|\n| 1 | 2 |\n| 3 | 4 |\n\nAfter\n";
|
||||
|
||||
/// `text` after `edit` at `at`, the caret as `‸`.
|
||||
fn edited_in(text: &str, at: &str, edit: TableEdit) -> Result<String, TableRefusal> {
|
||||
let at = text.find(at).expect("in the table");
|
||||
let (range, table, _) = edit_table(text, at, edit)?;
|
||||
Ok(format!(
|
||||
"{}{table}{}",
|
||||
&text[..range.start],
|
||||
&text[range.end..]
|
||||
))
|
||||
let (edits, caret) = edit_table(text, at, edit)?;
|
||||
let (range, table) = &edits[0];
|
||||
let out = format!("{}{table}{}", &text[..range.start], &text[range.end..]);
|
||||
Ok(format!("{}‸{}", &out[..caret.start], &out[caret.start..]))
|
||||
}
|
||||
|
||||
fn edited(at: &str, edit: TableEdit) -> Result<String, TableRefusal> {
|
||||
edited_in(TABLE, at, edit)
|
||||
}
|
||||
|
||||
fn around(table: &str) -> Result<String, TableRefusal> {
|
||||
Ok(format!("Intro\n\n{table}\n\nAfter\n"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows_and_columns_come_and_go() {
|
||||
let around = |table: &str| Ok(format!("Intro\n\n{table}\n\nAfter\n"));
|
||||
assert_eq!(
|
||||
edited("3", TableEdit::RowAbove),
|
||||
around("| A | B |\n| :-- | --: |\n| 1 | 2 |\n| | |\n| 3 | 4 |")
|
||||
around(
|
||||
"| A | B |\n| :--- | ---: |\n| 1 | 2 |\n| ‸ | |\n| 3 | 4 |"
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
edited("A", TableEdit::RowBelow),
|
||||
around("| A | B |\n| :-- | --: |\n| | |\n| 1 | 2 |\n| 3 | 4 |")
|
||||
around(
|
||||
"| A | B |\n| :--- | ---: |\n| ‸ | |\n| 1 | 2 |\n| 3 | 4 |"
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
edited("2", TableEdit::ColumnLeft),
|
||||
around("| A | | B |\n| :-- | --- | --: |\n| 1 | | 2 |\n| 3 | | 4 |")
|
||||
around(
|
||||
"| A | | B |\n| :--- | ---- | ---: |\n| 1 | ‸ | 2 |\n| 3 | | 4 |"
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
edited("1", TableEdit::DeleteRow),
|
||||
around("| A | B |\n| :-- | --: |\n| 3 | 4 |")
|
||||
around("| A | B |\n| :--- | ---: |\n| ‸3 | 4 |")
|
||||
);
|
||||
assert_eq!(
|
||||
edited("B", TableEdit::DeleteColumn),
|
||||
around("| A |\n| :-- |\n| 1 |\n| 3 |")
|
||||
around("| ‸A |\n| :--- |\n| 1 |\n| 3 |")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows_and_columns_move_with_the_caret() {
|
||||
assert_eq!(
|
||||
edited("3", TableEdit::MoveRowUp),
|
||||
around("| A | B |\n| :--- | ---: |\n| ‸3 | 4 |\n| 1 | 2 |")
|
||||
);
|
||||
assert_eq!(
|
||||
edited("A", TableEdit::MoveRowDown),
|
||||
around("| 1 | 2 |\n| :--- | ---: |\n| ‸A | B |\n| 3 | 4 |")
|
||||
);
|
||||
assert_eq!(
|
||||
edited("2", TableEdit::MoveColumnLeft),
|
||||
around("| B | A |\n| ---: | :--- |\n| ‸2 | 1 |\n| 4 | 3 |")
|
||||
);
|
||||
assert_eq!(edited("A", TableEdit::MoveRowUp), Err(TableRefusal::Edge));
|
||||
assert_eq!(edited("3", TableEdit::MoveRowDown), Err(TableRefusal::Edge));
|
||||
assert_eq!(
|
||||
edited("1", TableEdit::MoveColumnLeft),
|
||||
Err(TableRefusal::Edge)
|
||||
);
|
||||
assert_eq!(
|
||||
edited("4", TableEdit::MoveColumnRight),
|
||||
Err(TableRefusal::Edge)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tidy_pads_by_display_width_and_keeps_the_caret() {
|
||||
let text = "| 名前 | x |\n|:-:|-|\n| 長い名前です | yy‸z |\n";
|
||||
let at = text.find('‸').unwrap();
|
||||
let text = text.replace('‸', "");
|
||||
let (edits, caret) = edit_table(&text, at, TableEdit::Tidy).unwrap();
|
||||
let out = format!(
|
||||
"{}‸{}",
|
||||
&edits[0].1[..caret.start],
|
||||
&edits[0].1[caret.start..]
|
||||
);
|
||||
assert_eq!(
|
||||
out,
|
||||
"| 名前 | x |\n| :----------: | ---- |\n| 長い名前です | yy‸z |"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -307,9 +399,18 @@ mod tests {
|
||||
edited_in(text, "3", TableEdit::DeleteColumn),
|
||||
Err(TableRefusal::Beyond)
|
||||
);
|
||||
assert_eq!(
|
||||
edited_in(text, "3", TableEdit::MoveColumnLeft),
|
||||
Err(TableRefusal::Beyond)
|
||||
);
|
||||
assert_eq!(
|
||||
edited_in(text, "1", TableEdit::MoveRowUp),
|
||||
Err(TableRefusal::Beyond),
|
||||
"a head wider than its delimiter is no table"
|
||||
);
|
||||
assert_eq!(
|
||||
edited_in(text, "1", TableEdit::RowBelow),
|
||||
Ok("| A | B |\n| --- | --- |\n| 1 | 2 | 3 |\n| | |\n".into())
|
||||
Ok("| A | B |\n| ---- | ---- |\n| 1 | 2 | 3 |\n| ‸ | |\n".into())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -318,7 +419,10 @@ mod tests {
|
||||
let text = "> | A | B |\n> | - | - |\n> | 1 | 2 |\n";
|
||||
assert_eq!(
|
||||
edited_in(text, "1", TableEdit::RowBelow),
|
||||
Ok("> | A | B |\n> | --- | --- |\n> | 1 | 2 |\n> | | |\n".into())
|
||||
Ok(
|
||||
"> | A | B |\n> | ---- | ---- |\n> | 1 | 2 |\n> | ‸ | |\n"
|
||||
.into()
|
||||
)
|
||||
);
|
||||
let mixed = "- | A |\n | - |\n | 1 |\n";
|
||||
assert_eq!(
|
||||
|
||||
@@ -277,7 +277,7 @@ fn a_format_lapses_when_the_text_moves(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
/// The refusals a document sends by event.
|
||||
fn refusals(
|
||||
pub(super) fn refusals(
|
||||
document: &Entity<MarkdownDocument>,
|
||||
cx: &mut VisualTestContext,
|
||||
) -> Rc<RefCell<Vec<EditRefusal>>> {
|
||||
|
||||
@@ -72,7 +72,7 @@ pub(super) fn target(at: usize, block: TargetBlock) -> DocumentTarget {
|
||||
link: None,
|
||||
image: None,
|
||||
in_table: false,
|
||||
in_code: false,
|
||||
code: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ fn a_code_press_lands_in_its_code(cx: &mut TestAppContext) {
|
||||
let heard = heard(&document, cx);
|
||||
press_on(0, 0, MouseButton::Right, cx);
|
||||
let expected = DocumentTarget {
|
||||
in_code: true,
|
||||
code: Some("let x = 1;".into()),
|
||||
..target("```rust\n".len(), TargetBlock::Code)
|
||||
};
|
||||
assert_eq!(*heard.borrow(), [expected]);
|
||||
@@ -245,8 +245,9 @@ fn code_presses_land_line_for_line(cx: &mut TestAppContext) {
|
||||
let events = heard(&document, cx);
|
||||
press_lines(1, 1, MouseButton::Right, cx);
|
||||
let at = source.find("eta").unwrap();
|
||||
let eol = if source.contains('\r') { "\r\n" } else { "\n" };
|
||||
let expected = DocumentTarget {
|
||||
in_code: true,
|
||||
code: Some(format!("alpha{eol}beta")),
|
||||
..target(at, TargetBlock::Code)
|
||||
};
|
||||
assert_eq!(*events.borrow(), [expected], "{source:?}");
|
||||
@@ -304,7 +305,7 @@ beta
|
||||
press_lines(2, 0, MouseButton::Right, cx);
|
||||
let at = source.find("pha").unwrap();
|
||||
let expected = DocumentTarget {
|
||||
in_code: true,
|
||||
code: Some("alpha\nbeta".into()),
|
||||
..target(at, TargetBlock::Code)
|
||||
};
|
||||
assert_eq!(*events.borrow(), [expected]);
|
||||
@@ -392,3 +393,22 @@ fn an_html_space_moved_by_a_tag_presses_on_its_word(cx: &mut TestAppContext) {
|
||||
let at = source.find("eta").unwrap();
|
||||
assert_eq!(*events.borrow(), [target(at, TargetBlock::Html)]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_press_beside_the_code_still_names_it(cx: &mut TestAppContext) {
|
||||
let source = "```\nlet x;\n```\n";
|
||||
let (document, cx) = open(source, MarkdownMode::Visual, cx);
|
||||
let events = heard(&document, cx);
|
||||
let bounds = cx.debug_bounds("markdown-block-0").expect("drawn");
|
||||
press_at(
|
||||
bounds.left() + px(4.0),
|
||||
bounds.top() + px(4.0),
|
||||
MouseButton::Right,
|
||||
cx,
|
||||
);
|
||||
let code = events
|
||||
.borrow()
|
||||
.first()
|
||||
.and_then(|target| target.code.clone());
|
||||
assert_eq!(code.as_deref(), Some("let x;"));
|
||||
}
|
||||
@@ -47,7 +47,11 @@ fn a_table_edit_is_one_undo_step_at_the_caret(cx: &mut TestAppContext) {
|
||||
document.edit_table(crate::documents::TableEdit::RowBelow, cx)
|
||||
});
|
||||
assert_eq!(edited, Ok(()));
|
||||
assert_eq!(text(&document, cx), "| A |\n| --- |\n| 1 |\n| |\n");
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
text(&document, cx),
|
||||
"| A |\n| ---- |\n| 1 |\n| |\n"
|
||||
);
|
||||
let editor = document.read_with(cx, |document, _| document.editor().clone());
|
||||
editor.update(cx, |editor, cx| editor.undo(cx));
|
||||
assert_eq!(text(&document, cx), "| A |\n| - |\n| 1 |\n");
|
||||
@@ -61,7 +65,7 @@ fn a_new_table_goes_after_the_caret_line(cx: &mut TestAppContext) {
|
||||
});
|
||||
assert_eq!(
|
||||
text(&document, cx),
|
||||
"Intro\n\n| | | |\n| --- | --- | --- |\n| | | |\n| | | |\n\nNext\n"
|
||||
"Intro\n\n| | | |\n| ---- | ---- | ---- |\n| | | |\n| | | |\n\nNext\n"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -143,7 +147,7 @@ fn preview_takes_no_edits(cx: &mut TestAppContext) {
|
||||
document.insert_table(cx),
|
||||
)
|
||||
});
|
||||
let preview = Err(crate::documents::TableRefusal::Preview);
|
||||
let preview = Err(crate::documents::EditRefusal::Preview);
|
||||
assert_eq!(refused, (false, preview, preview));
|
||||
assert_eq!(text(&document, cx), "| A |\n| - |\n| 1 |\n");
|
||||
}
|
||||
@@ -247,5 +251,58 @@ fn a_table_on_an_indented_blank_line_is_a_table(cx: &mut TestAppContext) {
|
||||
document.edit_table(crate::documents::TableEdit::ColumnRight, cx)
|
||||
});
|
||||
assert_eq!(column, Ok(()), "{}", text(&document, cx));
|
||||
cx.run_until_parked();
|
||||
assert!(text(&document, cx).starts_with("| "));
|
||||
}
|
||||
|
||||
const SMALL: &str = "| A | B |\n| - | - |\n| 1 | 2 |\n";
|
||||
|
||||
#[gpui::test]
|
||||
fn a_table_move_reads_apart_as_one_undo_step(cx: &mut TestAppContext) {
|
||||
let (document, cx) = open(SMALL, MarkdownMode::Source, cx);
|
||||
document.update(cx, |document, cx| document.set_caret(2, 2, cx));
|
||||
let moved = document.update(cx, |document, cx| {
|
||||
document.edit_table(crate::documents::TableEdit::MoveRowUp, cx)
|
||||
});
|
||||
assert_eq!(moved, Ok(()));
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
text(&document, cx),
|
||||
"| 1 | 2 |\n| ---- | ---- |\n| A | B |\n"
|
||||
);
|
||||
let editor = document.read_with(cx, |document, _| document.editor().clone());
|
||||
editor.update(cx, |editor, cx| editor.undo(cx));
|
||||
assert_eq!(text(&document, cx), SMALL);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_table_edit_at_an_edge_refuses_by_event(cx: &mut TestAppContext) {
|
||||
let (document, cx) = open(SMALL, MarkdownMode::Source, cx);
|
||||
let heard = super::format::refusals(&document, cx);
|
||||
document.update(cx, |document, cx| document.set_caret(2, 2, cx));
|
||||
document.update(cx, |document, cx| {
|
||||
document
|
||||
.edit_table(crate::documents::TableEdit::MoveRowDown, cx)
|
||||
.expect("taken")
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let edge = crate::documents::EditRefusal::Cells(crate::documents::TableRefusal::Edge);
|
||||
assert_eq!(*heard.borrow(), [edge]);
|
||||
assert_eq!(text(&document, cx), SMALL);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn tidy_in_an_open_table_keeps_it_open(cx: &mut TestAppContext) {
|
||||
let (document, cx) = open(SMALL, MarkdownMode::Visual, cx);
|
||||
press(0, Modifiers::none(), cx);
|
||||
document.update(cx, |document, cx| {
|
||||
document
|
||||
.edit_table(crate::documents::TableEdit::Tidy, cx)
|
||||
.expect("taken")
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
opened(&document, cx),
|
||||
"| A | B |\n| ---- | ---- |\n| 1 | 2 |"
|
||||
);
|
||||
}
|
||||
@@ -264,7 +264,7 @@ impl Draw<'_> {
|
||||
} => {
|
||||
if language.as_deref() == Some("mermaid") {
|
||||
let diagram = Diagram::new(self.key("diagram"), text.clone());
|
||||
return self.point_code(diagram.into_any_element(), None, *from);
|
||||
return self.point_code(diagram.into_any_element(), None, text, *from);
|
||||
}
|
||||
if let Some(draw) = self.code {
|
||||
return draw(
|
||||
@@ -299,7 +299,7 @@ impl Draw<'_> {
|
||||
}))
|
||||
.child(code)
|
||||
.into_any_element();
|
||||
self.point_code(block, Some((layout, text)), *from)
|
||||
self.point_code(block, Some(layout), text, *from)
|
||||
}
|
||||
Node::List { start, items } => self.list(start, items, cx),
|
||||
Node::Table { aligns, head, rows } => {
|
||||
|
||||
@@ -29,7 +29,8 @@ pub(crate) struct Pointed {
|
||||
pub place: Place,
|
||||
pub link: Option<String>,
|
||||
pub image: Option<String>,
|
||||
pub code: bool,
|
||||
/// The code's text, pressed in a code block.
|
||||
pub code: Option<Rc<str>>,
|
||||
pub table: bool,
|
||||
}
|
||||
|
||||
@@ -44,7 +45,7 @@ impl Pointed {
|
||||
place,
|
||||
link: None,
|
||||
image: None,
|
||||
code: false,
|
||||
code: None,
|
||||
table: false,
|
||||
}
|
||||
}
|
||||
@@ -100,7 +101,7 @@ pub(crate) fn point_lines(
|
||||
.index_for_position(event.position)
|
||||
.unwrap_or_else(|near| near);
|
||||
Pointed {
|
||||
code,
|
||||
code: code.then(|| text.clone()),
|
||||
..Pointed::at(Place::Lines {
|
||||
text: text.clone(),
|
||||
from,
|
||||
@@ -146,12 +147,13 @@ impl Draw<'_> {
|
||||
pub(super) fn point_code(
|
||||
&self,
|
||||
element: AnyElement,
|
||||
drawn: Option<(TextLayout, &str)>,
|
||||
layout: Option<TextLayout>,
|
||||
text: &str,
|
||||
from: usize,
|
||||
) -> AnyElement {
|
||||
let table = self.table.get();
|
||||
match drawn {
|
||||
Some((layout, text)) => point_lines(
|
||||
match layout {
|
||||
Some(layout) => point_lines(
|
||||
element,
|
||||
layout,
|
||||
text,
|
||||
@@ -159,10 +161,13 @@ impl Draw<'_> {
|
||||
self.hooks.on_point,
|
||||
(table, true),
|
||||
),
|
||||
None => self.pointable(element, |_| Pointed {
|
||||
code: true,
|
||||
..Pointed::block()
|
||||
}),
|
||||
None => {
|
||||
let text: Rc<str> = Rc::from(text);
|
||||
self.pointable(element, move |_| Pointed {
|
||||
code: Some(text.clone()),
|
||||
..Pointed::block()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user