Add action to toggle block quotes in Markdown (#58383)
This PR adds an action to toggle block quotes on and off for the selected lines while in Markdown. I'm using my own binding in the demo video, this PR does not bind it to anything. https://github.com/user-attachments/assets/5c1f4121-e863-459d-8a9b-8eefdbfb888e When there's a mixture of quoted and "unquoted" lines, we normalize to quoted: https://github.com/user-attachments/assets/ca003844-0c4f-4cae-a7d0-a9d355258f2d While adding this, I noticed that `editor.rs` had been through some refactoring in order to reduce its size. Because of this, I introduced `markdown_actions.rs`, as I intend to follow up with some other markdown toggling actions (eg: `markdown: toggle bold`, `markdown: toggle italic`, etc.) The implementation reuses the existing `manipulate_mutable_lines` code that all line manipulating actions run through. Self-Review Checklist: - [X] I've reviewed my own diff for quality, security, and reliability - [X] Unsafe blocks (if any) have justifying comments - [X] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [X] Tests cover the new/changed behavior - [X] Performance impact has been considered and is acceptable Release Notes: - Added a `markdown: toggle block quote` action
This commit is contained in:
1 parent
fb8da5b73d
commit
24f4b66fa6
5 files changed
+163
No files matched your search
@@ -397,6 +397,15 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
actions!(
|
||||
markdown,
|
||||
[
|
||||
/// Toggles a block quote (`> `) prefix on the selected lines (or the
|
||||
/// current line) while in Markdown files.
|
||||
ToggleBlockQuote,
|
||||
]
|
||||
);
|
||||
|
||||
actions!(
|
||||
editor,
|
||||
[
|
||||
|
||||
@@ -66,6 +66,7 @@ mod config;
|
||||
mod diagnostics;
|
||||
mod edit_prediction;
|
||||
mod input;
|
||||
mod markdown_actions;
|
||||
mod navigation;
|
||||
mod rewrap;
|
||||
mod selection;
|
||||
|
||||
@@ -38027,3 +38027,99 @@ async fn test_toggle_diagnostics_persists_across_settings_change(cx: &mut TestAp
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggle_markdown_block_quote(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
// No-op with no language
|
||||
cx.set_state(indoc! {"
|
||||
«helloˇ» world
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.toggle_markdown_block_quote(&ToggleBlockQuote, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
«helloˇ» world
|
||||
"});
|
||||
|
||||
// No-op in non-Markdown language (Rust)
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
|
||||
cx.set_state(indoc! {"
|
||||
«helloˇ» world
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.toggle_markdown_block_quote(&ToggleBlockQuote, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
«helloˇ» world
|
||||
"});
|
||||
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_lang()), cx));
|
||||
|
||||
// Line is quoted with an empty selection
|
||||
cx.set_state(indoc! {"
|
||||
helˇlo world
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.toggle_markdown_block_quote(&ToggleBlockQuote, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
«> hello worldˇ»
|
||||
"});
|
||||
|
||||
// Line is unquoted with an empty selection
|
||||
cx.update_editor(|e, window, cx| e.toggle_markdown_block_quote(&ToggleBlockQuote, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
«hello worldˇ»
|
||||
"});
|
||||
|
||||
// Multi-line selection is quoted, including blank lines
|
||||
cx.set_state(indoc! {"
|
||||
«first
|
||||
|
||||
thirdˇ»
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.toggle_markdown_block_quote(&ToggleBlockQuote, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
«> first
|
||||
>
|
||||
> thirdˇ»
|
||||
"});
|
||||
|
||||
// Multi-line selection is unquoted, including blank lines
|
||||
cx.update_editor(|e, window, cx| e.toggle_markdown_block_quote(&ToggleBlockQuote, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
«first
|
||||
|
||||
thirdˇ»
|
||||
"});
|
||||
|
||||
// A multi-line selection, including a mixture of quoted and unquoted lines
|
||||
// and a mixture of empty and non-empty lines, normalizes each line to a
|
||||
// single quote.
|
||||
cx.set_state(indoc! {"
|
||||
«> first
|
||||
second
|
||||
>
|
||||
|
||||
> third
|
||||
>fourthˇ»
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.toggle_markdown_block_quote(&ToggleBlockQuote, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
«> first
|
||||
> second
|
||||
>
|
||||
>
|
||||
> third
|
||||
> fourthˇ»
|
||||
"});
|
||||
|
||||
// A multi-line selection is unquoted.
|
||||
cx.update_editor(|e, window, cx| e.toggle_markdown_block_quote(&ToggleBlockQuote, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
«first
|
||||
second
|
||||
|
||||
|
||||
third
|
||||
fourthˇ»
|
||||
"});
|
||||
}
|
||||
@@ -571,6 +571,7 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::redo);
|
||||
register_action(editor, window, Editor::toggle_comments);
|
||||
register_action(editor, window, Editor::toggle_block_comments);
|
||||
register_action(editor, window, Editor::toggle_markdown_block_quote);
|
||||
register_action(editor, window, Editor::unwrap_syntax_node);
|
||||
register_action(editor, window, Editor::accept_next_word_edit_prediction);
|
||||
register_action(editor, window, Editor::accept_next_line_edit_prediction);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
use super::*;
|
||||
|
||||
impl Editor {
|
||||
pub fn toggle_markdown_block_quote(
|
||||
&mut self,
|
||||
_: &ToggleBlockQuote,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.manipulate_mutable_lines_in_markdown(window, cx, |lines| {
|
||||
let all_lines_quoted = lines.iter().all(|line| line.starts_with('>'));
|
||||
|
||||
for line in lines.iter_mut() {
|
||||
let stripped_line = match line.strip_prefix("> ").or_else(|| line.strip_prefix('>'))
|
||||
{
|
||||
Some(rest) => rest.to_string(),
|
||||
None => line.to_string(),
|
||||
};
|
||||
|
||||
*line = if all_lines_quoted {
|
||||
Cow::Owned(stripped_line)
|
||||
} else if stripped_line.trim().is_empty() {
|
||||
Cow::Borrowed(">")
|
||||
} else {
|
||||
Cow::Owned(format!("> {stripped_line}"))
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn manipulate_mutable_lines_in_markdown<Fn>(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
callback: Fn,
|
||||
) where
|
||||
Fn: FnMut(&mut Vec<Cow<'_, str>>),
|
||||
{
|
||||
if !self.is_in_markdown_language(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.manipulate_mutable_lines(window, cx, callback);
|
||||
}
|
||||
|
||||
fn is_in_markdown_language(&self, cx: &mut App) -> bool {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let head = self
|
||||
.selections
|
||||
.newest::<MultiBufferOffset>(&self.display_snapshot(cx))
|
||||
.head();
|
||||
snapshot
|
||||
.language_at(head)
|
||||
.is_some_and(|language| language.name() == "Markdown")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user