Files
ElyOffice/crates/ely-lok/src/view/formula.rs
T
ZacharyZhang-NY b7fca0146d Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
2026-10-08 21:20:08 -04:00

129 lines
4.5 KiB
Rust

//! Formula entry: reference marks and function hints (TABLE-03).
use ely_gpui_component::{
editor::{CompletionItem, CompletionMenu, Signature, SignatureHelp, SymbolKind, completions},
primitives::raise,
};
use gpui::{
AnyElement, Bounds, Context, Hsla, IntoElement, Keystroke, Modifiers, ParentElement, Pixels,
anchored, rgb,
};
use super::{LokView, input::Input};
impl LokView {
/// The sheet shown, which reference marks follow.
pub fn set_part(&mut self, part: usize) {
self.part = part;
}
/// The shown sheet's reference marks, in window pixels.
pub(super) fn marks(&self) -> Vec<(Bounds<Pixels>, Hsla)> {
self.cell
.marks
.iter()
.filter(|mark| mark.part == self.part)
.map(|mark| (self.screen_rect(mark.rect), rgb(mark.color).into()))
.collect()
}
/// The offered functions, in the order the menu shows.
fn offered(&self) -> Vec<CompletionItem> {
let items: Vec<CompletionItem> = self
.cell
.functions
.iter()
.map(|function| {
CompletionItem::new(function.name.clone(), SymbolKind::Function)
.detail(function.signature.clone())
.docs(function.description.clone())
})
.collect();
completions(&items, "")
.into_iter()
.map(|(item, _)| item)
.collect()
}
/// 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() {
return false;
}
self.picked = match keystroke.key.as_str() {
"down" => (self.picked + 1) % count,
"up" => (self.picked + count - 1) % count,
_ => return false,
};
cx.notify();
true
}
/// Takes the chosen function, in order with typing.
pub(super) fn complete(&mut self) -> bool {
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()));
true
}
/// The offered functions, else the call's usage, under the cell.
pub(super) fn hints(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
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();
cx.notify();
})
})
.into_any_element()
};
let place = anchored()
.position(cell.bottom_left())
.snap_to_window()
.child(hint);
Some(raise("lok-view-hints", place).into_any_element())
}
}
/// The kernel's usage text as a signature.
fn usage(text: &str) -> Option<Signature> {
let (call, docs) = text.split_once(" : ").unwrap_or((text, ""));
let at = call.find('►')?;
let label = call.replacen('►', "", 1);
let rest = &label[at..];
let end = at + rest.find([',', ';', ')']).unwrap_or(rest.len());
Some(Signature {
label: label.clone().into(),
params: std::iter::once(at..end).collect(),
docs: (!docs.is_empty()).then(|| docs.to_string().into()),
})
}
#[cfg(test)]
mod tests {
use super::usage;
#[test]
fn the_kernel_usage_reads_as_a_signature() {
let signature = usage("SUM(► Number 1, Number 2) : The numbers.").expect("a usage");
assert_eq!(signature.label.as_ref(), "SUM( Number 1, Number 2)");
assert_eq!(&signature.label[signature.params[0].clone()], " Number 1");
assert_eq!(signature.docs.as_deref(), Some("The numbers."));
assert!(usage("").is_none());
}
}