diff --git a/README.md b/README.md index 2e2804d..30f8f47 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,15 @@ Terminal=false | `Ctrl/Cmd + E` | `inline code` | | `Ctrl/Cmd + Shift + X` | ~~strikethrough~~ (`~~…~~`) | | `Ctrl/Cmd + K` | link — `[selection](url)`, with `url` selected to replace | + | `Ctrl/Cmd + /` | comment the selected lines out (``), or take it back off | + + `Ctrl/Cmd + /` is the odd one out: it works on whole lines, growing the + range out to line boundaries first, and it toggles the block off again + only when every non-blank line in it is already commented. Commented lines + stay in the file but drop out of the export and the word count, so this is + the way to shelve a paragraph without losing it. Blank lines in the range + are left as-is so paragraph breaks survive; pressing it on an empty line + opens `` with the caret inside. Press **`Ctrl/Cmd + F`** (or **Edit ▸ Find / Replace…**) to open the find/replace bar above the editor; **`Ctrl/Cmd + H`** opens it with the diff --git a/src/app/editor.rs b/src/app/editor.rs index 8b426e4..3eec541 100644 --- a/src/app/editor.rs +++ b/src/app/editor.rs @@ -108,7 +108,8 @@ impl App { } // Markdown formatting hotkeys, applied to the current selection // while the editor is focused (Ctrl/Cmd + B / I / E / K, and - // Ctrl/Cmd+Shift+X for strikethrough). + // Ctrl/Cmd+Shift+X for strikethrough). Ctrl/Cmd + / is in here + // too, though it rewrites whole lines rather than the selection. if output.response.has_focus() { if let Some(fmt) = ui.input_mut(detect_format_hotkey) { let sel = output @@ -433,6 +434,9 @@ pub(super) enum Fmt { Code, Strike, Link, + /// Whole-line `` comments, toggled over every line the selection + /// touches rather than wrapped around the selection itself. + Comment, } impl Fmt { @@ -444,6 +448,8 @@ impl Fmt { Fmt::Code => ("`", "`"), Fmt::Strike => ("~~", "~~"), Fmt::Link => ("[", "](url)"), + // Unused: `Comment` is line-based and never reaches the wrap path. + Fmt::Comment => (COMMENT_OPEN, COMMENT_CLOSE), } } } @@ -469,6 +475,9 @@ pub(super) fn detect_format_hotkey(input: &mut egui::InputState) -> Option if input.consume_key(cmd, Key::K) { return Some(Fmt::Link); } + if input.consume_key(cmd, Key::Slash) { + return Some(Fmt::Comment); + } None } @@ -495,12 +504,16 @@ pub(super) fn byte_to_char(s: &str, byte: usize) -> usize { /// inside it, or immediately surrounding it — the markers are removed; otherwise /// they are added (an empty selection just drops the markers with the caret /// between them). A link wraps the selection as the link text and selects the -/// `url` placeholder so it can be replaced. +/// `url` placeholder so it can be replaced. [`Fmt::Comment`] is the exception: +/// it works on whole lines, so it hands straight off to [`toggle_line_comment`]. pub(super) fn apply_format( text: &str, sel: std::ops::Range, fmt: Fmt, ) -> (String, std::ops::Range) { + if fmt == Fmt::Comment { + return toggle_line_comment(text, sel); + } let (prefix, suffix) = fmt.markers(); let start = sel.start.min(sel.end); let end = sel.start.max(sel.end); @@ -553,6 +566,116 @@ pub(super) fn apply_format( } } +/// The markers Ctrl+/ writes. The app already treats `` as editorial +/// notes — [`crate::preprocess`] strips them from the export and the word count +/// — so commenting a line hides it from the manuscript, not from a compiler. +const COMMENT_OPEN: &str = ""; + +/// Split a line into its leading whitespace and everything after it. +fn split_indent(line: &str) -> (&str, &str) { + line.split_at(line.len() - line.trim_start().len()) +} + +/// Is this whole line a comment, i.e. one Ctrl+/ can take back off? The length +/// test keeps the two markers from overlapping in a bare `\nlast"); + // It rides the inserted "\n\ngamma"); + // It ends up covering exactly the two lines it touched. + let picked = &text[char_to_byte(&text, sel.start)..char_to_byte(&text, sel.end)]; + assert_eq!(picked, "\n"); + } + + #[test] + fn comment_leaves_blank_lines_alone_between_commented_ones() { + // The blank line has to survive, or the paragraph break is lost. + let (text, _) = apply_format("one\n\ntwo", 0..8, Fmt::Comment); + assert_eq!(text, "\n\n"); + } + + #[test] + fn a_mixed_block_comments_rather_than_uncomments() { + // One line already commented, one not: adding wins, so a second press + // takes the whole block back off again. + let (text, _) = apply_format("\ntwo", 0..16, Fmt::Comment); + assert_eq!(text, " -->\n"); + let (back, _) = apply_format(&text, 0..34, Fmt::Comment); + assert_eq!(back, "\ntwo"); + } + + #[test] + fn comment_preserves_indentation() { + let (text, _) = apply_format(" indented", 0..0, Fmt::Comment); + assert_eq!(text, " "); + assert_eq!(apply_format(&text, 0..0, Fmt::Comment).0, " indented"); + } + + #[test] + fn comment_on_a_blank_line_opens_an_empty_note() { + let (text, sel) = apply_format("a\n\nb", 2..2, Fmt::Comment); + assert_eq!(text, "a\n\nb"); + // The caret lands between the two padding spaces, ready to type. + assert_eq!(&text[..char_to_byte(&text, sel.start)], "a\n"); + assert_eq!(apply_format(&text, 0..0, Fmt::Comment).0, "café au lait"); + } + + #[test] + fn commented_prose_is_dropped_from_the_exported_body() { + // The whole point of the hotkey: preprocess must not see the hidden line. + let md = "### Rough Draft:\n\nKeep this.\n\n\n"; + let body = crate::preprocess::parse(md, "### Rough Draft:").body; + assert!(body.contains("Keep this.")); + assert!(!body.contains("Cut this.")); + } + #[test] fn format_respects_multibyte_char_offsets() { // "café " is 5 chars but 6 bytes; selecting "word" (chars 5..9) must diff --git a/src/help.rs b/src/help.rs index 51c3cc1..8053cc9 100644 --- a/src/help.rs +++ b/src/help.rs @@ -64,6 +64,7 @@ fn cheatsheet_body(ui: &mut egui::Ui) { ("Ctrl/Cmd + E", "Inline code (`…`)."), ("Ctrl/Cmd + Shift + X", "Strikethrough (~~…~~)."), ("Ctrl/Cmd + K", "Wrap as a link and select the url to replace."), + ("Ctrl/Cmd + /", "Comment out the lines you have selected, or take it back off."), ("Ctrl/Cmd + F", "Open the find bar."), ("Ctrl/Cmd + H", "Open find/replace (replace field focused)."), ], @@ -72,7 +73,9 @@ fn cheatsheet_body(ui: &mut egui::Ui) { ui, "Wrapping keys apply to the selected text (or the caret, for an empty \ selection); pressing the same key on already-wrapped text removes the \ - markers. In the find bar, Enter / Shift+Enter step through matches and \ + markers. Ctrl/Cmd + / works on whole lines instead, hiding them from the \ + export and the word count without deleting them; blank lines in the range \ + are left alone. In the find bar, Enter / Shift+Enter step through matches and \ Esc closes it.", );