`:` opens a vim-style command line. Every command is described once, in a typed table (name, the short form that still works, aliases, a `!`, its arguments and what it does), so parsing, completion and the help can never disagree. Short forms work as in vim (`:e`, `:vs`, `:q!`, `:x`), and a test checks that no typed name can mean two commands. Parsing turns a line into a typed Call, checking every argument against what the app offers (open documents, nodes, colors, styles, settings), or says what's wrong in words: the usage when an argument is missing, the choices when a word isn't one, "did you mean :fold?" for a typo. Documents and nodes match fuzzily (`:b gpl` finds Garden plans). While typing, a popup above the word lists what can finish it (command names by prefix, choices, files from the folders on disk, documents and nodes fuzzily), with Tab and Shift-Tab putting them in the line. Ghost text after the cursor comes from the history first, like fish's, then from the best candidate; → or Ctrl-e takes it. Up and Down browse the lines that start with what was typed. The line itself is a small input that steps over whole characters, accents and emoji included. The canvas example opens it with `:` and runs a handful of commands: :fold, :color, :border, :tag, :focus, :rename, :in (to an Inbox), :link, :unlink, :find, :version and :q. Co-Authored-By: Claude Opus 5.5 <[email protected]>
101 lines
3.3 KiB
Rust
101 lines
3.3 KiB
Rust
//! Earlier command lines, for Up and Down and for the ghost text.
|
|
|
|
/// How many lines are kept.
|
|
const LIMIT: usize = 200;
|
|
|
|
/// Earlier command lines, oldest first. A line is kept once: running it
|
|
/// again moves it to the end.
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
pub struct History {
|
|
lines: Vec<String>,
|
|
}
|
|
|
|
impl History {
|
|
/// A history holding these lines, oldest first (as loaded from disk).
|
|
pub fn from_lines(lines: Vec<String>) -> History {
|
|
let mut history = History::default();
|
|
for line in lines {
|
|
history.add(&line);
|
|
}
|
|
history
|
|
}
|
|
|
|
/// The lines, oldest first (to save them).
|
|
pub fn lines(&self) -> &[String] {
|
|
&self.lines
|
|
}
|
|
|
|
/// Remembers a line that was run.
|
|
pub fn add(&mut self, line: &str) {
|
|
let line = line.trim();
|
|
if line.is_empty() {
|
|
return;
|
|
}
|
|
self.lines.retain(|earlier| earlier != line);
|
|
self.lines.push(line.to_string());
|
|
if self.lines.len() > LIMIT {
|
|
self.lines.remove(0);
|
|
}
|
|
}
|
|
|
|
/// The line at an index (from [`older`](History::older) or
|
|
/// [`newer`](History::newer)).
|
|
pub fn get(&self, index: usize) -> Option<&str> {
|
|
self.lines.get(index).map(String::as_str)
|
|
}
|
|
|
|
/// The newest line before `before` (or before the end, for `None`) that
|
|
/// starts with `start`, as Up finds it.
|
|
pub fn older(&self, start: &str, before: Option<usize>) -> Option<usize> {
|
|
let end = before.unwrap_or(self.lines.len()).min(self.lines.len());
|
|
self.lines[..end].iter().rposition(|line| line.starts_with(start))
|
|
}
|
|
|
|
/// The oldest line after `after` that starts with `start`, as Down finds
|
|
/// it.
|
|
pub fn newer(&self, start: &str, after: usize) -> Option<usize> {
|
|
let from = after + 1;
|
|
self.lines.get(from..)?.iter().position(|line| line.starts_with(start)).map(|offset| from + offset)
|
|
}
|
|
|
|
/// The newest line that starts with `start` and goes further: what the
|
|
/// ghost text suggests, like fish's.
|
|
pub fn suggestion(&self, start: &str) -> Option<&str> {
|
|
if start.trim().is_empty() {
|
|
return None;
|
|
}
|
|
self.lines
|
|
.iter()
|
|
.rev()
|
|
.find(|line| line.starts_with(start) && line.len() > start.len())
|
|
.map(String::as_str)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn up_and_down_only_see_matching_lines() {
|
|
let history =
|
|
History::from_lines(vec!["fold open".into(), "e garden.cum".into(), "fold auto".into()]);
|
|
let up = history.older("fold", None).unwrap();
|
|
assert_eq!(history.get(up), Some("fold auto"));
|
|
let up_again = history.older("fold", Some(up)).unwrap();
|
|
assert_eq!(history.get(up_again), Some("fold open"));
|
|
assert_eq!(history.older("fold", Some(up_again)), None);
|
|
assert_eq!(history.newer("fold", up_again), Some(up));
|
|
assert_eq!(history.suggestion("e g"), Some("e garden.cum"));
|
|
assert_eq!(history.suggestion(""), None);
|
|
}
|
|
|
|
#[test]
|
|
fn running_a_line_again_moves_it_to_the_end() {
|
|
let mut history = History::from_lines(vec!["q".into(), "w".into()]);
|
|
history.add("q");
|
|
history.add(" ");
|
|
assert_eq!(history.lines(), ["w", "q"]);
|
|
}
|
|
}
|