Guard completion callbacks and cancel occluded grid drags
This commit is contained in:
1 parent
cc0f5a9caf
commit
c5cb29fa34
9 files changed
+198
-22
No files matched your search
@@ -123,6 +123,7 @@ pub struct LokView {
|
||||
pointer: gpui::CursorStyle,
|
||||
selection: Vec<Rect>,
|
||||
cell: crate::Cell,
|
||||
completion: formula::Completion,
|
||||
/// Whether the kernel was told Ely's scheme.
|
||||
lit: bool,
|
||||
/// The zoom follows the viewport so a page fits.
|
||||
@@ -218,6 +219,7 @@ impl LokView {
|
||||
pointer: gpui::CursorStyle::Arrow,
|
||||
selection: Vec::new(),
|
||||
cell: crate::Cell::default(),
|
||||
completion: formula::Completion::default(),
|
||||
lit: false,
|
||||
fit: false,
|
||||
turning: None,
|
||||
@@ -392,6 +394,7 @@ impl LokView {
|
||||
self.picked = 0;
|
||||
}
|
||||
self.cell = batch.cell;
|
||||
self.completion.reported(batch.revision);
|
||||
self.graphic = batch.graphic;
|
||||
self.read_borders(batch.table, batch.revision);
|
||||
self.review = batch.review;
|
||||
|
||||
@@ -11,6 +11,9 @@ use gpui::{
|
||||
|
||||
use super::{LokView, input::Input};
|
||||
|
||||
mod completion;
|
||||
pub(super) use completion::Completion;
|
||||
|
||||
impl LokView {
|
||||
/// The sheet shown, which reference marks follow.
|
||||
pub fn set_part(&mut self, part: usize) {
|
||||
@@ -48,7 +51,7 @@ impl LokView {
|
||||
/// Up and Down walk the offered functions.
|
||||
pub(super) fn walk(&mut self, keystroke: &Keystroke, cx: &mut Context<Self>) -> bool {
|
||||
let count = self.cell.functions.len();
|
||||
if count == 0 || keystroke.modifiers != Modifiers::none() {
|
||||
if count == 0 || self.completion.pending() || keystroke.modifiers != Modifiers::none() {
|
||||
return false;
|
||||
}
|
||||
self.picked = match keystroke.key.as_str() {
|
||||
@@ -62,32 +65,42 @@ impl LokView {
|
||||
|
||||
/// Takes the chosen function, in order with typing.
|
||||
pub(super) fn complete(&mut self) -> bool {
|
||||
if self.completion.pending() {
|
||||
return false;
|
||||
}
|
||||
let Some(item) = self.offered().into_iter().nth(self.picked) else {
|
||||
return false;
|
||||
};
|
||||
log::info!("lok: completes {}", item.label);
|
||||
self.send(Input::Complete(item.label.to_string()));
|
||||
self.cell.functions.clear();
|
||||
self.complete_function(&item.label)
|
||||
}
|
||||
|
||||
fn complete_function(&mut self, name: &str) -> bool {
|
||||
if self.completion.pending() || !self.cell.functions.iter().any(|item| item.name == name) {
|
||||
return false;
|
||||
}
|
||||
log::info!("lok: completes {name}");
|
||||
self.send(Input::Complete(name.to_string()));
|
||||
self.completion.queued(self.sent);
|
||||
self.picked = 0;
|
||||
true
|
||||
}
|
||||
|
||||
/// The offered functions, else the call's usage, under the cell.
|
||||
pub(super) fn hints(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
if self.completion.pending() {
|
||||
return None;
|
||||
}
|
||||
let cell = self.screen_rect(self.cell.cursor?);
|
||||
let hint = if self.cell.functions.is_empty() {
|
||||
SignatureHelp::new("lok-view-usage", [usage(&self.cell.usage)?]).into_any_element()
|
||||
} else {
|
||||
let this = cx.entity();
|
||||
let offered = self.offered();
|
||||
let labels: Vec<_> = offered.iter().map(|item| item.label.clone()).collect();
|
||||
CompletionMenu::new("lok-view-functions", offered)
|
||||
.selected(self.picked)
|
||||
.on_pick(move |item, _, cx| {
|
||||
let at = labels.iter().position(|label| *label == item.label);
|
||||
this.update(cx, |view, cx| {
|
||||
view.picked = at.expect("a picked item is offered");
|
||||
view.complete();
|
||||
view.complete_function(&item.label);
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Completion {
|
||||
pending: Option<(u64, Option<u64>)>,
|
||||
reported: u64,
|
||||
}
|
||||
|
||||
impl Completion {
|
||||
pub(crate) fn pending(&self) -> bool {
|
||||
self.pending.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn queued(&mut self, seq: u64) {
|
||||
self.pending = Some((seq, None));
|
||||
}
|
||||
|
||||
pub(crate) fn reported(&mut self, revision: u64) {
|
||||
self.reported = revision;
|
||||
self.finish();
|
||||
}
|
||||
|
||||
pub(crate) fn landed(&mut self, seq: u64, revision: Option<u64>) {
|
||||
if self.pending.is_some_and(|(waiting, _)| waiting == seq) {
|
||||
self.pending = revision.map(|revision| (seq, Some(revision)));
|
||||
self.finish();
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&mut self) {
|
||||
// Inline callbacks and serialized output preserve completion revisions.
|
||||
if self
|
||||
.pending
|
||||
.is_some_and(|(_, revision)| revision.is_some_and(|revision| self.reported >= revision))
|
||||
{
|
||||
self.pending = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Completion;
|
||||
|
||||
#[test]
|
||||
fn an_old_snapshot_cannot_reopen_an_accepted_menu() {
|
||||
let mut completion = Completion::default();
|
||||
completion.reported(10);
|
||||
completion.queued(7);
|
||||
completion.reported(10);
|
||||
assert!(completion.pending());
|
||||
completion.landed(7, Some(11));
|
||||
assert!(completion.pending());
|
||||
completion.reported(10);
|
||||
assert!(completion.pending());
|
||||
completion.reported(11);
|
||||
assert!(!completion.pending());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_snapshot_before_the_reply_waits_for_the_reply() {
|
||||
let mut completion = Completion::default();
|
||||
completion.queued(7);
|
||||
completion.reported(11);
|
||||
assert!(completion.pending());
|
||||
completion.landed(7, Some(11));
|
||||
assert!(!completion.pending());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reply_before_the_snapshot_waits_for_the_snapshot() {
|
||||
let mut completion = Completion::default();
|
||||
completion.queued(7);
|
||||
completion.landed(7, Some(11));
|
||||
assert!(completion.pending());
|
||||
completion.reported(11);
|
||||
assert!(!completion.pending());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn another_input_cannot_acknowledge_the_completion() {
|
||||
let mut completion = Completion::default();
|
||||
completion.queued(7);
|
||||
completion.reported(20);
|
||||
completion.landed(6, Some(20));
|
||||
assert!(completion.pending());
|
||||
completion.landed(8, None);
|
||||
assert!(completion.pending());
|
||||
completion.landed(7, Some(19));
|
||||
assert!(!completion.pending());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_completion_allows_retry() {
|
||||
let mut completion = Completion::default();
|
||||
completion.queued(7);
|
||||
completion.landed(7, None);
|
||||
assert!(!completion.pending());
|
||||
completion.queued(8);
|
||||
assert!(completion.pending());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_next_completion_requires_its_own_snapshot() {
|
||||
let mut completion = Completion::default();
|
||||
completion.reported(11);
|
||||
completion.queued(7);
|
||||
completion.landed(7, Some(11));
|
||||
assert!(!completion.pending());
|
||||
completion.queued(8);
|
||||
completion.landed(8, Some(12));
|
||||
assert!(completion.pending());
|
||||
completion.reported(11);
|
||||
assert!(completion.pending());
|
||||
completion.reported(12);
|
||||
assert!(!completion.pending());
|
||||
}
|
||||
}
|
||||
@@ -313,10 +313,14 @@ impl LokView {
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let completing = self.completion.pending();
|
||||
if self.walk(&event.keystroke, cx)
|
||||
|| self.turn_key(&event.keystroke)
|
||||
|| self.key_down(&event.keystroke)
|
||||
{
|
||||
if !completing && self.completion.pending() {
|
||||
cx.notify();
|
||||
}
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
@@ -431,8 +435,10 @@ impl LokView {
|
||||
self.send(Input::Paste);
|
||||
}
|
||||
|
||||
pub(super) fn on_next_field(&mut self, _: &NextField, _: &mut Window, _: &mut Context<Self>) {
|
||||
if !self.complete() {
|
||||
pub(super) fn on_next_field(&mut self, _: &NextField, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.complete() {
|
||||
cx.notify();
|
||||
} else {
|
||||
self.key_down(&Keystroke::parse("tab").expect("a keystroke"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ impl LokView {
|
||||
}
|
||||
self.edited += u64::from(edits);
|
||||
let (outcome, now) = outcome;
|
||||
self.completion.landed(seq, if outcome.is_ok() { now } else { None });
|
||||
if edits {
|
||||
self.clock.landed(now);
|
||||
}
|
||||
|
||||
@@ -43,12 +43,20 @@ fn enter_completes_the_selected_function_without_leaving_the_cell() {
|
||||
.address
|
||||
.is_empty()));
|
||||
let address = window.update(|_, app| view.read(app).cell.address.clone());
|
||||
view.update(window, |view, _| view.commit(None, "=look"));
|
||||
view.update(window, |view, _| view.commit(None, "=lo"));
|
||||
assert!(settle(window, |app| {
|
||||
let functions = &view.read(app).cell.functions;
|
||||
functions.len() == 1 && functions[0].name == "LOOKUP"
|
||||
functions.iter().any(|item| item.name == "LOOKUP")
|
||||
&& functions.iter().any(|item| item.name == "LOG")
|
||||
}));
|
||||
window.simulate_keystrokes("enter");
|
||||
// Keep two valid offers to exercise keyboard selection.
|
||||
view.update(window, |view, cx| {
|
||||
view.cell
|
||||
.functions
|
||||
.retain(|item| item.name == "LOG" || item.name == "LOOKUP");
|
||||
cx.notify();
|
||||
});
|
||||
window.simulate_keystrokes("down enter");
|
||||
assert!(
|
||||
settle(window, |app| {
|
||||
let view = view.read(app);
|
||||
@@ -104,16 +112,18 @@ fn accepting_a_candidate_consumes_it_before_the_worker_answers() {
|
||||
.any(|item| item.name == "LOOKUP")));
|
||||
let held = worker.lock().expect("the worker lock");
|
||||
cx.update(|app| {
|
||||
view.update(app, |view, _| {
|
||||
view.update(app, |view, cx| {
|
||||
let old = Events {
|
||||
revision: view.doc.rev,
|
||||
cell: view.cell.clone(),
|
||||
..Events::default()
|
||||
};
|
||||
let before = view.sent();
|
||||
assert!(view.key_down(&Keystroke::parse("enter").expect("Enter")));
|
||||
assert!(
|
||||
view.cell.functions.is_empty(),
|
||||
"accepted candidates cannot be reused"
|
||||
);
|
||||
view.apply_batch(old, None, cx);
|
||||
assert!(
|
||||
!view.complete(),
|
||||
"a second acceptance must not queue another completion"
|
||||
"an old event must not reoffer an accepted candidate"
|
||||
);
|
||||
assert_eq!(view.sent(), before + 1);
|
||||
assert!(view.key_down(&Keystroke::parse("enter").expect("Enter")));
|
||||
@@ -188,6 +198,17 @@ fn clicking_a_function_does_not_send_mouse_input_to_the_sheet() {
|
||||
"the candidate press never starts a sheet drag"
|
||||
);
|
||||
});
|
||||
// Change candidates without repainting the already pressed row.
|
||||
view.update(window, |view, _| {
|
||||
view.cell.functions.insert(
|
||||
0,
|
||||
crate::Function {
|
||||
name: "LOG".into(),
|
||||
signature: String::new(),
|
||||
description: String::new(),
|
||||
},
|
||||
);
|
||||
});
|
||||
window.simulate_mouse_up(on, gpui::MouseButton::Left, Default::default());
|
||||
assert!(
|
||||
settle(window, |app| {
|
||||
|
||||
Reference in new issue
Block a user