Guard completion callbacks and cancel occluded grid drags

This commit is contained in:
Jetkwok committed 2026-10-20 11:42:13 +08:00
1 parent cc0f5a9caf
commit c5cb29fa34
9 files changed
+198 -22

No files matched your search

+3
View File
@@ -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;
+21 -8
View File
@@ -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());
}
}
+8 -2
View File
@@ -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"));
}
}
+1
View File
@@ -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);
}
+30 -9
View File
@@ -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| {