ElyMark: R4.9 round 1 fixes

Menus act on an open block's selection (component 6d90351), clipboard
keys reach a focused field outside the document, Strike binds per
platform, platform-correct key tests, Delete Table in the table menu
(component 24ffc45), short comments. Pin 6b033f3 -> 24ffc45.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-14 17:18:57 -04:00
1 parent cb513d49de
commit f84f990f90
13 files changed
+205 -46

No files matched your search

+9 -7
View File
@@ -50,10 +50,10 @@ actions!(
);
/// Shift with punctuation: Windows names the key, others the character.
pub(crate) fn shifted(key: char, typed: char) -> String {
pub(crate) fn shifted(mods: &str, key: char, typed: char) -> String {
match cfg!(target_os = "windows") {
true => format!("secondary-shift-{key}"),
false => format!("secondary-{typed}"),
true => format!("{mods}-shift-{key}"),
false => format!("{mods}-{typed}"),
}
}
@@ -72,14 +72,14 @@ pub fn bind(cx: &mut App) {
KeyBinding::new("secondary-shift-m", MathBlock, at),
KeyBinding::new("secondary-shift-k", CodeFences, at),
KeyBinding::new("secondary-shift-q", Quote, at),
KeyBinding::new(&shifted('[', '{'), OrderedList, at),
KeyBinding::new(&shifted(']', '}'), BulletList, at),
KeyBinding::new(&shifted("secondary", '[', '{'), OrderedList, at),
KeyBinding::new(&shifted("secondary", ']', '}'), BulletList, at),
KeyBinding::new("secondary-shift-x", TaskList, at),
KeyBinding::new("secondary-b", Strong, at),
KeyBinding::new("secondary-i", Emphasis, at),
KeyBinding::new("secondary-u", Underline, at),
KeyBinding::new(&shifted('`', '~'), InlineCode, at),
KeyBinding::new("alt-shift-5", Strike, at),
KeyBinding::new(&shifted("secondary", '`', '~'), InlineCode, at),
KeyBinding::new(&shifted("alt", '5', '%'), Strike, at),
KeyBinding::new("secondary-k", Hyperlink, at),
KeyBinding::new("secondary-shift-i", InsertImage, at),
KeyBinding::new("secondary-\\", ClearFormat, at),
@@ -230,5 +230,7 @@ impl Desk {
}
}
#[cfg(test)]
mod menus;
#[cfg(test)]
mod tests;
+78
View File
@@ -0,0 +1,78 @@
use ely_gpui_component::theme::{ActiveTheme as _, ControlSize};
use gpui::{
App, Entity, KeyUpEvent, Keystroke, Menu, Modifiers, TestAppContext, VisualTestContext, point,
px,
};
use super::tests::{current, document, opened, press};
use crate::{desk::Desk, view::Mode};
/// Selects `alpha` in the first block, opened.
fn select_alpha(desk: &Entity<Desk>, cx: &mut VisualTestContext) {
let bounds = cx.debug_bounds("markdown-block-0").expect("a block");
let at = point(bounds.left() + px(4.), bounds.top() + px(4.));
cx.simulate_click(at, Modifiers::none());
press(
"home shift-right shift-right shift-right shift-right shift-right",
cx,
);
let selected = document(desk, cx).read_with(cx, |document, cx| document.selected(cx));
assert_eq!(selected, "alpha");
}
/// Runs `menu`'s row `row` from the title bar.
fn choose(menu: fn(&App) -> Menu, row: usize, cx: &mut VisualTestContext) {
cx.update(|window, cx| {
cx.set_menus(vec![menu(cx)]);
window.refresh();
});
cx.run_until_parked();
let at = cx.update(|window, cx| {
let rem = window.rem_size();
let bar = cx.theme().titlebar_height().to_pixels(rem);
let title = cx.theme().control_height(ControlSize::Sm).to_pixels(rem);
point(px(12.), bar + title / 2.)
});
cx.simulate_click(at, Modifiers::none());
cx.run_until_parked();
press(&format!("{} enter", vec!["down"; row].join(" ")), cx);
let enter = Keystroke::parse("enter").expect("a key");
cx.simulate_event(KeyUpEvent { keystroke: enter });
cx.run_until_parked();
}
fn edit_menu(cx: &App) -> Menu {
let look = crate::look::Look::default();
crate::keys::menus(&look, cx).remove(0)
}
#[gpui::test]
fn a_menu_formats_the_open_blocks_selection(cx: &mut TestAppContext) {
let (desk, cx, _files) = opened("alpha beta\n", cx);
select_alpha(&desk, cx);
choose(super::format_menu, 1, cx);
assert_eq!(current(&desk, cx), "**alpha** beta\n");
}
#[gpui::test]
fn a_menu_copies_the_open_blocks_selection(cx: &mut TestAppContext) {
let (desk, cx, _files) = opened("alpha beta\n", cx);
select_alpha(&desk, cx);
choose(edit_menu, 1, cx);
let copied = cx.read_from_clipboard().and_then(|item| item.text());
assert_eq!(copied.as_deref(), Some("alpha"));
}
const TABLED: &str = "before\n\n| A |\n| - |\n| 1 |\n\nafter\n";
#[gpui::test]
fn delete_table_from_the_menu_is_one_undo_step(cx: &mut TestAppContext) {
let (desk, cx, _files) = opened(TABLED, cx);
desk.update_in(cx, |desk, window, cx| desk.switch(Mode::Source, window, cx));
let document = document(&desk, cx);
document.update(cx, |document, cx| document.set_caret(4, 2, cx));
choose(crate::tools::table_menu, 13, cx);
assert_eq!(current(&desk, cx), "before\n\nafter\n");
press("secondary-z", cx);
assert_eq!(current(&desk, cx), TABLED);
}
+31 -24
View File
@@ -5,6 +5,7 @@ use gpui::{
};
use crate::{
blocks::shifted,
desk::{
Desk,
tests::{open, start},
@@ -12,7 +13,7 @@ use crate::{
view::Mode,
};
fn opened(
pub(super) fn opened(
text: &str,
cx: &mut TestAppContext,
) -> (
@@ -33,14 +34,17 @@ fn opened(
(desk, cx, [data, files])
}
fn document(desk: &Entity<Desk>, cx: &mut VisualTestContext) -> Entity<MarkdownDocument> {
pub(super) fn document(
desk: &Entity<Desk>,
cx: &mut VisualTestContext,
) -> Entity<MarkdownDocument> {
desk.read_with(cx, |desk, cx| {
let view = desk.active_view().expect("a tab");
view.read(cx).document().clone()
})
}
fn current(desk: &Entity<Desk>, cx: &mut VisualTestContext) -> String {
pub(super) fn current(desk: &Entity<Desk>, cx: &mut VisualTestContext) -> String {
let document = document(desk, cx);
document.read_with(cx, |document, cx| document.text(cx))
}
@@ -52,7 +56,7 @@ fn mode(desk: &Entity<Desk>, cx: &mut VisualTestContext) -> Mode {
}
/// `keys`, then the background reads.
fn press(keys: &str, cx: &mut VisualTestContext) {
pub(super) fn press(keys: &str, cx: &mut VisualTestContext) {
cx.simulate_keystrokes(keys);
cx.run_until_parked();
}
@@ -70,19 +74,19 @@ fn toasted(desk: &Entity<Desk>, words: &str, cx: &mut VisualTestContext) -> usiz
#[gpui::test]
fn paragraph_keys_retype_the_caret_block(cx: &mut TestAppContext) {
let (desk, cx, _files) = opened("Intro\n", cx);
press("ctrl-1", cx);
press("secondary-1", cx);
assert_eq!(current(&desk, cx), "# Intro\n");
press("ctrl--", cx);
press("secondary--", cx);
assert_eq!(current(&desk, cx), "## Intro\n");
press("ctrl-=", cx);
press("secondary-=", cx);
assert_eq!(current(&desk, cx), "# Intro\n");
press("ctrl-0", cx);
press("secondary-0", cx);
assert_eq!(current(&desk, cx), "Intro\n");
press("ctrl-shift-[", cx);
press(&shifted("secondary", '[', '{'), cx);
assert_eq!(current(&desk, cx), "1. Intro\n");
press("ctrl-shift-]", cx);
press(&shifted("secondary", ']', '}'), cx);
assert_eq!(current(&desk, cx), "- Intro\n");
press("ctrl-shift-q", cx);
press("secondary-shift-q", cx);
assert_eq!(
current(&desk, cx),
"- > Intro\n",
@@ -93,23 +97,23 @@ fn paragraph_keys_retype_the_caret_block(cx: &mut TestAppContext) {
#[gpui::test]
fn format_keys_wrap_the_caret_word(cx: &mut TestAppContext) {
let (desk, cx, _files) = opened("word\n", cx);
press("ctrl-b", cx);
press("secondary-b", cx);
assert_eq!(current(&desk, cx), "**word**\n");
press("ctrl-\\", cx);
press("secondary-\\", cx);
assert_eq!(current(&desk, cx), "word\n");
press("alt-shift-5", cx);
press(&shifted("alt", '5', '%'), cx);
assert_eq!(current(&desk, cx), "~~word~~\n");
}
#[gpui::test]
fn ctrl_slash_toggles_source_inside_the_code_editor(cx: &mut TestAppContext) {
let (desk, cx, _files) = opened("a\n", cx);
press("ctrl-/", cx);
press("secondary-/", cx);
assert_eq!(mode(&desk, cx), Mode::Source);
let editor = document(&desk, cx).read_with(cx, |document, _| document.editor().clone());
let focus = editor.read_with(cx, |editor, cx| editor.focus_handle(cx));
cx.update(|window, cx| window.focus(&focus, cx));
press("ctrl-/", cx);
press("secondary-/", cx);
assert_eq!(mode(&desk, cx), Mode::Visual);
assert_eq!(current(&desk, cx), "a\n", "no comment marks");
}
@@ -117,7 +121,7 @@ fn ctrl_slash_toggles_source_inside_the_code_editor(cx: &mut TestAppContext) {
#[gpui::test]
fn a_refused_command_says_why(cx: &mut TestAppContext) {
let (desk, cx, _files) = opened("```\ncode\n```\n", cx);
press("ctrl-1", cx);
press("secondary-1", cx);
assert_eq!(current(&desk, cx), "```\ncode\n```\n");
assert_eq!(toasted(&desk, "inside code", cx), 1);
}
@@ -125,7 +129,7 @@ fn a_refused_command_says_why(cx: &mut TestAppContext) {
#[gpui::test]
fn alt_arrows_move_a_table_row(cx: &mut TestAppContext) {
let (desk, cx, _files) = opened("| A | B |\n| - | - |\n| 1 | 2 |\n", cx);
press("ctrl-/", cx);
press("secondary-/", cx);
document(&desk, cx).update(cx, |document, cx| document.set_caret(2, 2, cx));
press("alt-up", cx);
assert_eq!(
@@ -139,26 +143,26 @@ fn alt_arrows_move_a_table_row(cx: &mut TestAppContext) {
#[gpui::test]
fn view_keys_follow_typora(cx: &mut TestAppContext) {
let (desk, cx, _files) = opened("a\n", cx);
press("f8 f9 ctrl-shift-=", cx);
press(&format!("f8 f9 {}", shifted("secondary", '=', '+')), cx);
let look = desk.read_with(cx, |desk, _| desk.look.clone());
assert!(look.focus && look.typewriter && look.size > 100, "{look:?}");
press("ctrl-shift-0", cx);
press(&shifted("secondary", '0', ')'), cx);
assert_eq!(desk.read_with(cx, |desk, _| desk.look.size), 100);
}
#[gpui::test]
fn copy_as_markdown_and_paste_as_plain_text(cx: &mut TestAppContext) {
let (desk, cx, _files) = opened("a **b**\n", cx);
press("ctrl-shift-c", cx);
press("secondary-shift-c", cx);
let copied = cx.read_from_clipboard().and_then(|item| item.text());
assert_eq!(copied.as_deref(), Some("a **b**\n"));
cx.write_to_clipboard(ClipboardItem::new_string("*x*".into()));
press("ctrl-/", cx);
press("ctrl-shift-v", cx);
press("secondary-/", cx);
press("secondary-shift-v", cx);
assert_eq!(current(&desk, cx), "*x*a **b**\n");
}
/// Each row's name, `-` for a line, `▸` before a submenu.
/// Row names; `-` a line, `▸` a submenu.
fn rows(menu: &Menu) -> Vec<String> {
let mut out = Vec::new();
for item in &menu.items {
@@ -198,6 +202,8 @@ fn the_paragraph_and_format_menus_follow_typora(cx: &mut TestAppContext) {
"Delete Column",
"-",
"Prettify Source Code",
"-",
"Delete Table",
];
let mut paragraph: Vec<String> = [
"Heading 1",
@@ -248,6 +254,7 @@ fn the_paragraph_and_format_menus_follow_typora(cx: &mut TestAppContext) {
}
/// The keys a menu row shows for `action`.
#[cfg(target_os = "windows")]
fn keys(action: &dyn Action, cx: &mut VisualTestContext) -> String {
cx.update(|window, _| {
let binding = window
+37 -6
View File
@@ -1,9 +1,12 @@
//! Copy as Markdown and paste as plain text, as Typora's Edit menu.
//! Typora's Copy as Markdown and Paste as Plain Text.
use ely_gpui_component::primitives::Severity;
use ely_office::prelude::*;
use ely_ui::{commands, i18n::text};
use gpui::{App, ClipboardItem, Context, InteractiveElement as _, KeyBinding, actions};
use gpui::{
App, ClipboardItem, Context, Focusable as _, InteractiveElement as _, KeyBinding, Window,
actions,
};
use crate::desk::Desk;
@@ -18,8 +21,31 @@ pub fn bind(cx: &mut App) {
}
impl Desk {
/// Whether the document or nothing else holds focus.
fn on_document(&self, window: &Window, cx: &App) -> bool {
let Some(focused) = window.focused(cx) else {
return true;
};
let held = (self.active_view()).is_some_and(|view| {
let document = view.read(cx).document().read(cx);
document.focus_handle(cx).contains_focused(window, cx)
});
held || focused == self.core.focus
}
/// Hands a field outside the document its own `name` action.
fn to_field(name: &str, window: &mut Window, cx: &mut App) {
match cx.build_action(name, None) {
Ok(action) => window.dispatch_action(action, cx),
Err(error) => log::error!("mark: no action {name}: {error}"),
}
}
/// The selected Markdown, or the whole text, as written.
fn copy_markdown(&mut self, cx: &mut Context<Self>) {
fn copy_markdown(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if !self.on_document(window, cx) {
return Self::to_field("ely_input::Copy", window, cx);
}
let Some(view) = self.active_view() else {
return self.no_document(cx);
};
@@ -34,7 +60,10 @@ impl Desk {
}
/// The clipboard's text at the caret, pictures left out.
fn paste_plain(&mut self, cx: &mut Context<Self>) {
fn paste_plain(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if !self.on_document(window, cx) {
return Self::to_field("ely_input::Paste", window, cx);
}
let Some(view) = self.active_view() else {
return self.no_document(cx);
};
@@ -50,7 +79,9 @@ impl Desk {
}
pub fn clip_actions(root: gpui::Div, cx: &mut Context<Self>) -> gpui::Div {
root.on_action(cx.listener(|desk, _: &CopyMarkdown, _, cx| desk.copy_markdown(cx)))
.on_action(cx.listener(|desk, _: &PastePlain, _, cx| desk.paste_plain(cx)))
root.on_action(
cx.listener(|desk, _: &CopyMarkdown, window, cx| desk.copy_markdown(window, cx)),
)
.on_action(cx.listener(|desk, _: &PastePlain, window, cx| desk.paste_plain(window, cx)))
}
}
+22
View File
@@ -176,3 +176,25 @@ fn preview_keeps_its_text_from_tables_and_replacement(cx: &mut TestAppContext) {
replace_one(&desk, "x", cx);
assert_eq!(current_text(&desk, cx), "a\n");
}
#[gpui::test]
fn clipboard_keys_in_find_reach_the_field(cx: &mut TestAppContext) {
let (desk, cx, _files) = opened("alpha beta\n", cx);
query(&desk, "beta", cx);
let field = desk.read_with(cx, |desk, _| {
desk.finding.as_ref().expect("open").query.clone()
});
field.update(cx, |field, cx| field.select(0..4, cx));
cx.simulate_keystrokes("secondary-shift-c");
cx.run_until_parked();
let copied = cx.read_from_clipboard().and_then(|item| item.text());
assert_eq!(copied.as_deref(), Some("beta"));
cx.write_to_clipboard(gpui::ClipboardItem::new_string("alpha".into()));
cx.simulate_keystrokes("secondary-shift-v");
cx.run_until_parked();
assert_eq!(
field.read_with(cx, |field, _| field.text().to_string()),
"alpha"
);
assert_eq!(current_text(&desk, cx), "alpha beta\n");
}
+3 -3
View File
@@ -18,9 +18,9 @@ pub fn bind(cx: &mut App) {
KeyBinding::new("secondary-/", ToggleSource, Some("ElyEditor")),
KeyBinding::new("f8", crate::look::FocusMode, at),
KeyBinding::new("f9", crate::look::Typewriter, at),
KeyBinding::new(&shifted('=', '+'), crate::look::Larger, at),
KeyBinding::new(&shifted('-', '_'), crate::look::Smaller, at),
KeyBinding::new(&shifted('0', ')'), crate::look::ActualSize, at),
KeyBinding::new(&shifted("secondary", '=', '+'), crate::look::Larger, at),
KeyBinding::new(&shifted("secondary", '-', '_'), crate::look::Smaller, at),
KeyBinding::new(&shifted("secondary", '0', ')'), crate::look::ActualSize, at),
KeyBinding::new("secondary-f", ely_office::FindReplace, at),
]);
crate::blocks::bind(cx);
+5 -1
View File
@@ -20,7 +20,8 @@ actions!(
MoveColumnRight,
DeleteRow,
DeleteColumn,
TidyTable
TidyTable,
DeleteTable
]
);
@@ -60,6 +61,8 @@ pub fn table_menu(cx: &App) -> Menu {
item("mark.table.delete_column", Box::new(DeleteColumn)),
MenuItem::separator(),
item("mark.table.tidy", Box::new(TidyTable)),
MenuItem::separator(),
item("mark.table.delete", Box::new(DeleteTable)),
])
}
@@ -79,6 +82,7 @@ impl Desk {
DeleteRow => |d, cx| d.edit_table(T::DeleteRow, cx),
DeleteColumn => |d, cx| d.edit_table(T::DeleteColumn, cx),
TidyTable => |d, cx| d.edit_table(T::Tidy, cx),
DeleteTable => |d, cx| d.edit_table(T::Delete, cx),
)
}
}