Add ability to extend cheatsheets (#382)

Fixes #377
This commit is contained in:
Denis Isidoro 2020-08-23 20:09:27 -03:00 committed by GitHub
parent e64d1cc8aa
commit 00fe27ba79
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 106 additions and 26 deletions

2
Cargo.lock generated
View file

@ -174,7 +174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "navi"
version = "2.8.0"
version = "2.9.0"
dependencies = [
"anyhow 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)",
"dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)",

View file

@ -1,6 +1,6 @@
[package]
name = "navi"
version = "2.8.0"
version = "2.9.0"
authors = ["Denis Isidoro <denis_isidoro@live.com>"]
edition = "2018"
description = "An interactive cheatsheet tool for the command-line"

View file

@ -185,7 +185,8 @@ $ branch: git branch | awk '{print $NF}'
- lines starting with `%` determine the start of a new cheatsheet and should contain tags, useful for searching;
- lines starting with `#` should be descriptions of commands;
- lines starting with `;` are ignored. You can use them for metacomments;
- lines starting with `$` should contain commands that generate a list of possible values for a given argument;
- lines starting with `$` should contain [commands that generate a list of possible values for a given argument](#variables);
- lines starting with `@` should contain [tags whose associated cheatsheet you want to base on](#extending-cheatsheets);
- all the other non-empty lines are considered as executable commands.
It's irrelevant how many files are used to store cheatsheets. They can be all in a single file if you wish, as long as you split them accordingly with lines starting with `%`.
@ -232,7 +233,16 @@ In addition, it's possible to forward the following parameters to `fzf`:
### Variable dependency
The command for generating possible inputs can refer previous variables:
The command for generating possible inputs can implicitly refer previous variables by using the `<varname>` syntax:
```sh
# Should print /my/pictures/wallpapers
echo "<wallpaper_folder>"
$ pictures_folder: echo "/my/pictures"
$ wallpaper_folder: echo "<pictures_folder>/wallpapers"
```
If you want to make dependencies explicit, you can use the `$varname` syntax:
```sh
# If you select "hello" for <x>, the possible values of <y> will be "hello foo" and "hello bar"
echo <x> <y>
@ -244,13 +254,26 @@ $ x: echo "hello hi" | tr ' ' '\n'
$ y: echo "$x foo;$x bar" | tr ';' '\n'
```
If you want to have implicit variable dependency, you can use the `<varname>` syntax inside a variable command:
### Extending cheatsheets
With the `@ same tags from other cheatsheet` syntax you can reuse the same variable in multiple cheatsheets.
```sh
# Should print /my/pictures/wallpapers
echo "<wallpaper_folder>"
% dirs, common
$ pictures_folder: echo "/my/pictures"
$ wallpaper_folder: echo "<pictures_folder>/wallpapers"
% wallpapers
@ dirs, common
# Should print /my/pictures/wallpapers
echo "<pictures_folder>/wallpapers"
% screenshots
@ dirs, common
# Should print /my/pictures/screenshots
echo "<pictures_folder>/screenshots"
```
### Multiline snippets

View file

@ -66,7 +66,7 @@ pub fn suggestions(config: Config, dry_run: bool) -> Result<(), Error> {
let capture = display::VAR_REGEX.captures_iter(&snippet).next();
let bracketed_varname = &(capture.expect("Invalid capture"))[0];
let varname = &bracketed_varname[1..bracketed_varname.len() - 1];
let command = variables.get(&tags, &varname);
let command = variables.get_suggestion(&tags, &varname);
if dry_run {
if command.is_none() {

View file

@ -148,7 +148,7 @@ fn replace_variables_from_snippet(
e
} else {
variables
.get(&tags, &variable_name)
.get_suggestion(&tags, &variable_name)
.ok_or_else(|| anyhow!("No suggestions"))
.and_then(|suggestion| {
let mut new_suggestion = suggestion.clone();

View file

@ -160,7 +160,6 @@ fn read_file(
// duplicate
if !tags.is_empty() && !comment.is_empty() {}
// blank
if line.is_empty() {
}
@ -176,6 +175,15 @@ fn read_file(
String::from("")
};
}
// depedency
else if line.starts_with('@') {
let tags_dependency = if line.len() > 2 {
String::from(&line[2..])
} else {
String::from("")
};
variables.insert_dependency(&tags, &tags_dependency);
}
// metacomment
else if line.starts_with(';') {
}
@ -204,7 +212,7 @@ fn read_file(
path
)
})?;
variables.insert(&tags, &variable, (String::from(command), opts));
variables.insert_suggestion(&tags, &variable, (String::from(command), opts));
}
// snippet
else {
@ -331,7 +339,7 @@ mod tests {
..Default::default()
}),
);
let actual_suggestion = variables.get("ssh", "user");
let actual_suggestion = variables.get_suggestion("ssh", "user");
assert_eq!(Some(&expected_suggestion), actual_suggestion);
}

View file

@ -5,26 +5,56 @@ use std::collections::HashMap;
pub type Suggestion = (String, Option<Opts>);
#[derive(Clone)]
pub struct VariableMap(HashMap<u64, HashMap<String, Suggestion>>);
pub struct VariableMap {
variables: HashMap<u64, HashMap<String, Suggestion>>,
dependencies: HashMap<u64, Vec<u64>>,
}
impl VariableMap {
pub fn new() -> Self {
Self(HashMap::new())
Self {
variables: HashMap::new(),
dependencies: HashMap::new(),
}
}
pub fn insert(&mut self, tags: &str, variable: &str, value: Suggestion) {
pub fn insert_dependency(&mut self, tags: &str, tags_dependency: &str) {
let k = tags.hash_line();
if let Some(v) = self.dependencies.get_mut(&k) {
v.push(tags_dependency.hash_line());
} else {
let mut v: Vec<u64> = Vec::new();
v.push(tags_dependency.hash_line());
self.dependencies.insert(k, v);
}
}
pub fn insert_suggestion(&mut self, tags: &str, variable: &str, value: Suggestion) {
let k1 = tags.hash_line();
let k2 = String::from(variable);
if let Some(m) = self.0.get_mut(&k1) {
if let Some(m) = self.variables.get_mut(&k1) {
m.insert(k2, value);
} else {
let mut m = HashMap::new();
m.insert(k2, value);
self.0.insert(k1, m);
self.variables.insert(k1, m);
}
}
pub fn get(&self, tags: &str, variable: &str) -> Option<&Suggestion> {
self.0.get(&tags.hash_line())?.get(variable)
pub fn get_suggestion(&self, tags: &str, variable: &str) -> Option<&Suggestion> {
let k = tags.hash_line();
let res = self.variables.get(&k)?.get(variable);
if res.is_some() {
return res;
}
if let Some(dependency_keys) = self.dependencies.get(&k) {
for dependency_key in dependency_keys {
let res = self.variables.get(&dependency_key)?.get(variable);
if res.is_some() {
return res;
}
}
}
None
}
}

View file

@ -1,6 +1,6 @@
; author: CI/CD
% test, ci/cd
% test, first
# trivial case -> "foo"
echo "foo"
@ -34,9 +34,6 @@ echo "<x> <x2> <y> <x>"
# variable dependency, we can ignore intermediate values -> "12"
: <x>; echo "<x2>"
# nested used value -> "path: /my/pictures/wallpapers"
echo "path: <wallpaper_folder>"
# nested unused value -> "path: /my/pictures"
echo "path: <pictures_folder>"
@ -47,7 +44,29 @@ $ language: echo '0 rust rust-lang.org' --- --column 2
$ language2: echo '1;clojure;clojure.org' --- --column 2 --delimiter ';'
$ multiword: echo 'foo bar'
$ pictures_folder: echo "/my/pictures"
$ wallpaper_folder: echo "<pictures_folder>/wallpapers"
# this should be displayed -> "hi"
echo hi
echo hi
% test, second
@ test, first
@ test, third
# nested used value -> "path: /my/pictures/wallpapers"
echo "path: <wallpaper_folder>"
# same command as before -> "12"
: <x>; echo "<x2>"
# the order isn't relevant -> "br"
echo "<country>"
$ wallpaper_folder: echo "<pictures_folder>/wallpapers"
% test, third
; this cheathsheet doesnt have any commands
$ country: echo "br"