R5.2: ElyDoc's right-click menu follows Word by target, items enabled as the kernel's menu allows; two rounds, R2 PASS
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
e3c515b2f4
commit
9d80ba6f39
22 files changed
+764
-146
No files matched your search
@@ -48,7 +48,7 @@ impl Docs {
|
||||
.size_full()
|
||||
.child(tab.view.clone())
|
||||
.children(self.find_box(cx))
|
||||
.children(self.spelling_menu(cx))
|
||||
.children(self.context_menu(cx))
|
||||
.into_any_element();
|
||||
let risk = self.stopped_banner(cx).or_else(|| self.risk_banner(cx));
|
||||
let fonts = tab.view.read(cx).missing_fonts().join(", ");
|
||||
|
||||
@@ -37,7 +37,7 @@ pub struct Docs {
|
||||
pub review: Entity<Review>,
|
||||
/// What the right column shows.
|
||||
pub side: Side,
|
||||
pub offer: Option<crate::spell::Offer>,
|
||||
pub offer: Option<crate::menu::Offer>,
|
||||
pub merge: Entity<crate::merge::Merge>,
|
||||
pub navigator: Entity<crate::navigate::Navigator>,
|
||||
/// The left column shows the navigator.
|
||||
@@ -88,7 +88,7 @@ impl Host for Docs {
|
||||
fn observed(&mut self, id: u64, view: &Entity<LokView>, cx: &mut Context<Self>) {
|
||||
self.count(id, view, cx);
|
||||
self.found(id, view, cx);
|
||||
self.offer_spelling(id, view, cx);
|
||||
self.offer_menu(id, view, cx);
|
||||
}
|
||||
|
||||
/// A dialog about a closed tab goes with it.
|
||||
|
||||
@@ -7,13 +7,13 @@ mod compare;
|
||||
mod docs;
|
||||
mod files;
|
||||
mod insert;
|
||||
mod menu;
|
||||
mod merge;
|
||||
mod navigate;
|
||||
mod output;
|
||||
mod props;
|
||||
mod refs;
|
||||
mod review;
|
||||
mod spell;
|
||||
|
||||
use ely_platform::AppId;
|
||||
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
//! The right-click menu, laid out as Word's (DOC-02, DOC-07, DOC-12).
|
||||
|
||||
use ely_gpui_component::{
|
||||
menus::{ContextMenu, Menu, MenuItem},
|
||||
primitives::{IconName, Severity},
|
||||
};
|
||||
use ely_lok::{
|
||||
LokView,
|
||||
menu::{Offered, Target},
|
||||
};
|
||||
use gpui::{App, Context, Entity, Pixels, Point, Window};
|
||||
|
||||
use crate::docs::Docs;
|
||||
use crate::refs::{InsertCaption, InsertComment, InsertLink};
|
||||
use ely_office::prelude::*;
|
||||
|
||||
/// A menu the kernel offered, asked for at a point.
|
||||
pub struct Offer {
|
||||
tab: u64,
|
||||
pub(crate) offered: Offered,
|
||||
/// Bumps for each menu, so each opens once.
|
||||
request: (u64, Point<Pixels>),
|
||||
}
|
||||
|
||||
/// One thing the menu does.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Pick {
|
||||
Cut,
|
||||
Copy,
|
||||
Paste,
|
||||
Link,
|
||||
Comment,
|
||||
ColumnLeft,
|
||||
ColumnRight,
|
||||
RowAbove,
|
||||
RowBelow,
|
||||
DeleteRows,
|
||||
DeleteColumns,
|
||||
DeleteTable,
|
||||
Merge,
|
||||
SplitColumns,
|
||||
SplitRows,
|
||||
EvenRows,
|
||||
EvenColumns,
|
||||
Inline,
|
||||
Square,
|
||||
TopBottom,
|
||||
InFront,
|
||||
Front,
|
||||
Forward,
|
||||
Back,
|
||||
Backward,
|
||||
Caption,
|
||||
}
|
||||
|
||||
/// A row, a submenu or a line.
|
||||
enum Entry {
|
||||
Row(Pick),
|
||||
Sub(&'static str, &'static [Pick]),
|
||||
Line,
|
||||
}
|
||||
|
||||
const TABLE: &[Entry] = &[
|
||||
Entry::Sub(
|
||||
"doc.menu.insert",
|
||||
&[Pick::ColumnLeft, Pick::ColumnRight, Pick::RowAbove, Pick::RowBelow],
|
||||
),
|
||||
Entry::Sub(
|
||||
"doc.menu.delete",
|
||||
&[Pick::DeleteRows, Pick::DeleteColumns, Pick::DeleteTable],
|
||||
),
|
||||
Entry::Row(Pick::Merge),
|
||||
Entry::Sub("doc.menu.split", &[Pick::SplitColumns, Pick::SplitRows]),
|
||||
Entry::Row(Pick::EvenRows),
|
||||
Entry::Row(Pick::EvenColumns),
|
||||
Entry::Line,
|
||||
Entry::Row(Pick::Link),
|
||||
Entry::Row(Pick::Comment),
|
||||
];
|
||||
|
||||
const FRAME: &[Entry] = &[
|
||||
Entry::Sub(
|
||||
"doc.menu.wrap",
|
||||
&[Pick::Inline, Pick::Square, Pick::TopBottom, Pick::InFront],
|
||||
),
|
||||
Entry::Sub("doc.menu.front", &[Pick::Front, Pick::Forward]),
|
||||
Entry::Sub("doc.menu.back", &[Pick::Back, Pick::Backward]),
|
||||
Entry::Line,
|
||||
Entry::Row(Pick::Caption),
|
||||
Entry::Row(Pick::Comment),
|
||||
];
|
||||
|
||||
const TEXT: &[Entry] = &[Entry::Row(Pick::Link), Entry::Row(Pick::Comment)];
|
||||
|
||||
impl Pick {
|
||||
/// Its label key and the kernel command that gates it.
|
||||
fn row(self) -> (&'static str, &'static str) {
|
||||
match self {
|
||||
Pick::Cut => ("doc.menu.cut", ".uno:Cut"),
|
||||
Pick::Copy => ("doc.menu.copy", ".uno:Copy"),
|
||||
Pick::Paste => ("doc.menu.paste", ".uno:Paste"),
|
||||
Pick::Link => ("doc.menu.link", ".uno:InsertHyperlink"),
|
||||
Pick::Comment => ("doc.menu.comment", ".uno:InsertAnnotation"),
|
||||
Pick::ColumnLeft => ("doc.menu.column_left", ".uno:InsertColumnsBefore"),
|
||||
Pick::ColumnRight => ("doc.menu.column_right", ".uno:InsertColumnsAfter"),
|
||||
Pick::RowAbove => ("doc.menu.row_above", ".uno:InsertRowsBefore"),
|
||||
Pick::RowBelow => ("doc.menu.row_below", ".uno:InsertRowsAfter"),
|
||||
Pick::DeleteRows => ("doc.menu.delete_rows", ".uno:DeleteRows"),
|
||||
Pick::DeleteColumns => ("doc.menu.delete_columns", ".uno:DeleteColumns"),
|
||||
Pick::DeleteTable => ("doc.menu.delete_table", ".uno:DeleteTable"),
|
||||
Pick::Merge => ("doc.menu.merge", ".uno:MergeCells"),
|
||||
Pick::SplitColumns => ("doc.menu.split_columns", ".uno:SplitCell"),
|
||||
Pick::SplitRows => ("doc.menu.split_rows", ".uno:SplitCell"),
|
||||
Pick::EvenRows => ("doc.menu.even_rows", ".uno:DistributeRows"),
|
||||
Pick::EvenColumns => ("doc.menu.even_columns", ".uno:DistributeColumns"),
|
||||
Pick::Inline => ("doc.menu.inline", ".uno:SetAnchorToChar"),
|
||||
Pick::Square => ("doc.menu.square", ".uno:WrapOn"),
|
||||
Pick::TopBottom => ("doc.menu.top_bottom", ".uno:WrapOff"),
|
||||
Pick::InFront => ("doc.menu.in_front", ".uno:WrapThrough"),
|
||||
Pick::Front => ("doc.menu.front", ".uno:BringToFront"),
|
||||
Pick::Forward => ("doc.menu.forward", ".uno:ObjectForwardOne"),
|
||||
Pick::Back => ("doc.menu.back", ".uno:SendToBack"),
|
||||
Pick::Backward => ("doc.menu.backward", ".uno:ObjectBackOne"),
|
||||
Pick::Caption => ("doc.menu.caption", ".uno:InsertCaptionDialog"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The kernel offers it and it runs now.
|
||||
pub(crate) fn on(self, offered: &Offered) -> bool {
|
||||
offered.allows(self.row().1)
|
||||
}
|
||||
}
|
||||
|
||||
impl Docs {
|
||||
/// Turns the kernel's context menu into Ely's.
|
||||
pub(crate) fn offer_menu(&mut self, id: u64, view: &Entity<LokView>, cx: &mut Context<Self>) {
|
||||
let Some((json, at)) = view.update(cx, |view, _| view.take_menu()) else {
|
||||
return;
|
||||
};
|
||||
let offered = match ely_lok::menu::read(&json) {
|
||||
Ok(offered) => offered,
|
||||
Err(error) => {
|
||||
let text = Docs::text(cx, "office.error.engine", &[("error", &error.to_string())]);
|
||||
return self.toast(text, Severity::Danger, cx);
|
||||
}
|
||||
};
|
||||
// Without a click it opens at the caret.
|
||||
let caret = || view.read(cx).cursor_bounds().map(|caret| caret.bottom_left());
|
||||
let Some(at) = at.or_else(caret) else {
|
||||
log::warn!("doc: a menu with no click and no caret");
|
||||
return;
|
||||
};
|
||||
let number = self.offer.as_ref().map_or(1, |offer| offer.request.0 + 1);
|
||||
let suggestions = offered.spelling.as_ref().map_or(0, |spelling| spelling.suggestions.len());
|
||||
log::info!("doc: a {:?} menu, {suggestions} suggestions", offered.target);
|
||||
self.offer = Some(Offer {
|
||||
tab: id,
|
||||
offered,
|
||||
request: (number, at),
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// The menu's host, for the active tab.
|
||||
pub(crate) fn context_menu(&self, cx: &mut Context<Self>) -> Option<ContextMenu> {
|
||||
let offer = self
|
||||
.offer
|
||||
.as_ref()
|
||||
.filter(|offer| Some(offer.tab) == self.core.active)?;
|
||||
let menu = menu_of(&cx.entity(), &offer.offered, cx);
|
||||
Some(ContextMenu::new("doc-menu", menu).manual(Some(offer.request)))
|
||||
}
|
||||
|
||||
/// Runs one menu row.
|
||||
pub(crate) fn choose(&mut self, pick: Pick, window: &mut Window, cx: &mut Context<Self>) {
|
||||
log::info!("doc: the menu picks {pick:?}");
|
||||
let (id, values): (&str, &[&str]) = match pick {
|
||||
Pick::Cut | Pick::Copy | Pick::Paste => return self.clipboard(pick, cx),
|
||||
Pick::Link => return self.on_link(&InsertLink, window, cx),
|
||||
Pick::Comment => return self.on_comment(&InsertComment, window, cx),
|
||||
Pick::Caption => return self.on_caption(&InsertCaption, window, cx),
|
||||
Pick::ColumnLeft => ("table.column_left", &[]),
|
||||
Pick::ColumnRight => ("table.column_right", &[]),
|
||||
Pick::RowAbove => ("table.row_above", &[]),
|
||||
Pick::RowBelow => ("table.row_below", &[]),
|
||||
Pick::DeleteRows => ("table.delete_rows", &[]),
|
||||
Pick::DeleteColumns => ("table.delete_columns", &[]),
|
||||
Pick::DeleteTable => ("table.delete", &[]),
|
||||
Pick::Merge => ("table.merge", &[]),
|
||||
Pick::SplitColumns => ("table.split", &["2", "false"]),
|
||||
Pick::SplitRows => ("table.split", &["2", "true"]),
|
||||
Pick::EvenRows => ("table.even_rows", &[]),
|
||||
Pick::EvenColumns => ("table.even_columns", &[]),
|
||||
Pick::Inline => ("image.anchor_as_char", &[]),
|
||||
Pick::Square => ("image.wrap_on", &[]),
|
||||
Pick::TopBottom => ("image.wrap_off", &[]),
|
||||
Pick::InFront => ("image.wrap_through", &[]),
|
||||
Pick::Front => ("image.front", &[]),
|
||||
Pick::Forward => ("image.forward", &[]),
|
||||
Pick::Back => ("image.back", &[]),
|
||||
Pick::Backward => ("image.backward", &[]),
|
||||
};
|
||||
self.run_active(id, values, cx);
|
||||
}
|
||||
|
||||
fn clipboard(&mut self, pick: Pick, cx: &mut Context<Self>) {
|
||||
let Some(view) = self.active_tab().map(|tab| tab.view.clone()) else {
|
||||
log::warn!("doc: {pick:?} with no document");
|
||||
return;
|
||||
};
|
||||
view.update(cx, |view, _| match pick {
|
||||
Pick::Paste => view.paste(),
|
||||
_ => view.copy(pick == Pick::Cut),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Spelling first, then the clipboard row, then the target's rows.
|
||||
pub(crate) fn menu_of(this: &Entity<Docs>, offered: &Offered, cx: &App) -> Menu {
|
||||
let item = |pick: Pick| {
|
||||
let this = this.clone();
|
||||
MenuItem::new(Docs::text(cx, pick.row().0, &[]))
|
||||
.disabled(!pick.on(offered))
|
||||
.on_click(move |window, cx| this.update(cx, |docs, cx| docs.choose(pick, window, cx)))
|
||||
};
|
||||
let mut menu = Menu::new();
|
||||
if let Some(spelling) = &offered.spelling {
|
||||
for (label, rule) in &spelling.suggestions {
|
||||
let (this, rule) = (this.clone(), rule.clone());
|
||||
menu = menu.item(MenuItem::new(label.clone()).on_click(move |_, cx| {
|
||||
this.update(cx, |docs, cx| docs.run_active("spell.apply", &[&rule], cx))
|
||||
}));
|
||||
}
|
||||
if spelling.suggestions.is_empty() {
|
||||
menu = menu.item(MenuItem::new(Docs::text(cx, "doc.spell.none", &[])).disabled(true));
|
||||
}
|
||||
let this = this.clone();
|
||||
let ignore = MenuItem::new(Docs::text(cx, "doc.spell.ignore_all", &[])).on_click(
|
||||
move |_, cx| {
|
||||
this.update(cx, |docs, cx| {
|
||||
docs.run_active("spell.ignore_all", &["Spelling"], cx)
|
||||
})
|
||||
},
|
||||
);
|
||||
menu = menu.item(ignore).separator();
|
||||
}
|
||||
let clipboard = [
|
||||
(Pick::Cut, IconName::Scissors),
|
||||
(Pick::Copy, IconName::Copy),
|
||||
(Pick::Paste, IconName::Clipboard),
|
||||
];
|
||||
menu = menu
|
||||
.buttons(clipboard.map(|(pick, icon)| item(pick).icon(icon)))
|
||||
.separator();
|
||||
let entries = match offered.target {
|
||||
Target::Table => TABLE,
|
||||
Target::Frame => FRAME,
|
||||
Target::Text => TEXT,
|
||||
};
|
||||
for entry in entries {
|
||||
menu = match entry {
|
||||
Entry::Row(pick) => menu.item(item(*pick)),
|
||||
Entry::Sub(key, picks) => {
|
||||
let sub = picks.iter().fold(Menu::new(), |sub, pick| sub.item(item(*pick)));
|
||||
let on = picks.iter().any(|pick| pick.on(offered));
|
||||
menu.item(MenuItem::submenu(Docs::text(cx, key, &[]), sub).disabled(!on))
|
||||
}
|
||||
Entry::Line => menu.separator(),
|
||||
};
|
||||
}
|
||||
menu
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,117 @@
|
||||
use ely_gpui_component::theme::Theme;
|
||||
use ely_lok::{LokView, menu::Target};
|
||||
use gpui::{Entity, Modifiers, MouseButton, Pixels, Point, TestAppContext, VisualTestContext};
|
||||
|
||||
use super::Pick;
|
||||
use crate::docs::Docs;
|
||||
use crate::tests::{blank, docs, settled, text_of, type_into, with_window, written};
|
||||
use ely_office::prelude::*;
|
||||
|
||||
/// The view with the menu's host over it.
|
||||
struct Host(Entity<Docs>, Entity<LokView>);
|
||||
|
||||
impl gpui::Render for Host {
|
||||
fn render(&mut self, _: &mut gpui::Window, cx: &mut gpui::Context<Self>) -> impl gpui::IntoElement {
|
||||
use gpui::{ParentElement as _, Styled as _};
|
||||
let menu = self.0.update(cx, |docs, cx| docs.context_menu(cx));
|
||||
gpui::div().relative().size_full().child(self.1.clone()).children(menu)
|
||||
}
|
||||
}
|
||||
|
||||
fn frames(cx: &mut VisualTestContext) {
|
||||
for _ in 0..3 {
|
||||
cx.update(|window, _| window.refresh());
|
||||
cx.run_until_parked();
|
||||
}
|
||||
}
|
||||
|
||||
/// The caret's middle, in window pixels.
|
||||
fn caret(cx: &mut VisualTestContext, view: &Entity<LokView>) -> Point<Pixels> {
|
||||
frames(cx);
|
||||
cx.update(|_, app| view.read(app).cursor_bounds().expect("a caret").center())
|
||||
}
|
||||
|
||||
/// Right-clicks through the view and waits for the target's menu.
|
||||
fn right_click(cx: &mut VisualTestContext, docs: &Entity<Docs>, at: Point<Pixels>, target: Target) {
|
||||
cx.update(|_, app| docs.update(app, |docs, _| docs.offer = None));
|
||||
cx.simulate_mouse_move(at, None, Modifiers::none());
|
||||
cx.simulate_mouse_down(at, MouseButton::Right, Modifiers::none());
|
||||
cx.simulate_mouse_up(at, MouseButton::Right, Modifiers::none());
|
||||
let shown = |app: &gpui::App| {
|
||||
docs.read(app)
|
||||
.offer
|
||||
.as_ref()
|
||||
.is_some_and(|offer| offer.offered.target == target)
|
||||
};
|
||||
for _ in 0..500 {
|
||||
cx.run_until_parked();
|
||||
if cx.update(|_, app| shown(app)) {
|
||||
return frames(cx);
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
}
|
||||
panic!("no {target:?} menu");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_right_click_opens_word_rows_where_it_lands() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let mut cx = TestAppContext::single();
|
||||
let Some(docs) = docs(&mut cx, dir.path()) else {
|
||||
eprintln!("SKIP: no LOK build");
|
||||
return;
|
||||
};
|
||||
cx.update(|cx| {
|
||||
ely_gpui_component::init_for_tests(cx);
|
||||
Theme::update(cx, |theme| theme.reduced_motion = true);
|
||||
});
|
||||
let id = blank(&mut cx, &docs);
|
||||
type_into(&mut cx, &docs, "Text");
|
||||
let view = cx.update(|app| docs.read(app).active_tab().expect("a tab").view.clone());
|
||||
let window = cx.add_window(|_, _| Host(docs.clone(), view.clone()));
|
||||
let mut shown = VisualTestContext::from_window(window.into(), &cx);
|
||||
let text = caret(&mut shown, &view) - gpui::point(gpui::px(8.0), gpui::px(0.0));
|
||||
right_click(&mut shown, &docs, text, Target::Text);
|
||||
let cut = shown.debug_bounds("menu-button-0-0").expect("the clipboard row");
|
||||
let off = cut.origin - text;
|
||||
assert!(
|
||||
off.x >= gpui::px(0.0) && off.x < gpui::px(40.0) && off.y < gpui::px(40.0),
|
||||
"the menu opens at the click, not the caret: {off:?}"
|
||||
);
|
||||
let on = |shown: &mut VisualTestContext, pick: Pick| {
|
||||
shown.update(|_, app| pick.on(&docs.read(app).offer.as_ref().expect("an offer").offered))
|
||||
};
|
||||
assert!(on(&mut shown, Pick::Comment));
|
||||
assert!(!on(&mut shown, Pick::Link), "the kernel links a selection");
|
||||
assert!(!on(&mut shown, Pick::Cut), "nothing is selected");
|
||||
with_window(&mut cx, &docs, |docs, _, cx| docs.insert_table(2.0, 2.0, cx));
|
||||
settled(&cx, &docs);
|
||||
let cell = caret(&mut shown, &view);
|
||||
right_click(&mut shown, &docs, cell, Target::Table);
|
||||
assert!(on(&mut shown, Pick::RowBelow) && on(&mut shown, Pick::SplitRows));
|
||||
assert!(!on(&mut shown, Pick::Merge), "one cell does not merge");
|
||||
for pick in [Pick::RowBelow, Pick::ColumnRight] {
|
||||
with_window(&mut cx, &docs, |docs, window, cx| docs.choose(pick, window, cx));
|
||||
settled(&cx, &docs);
|
||||
}
|
||||
let path = dir.path().join("grown.odt");
|
||||
with_window(&mut cx, &docs, |docs, window, cx| {
|
||||
docs.write_as(id, path.clone(), None, window, cx)
|
||||
});
|
||||
written(&cx, &path, None);
|
||||
let flat = text_of(&mut cx, &docs, &path);
|
||||
assert_eq!(flat.matches("<table:table-row").count(), 3, "a row below");
|
||||
assert_eq!(flat.matches("<table:table-cell").count(), 9, "and a column right");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn link_follows_the_kernel_flag() {
|
||||
let menu = |enabled: bool| {
|
||||
let json = format!(
|
||||
r#"{{"menu":[{{"text":"x","type":"command","command":".uno:InsertHyperlink","enabled":"{enabled}"}}]}}"#
|
||||
);
|
||||
ely_lok::menu::read(&json).expect("the menu parses")
|
||||
};
|
||||
assert!(Pick::Link.on(&menu(true)));
|
||||
assert!(!Pick::Link.on(&menu(false)), "protected text gets no link");
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
//! Spelling suggestions from the kernel's context menu (DOC-12).
|
||||
|
||||
use ely_gpui_component::{
|
||||
menus::{ContextMenu, Menu, MenuItem},
|
||||
primitives::Severity,
|
||||
};
|
||||
use ely_lok::LokView;
|
||||
use gpui::{Context, Entity, IntoElement, Pixels, Point};
|
||||
|
||||
use crate::docs::Docs;
|
||||
use ely_office::prelude::*;
|
||||
|
||||
/// A misspelled word's menu, asked for at a point.
|
||||
pub struct Offer {
|
||||
tab: u64,
|
||||
pub(crate) suggestions: Vec<(String, String)>,
|
||||
/// Bumps for each menu, so each opens once.
|
||||
request: (u64, Point<Pixels>),
|
||||
}
|
||||
|
||||
impl Docs {
|
||||
/// Turns the kernel's context menu into a spelling offer.
|
||||
pub(crate) fn offer_spelling(
|
||||
&mut self,
|
||||
id: u64,
|
||||
view: &Entity<LokView>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(json) = view.update(cx, |view, _| view.take_menu()) else {
|
||||
return;
|
||||
};
|
||||
let spelling = match ely_lok::review::spelling(&json) {
|
||||
Ok(spelling) => spelling,
|
||||
Err(error) => {
|
||||
let text = Docs::text(cx, "office.error.engine", &[("error", &error.to_string())]);
|
||||
return self.toast(text, Severity::Danger, cx);
|
||||
}
|
||||
};
|
||||
let Some(spelling) = spelling else {
|
||||
log::info!("doc: the kernel menu has no spelling");
|
||||
return;
|
||||
};
|
||||
let Some(caret) = view.read(cx).cursor_bounds() else {
|
||||
log::warn!("doc: a spelling menu without a caret");
|
||||
return;
|
||||
};
|
||||
let number = self.offer.as_ref().map_or(1, |offer| offer.request.0 + 1);
|
||||
log::info!("doc: {} spelling suggestions", spelling.suggestions.len());
|
||||
self.offer = Some(Offer {
|
||||
tab: id,
|
||||
suggestions: spelling.suggestions,
|
||||
request: (number, caret.bottom_left()),
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// The spelling menu's host, for the active tab.
|
||||
pub(crate) fn spelling_menu(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
|
||||
let offer = self
|
||||
.offer
|
||||
.as_ref()
|
||||
.filter(|offer| Some(offer.tab) == self.core.active)?;
|
||||
let this = cx.entity();
|
||||
let mut menu = Menu::new();
|
||||
for (label, rule) in &offer.suggestions {
|
||||
let (this, rule) = (this.clone(), rule.clone());
|
||||
menu = menu.item(MenuItem::new(label.clone()).on_click(move |_, cx| {
|
||||
this.update(cx, |docs, cx| docs.run_active("spell.apply", &[&rule], cx))
|
||||
}));
|
||||
}
|
||||
if offer.suggestions.is_empty() {
|
||||
let none = Docs::text(cx, "doc.spell.none", &[]);
|
||||
menu = menu.item(MenuItem::new(none).disabled(true));
|
||||
}
|
||||
let ignore = Docs::text(cx, "doc.spell.ignore_all", &[]);
|
||||
let menu = menu
|
||||
.separator()
|
||||
.item(MenuItem::new(ignore).on_click(move |_, cx| {
|
||||
this.update(cx, |docs, cx| {
|
||||
docs.run_active("spell.ignore_all", &["Spelling"], cx)
|
||||
})
|
||||
}));
|
||||
Some(ContextMenu::new("doc-spelling", menu).manual(Some(offer.request)))
|
||||
}
|
||||
}
|
||||
@@ -126,7 +126,8 @@ fn a_misspelled_word_offers_suggestions() {
|
||||
settle(&cx, |app| docs.read(app).offer.is_some());
|
||||
let rule = cx.update(|app| {
|
||||
let offer = docs.read(app).offer.as_ref().expect("an offer");
|
||||
let (_, rule) = offer
|
||||
let spelling = offer.offered.spelling.as_ref().expect("a spelling menu");
|
||||
let (_, rule) = spelling
|
||||
.suggestions
|
||||
.iter()
|
||||
.find(|(label, _)| label == "The")
|
||||
|
||||
Reference in new issue
Block a user