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:
Joseph T. Lyons authored and GitHub committed 2026-06-04 23:34:13 +00:00
1 parent fb8da5b73d
commit 24f4b66fa6
5 files changed
+163

No files matched your search

+9
View File
@@ -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,
[
+1
View File
@@ -66,6 +66,7 @@ mod config;
mod diagnostics;
mod edit_prediction;
mod input;
mod markdown_actions;
mod navigation;
mod rewrap;
mod selection;
+96
View File
@@ -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ˇ»
"});
}
+1
View File
@@ -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);
+56
View File
@@ -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")
}
}