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:
1 parent
cb513d49de
commit
f84f990f90
13 files changed
+205
-46
No files matched your search
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user