Skip to content

Commit

Permalink
feat(cmd): Support completion for keywords and repo name (#76)
Browse files Browse the repository at this point in the history
  • Loading branch information
fioncat authored Jan 7, 2024
1 parent 1c66391 commit 0153e79
Show file tree
Hide file tree
Showing 8 changed files with 342 additions and 16 deletions.
6 changes: 6 additions & 0 deletions TODOLIST.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# TODOList

## v0.10.1

- [x] In `url`, expand the alias name.
- [ ] Support completion for `keyword` and `repo name`.
- [ ] Support `@[n]`, to select the `nth` visited repo. Usage: `rox home @3`, `rox home github @5`. The `n` can be defaulted to `5`.

## v0.10.0

- [x] Use `Cow`, to further reduce memory `clone` overhead.
Expand Down
72 changes: 70 additions & 2 deletions src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use strum::EnumVariantNames;

use crate::config::Config;
use crate::repo::database::{self, Database};
use crate::repo::keywords::Keywords;
use crate::term::{GitBranch, GitRemote};
use crate::{api, hashmap, term};

Expand Down Expand Up @@ -188,8 +189,9 @@ impl Completion {
pub fn repo_args(cfg: &Config, args: &[&str]) -> Result<CompletionResult> {
match args.len() {
0 | 1 => {
let to_complete = args.get(0).map(|s| *s).unwrap_or("");
let remotes = cfg.list_remotes();
Ok(CompletionResult::from(remotes))
Self::wrap_with_keywords(cfg, "", to_complete, remotes, false)
}
2 => {
let db = Database::load(cfg)?;
Expand All @@ -203,7 +205,7 @@ impl Completion {
.into_iter()
.map(|owner| format!("{}/", owner))
.collect();
return Ok(CompletionResult::from(items).no_space());
return Self::wrap_with_keywords(cfg, remote, query, items, true);
}

let (owner, _) = database::parse_owner(query);
Expand All @@ -219,6 +221,72 @@ impl Completion {
}
}

fn wrap_with_keywords(
cfg: &Config,
remote: &str,
to_complete: &str,
items: Vec<String>,
no_space: bool,
) -> Result<CompletionResult> {
if to_complete == "" {
if no_space {
return Ok(CompletionResult::from(items).no_space());
}
return Ok(CompletionResult::from(items));
}

let mut completion = vec![];
let mut found = false;
for item in items {
if item.starts_with(to_complete) {
found = true;
completion.push(item);
}
}
if found {
// The highest priority is given to the `items` - if `to_complete` matches any
// element in `items`, directly return the matching item, disregarding keywords
// and repository names.
if no_space {
return Ok(CompletionResult::from(completion).no_space());
}
return Ok(CompletionResult::from(completion));
}

// Return the matched keywords and repository names as the completion items.
let keywords = Keywords::load(cfg)?;
let mut keywords = keywords.complete(remote);
let db = Database::load(cfg)?;
let names: Vec<_> = if remote != "" {
db.list_by_remote(remote, &None)
} else {
db.list_all(&None)
}
.into_iter()
.map(|repo| repo.name.to_string())
.collect();
keywords.extend(names);
for kw in keywords {
if kw.starts_with(to_complete) {
completion.push(kw);
}
}

let mut set: HashSet<String> = HashSet::with_capacity(completion.len());
let completion: Vec<_> = completion
.into_iter()
.filter(|item| {
if set.contains(item.as_str()) {
return false;
}
set.insert(item.clone());
true
})
.collect();

Ok(CompletionResult::from(completion))
}

pub fn owner_args(cfg: &Config, args: &[&str]) -> Result<CompletionResult> {
match args.len() {
0 | 1 => {
Expand Down
5 changes: 5 additions & 0 deletions src/config/defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::collections::HashMap;

use crate::config::Docker;
use crate::config::RemoteConfig;
use crate::utils;

pub fn workspace() -> String {
String::from("~/dev")
Expand Down Expand Up @@ -50,6 +51,10 @@ pub fn docker_shell() -> String {
String::from("sh")
}

pub fn keyword_expire() -> u64 {
utils::DAY
}

pub fn empty_map<K, V>() -> HashMap<K, V> {
HashMap::new()
}
Expand Down
9 changes: 9 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ pub struct Config {
#[serde(default = "defaults::docker")]
pub docker: Docker,

#[serde(default = "defaults::keyword_expire")]
pub keyword_expire: u64,

/// The remotes config.
#[serde(default = "defaults::empty_map")]
pub remotes: HashMap<String, RemoteConfig>,
Expand Down Expand Up @@ -419,6 +422,7 @@ impl Config {
workspace: defaults::workspace(),
metadir: defaults::metadir(),
docker: defaults::docker(),
keyword_expire: defaults::keyword_expire(),
cmd: defaults::cmd(),
remotes: HashMap::new(),
release: defaults::release(),
Expand Down Expand Up @@ -533,6 +537,11 @@ impl Config {
pub fn now(&self) -> u64 {
self.now.unwrap()
}

#[cfg(test)]
pub fn set_now(&mut self, now: u64) {
self.now = Some(now);
}
}

#[cfg(test)]
Expand Down
43 changes: 33 additions & 10 deletions src/repo/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize};

use crate::api::Provider;
use crate::config::{Config, RemoteConfig};
use crate::repo::keywords::Keywords;
use crate::repo::{NameLevel, Repo};
use crate::utils::{self, FileLock};
use crate::{info, term};
Expand Down Expand Up @@ -1201,10 +1202,7 @@ impl<'a, T: TerminalHelper, P: ProviderBuilder> Selector<'_, T, P> {
}?;
Ok((repo, true))
}
None => {
let repo = db.must_get_fuzzy("", self.head)?;
Ok((repo, true))
}
None => self.fuzzy_get_repo(db, "", self.head),
}
}

Expand Down Expand Up @@ -1265,10 +1263,7 @@ impl<'a, T: TerminalHelper, P: ProviderBuilder> Selector<'_, T, P> {
if owner.is_empty() {
return match self.opts.mode {
SelectMode::Search => self.one_from_provider(db, &remote_cfg),
SelectMode::Fuzzy => {
let repo = db.must_get_fuzzy(remote, self.query)?;
Ok((repo, true))
}
SelectMode::Fuzzy => self.fuzzy_get_repo(db, remote, self.query),
};
}

Expand Down Expand Up @@ -1346,6 +1341,33 @@ impl<'a, T: TerminalHelper, P: ProviderBuilder> Selector<'_, T, P> {
Ok((repo, false))
}

/// Wrap fuzzy get, add keywords addon.
fn fuzzy_get_repo<'b, R, K>(
&self,
db: &'b Database,
remote: R,
keyword: K,
) -> Result<(Repo<'b>, bool)>
where
R: AsRef<str>,
K: AsRef<str>,
{
let repo = db.must_get_fuzzy(remote.as_ref(), keyword.as_ref())?;

if repo.name != keyword.as_ref() {
// If a fuzzy match hits a repository, record the fuzzy matching keywords in a
// file for automatic keyword completion. If the fuzzy match word exactly matches
// the repository name, no additional recording is needed, as the completion
// logic will automatically include the repository name in the completion
// candidates.
let mut keywords = Keywords::load(db.cfg)?;
keywords.upsert(remote.as_ref(), keyword.as_ref());
keywords.save()?;
}

Ok((repo, true))
}

/// Selecting multiple repositories from the local database.
///
/// # Returns
Expand Down Expand Up @@ -1389,7 +1411,7 @@ impl<'a, T: TerminalHelper, P: ProviderBuilder> Selector<'_, T, P> {
Ok((repos, NameLevel::Owner))
}
None => {
let repo = db.must_get_fuzzy("", self.head)?;
let (repo, _) = self.fuzzy_get_repo(db, "", self.head)?;
Ok((vec![repo], NameLevel::Owner))
}
};
Expand All @@ -1412,7 +1434,8 @@ impl<'a, T: TerminalHelper, P: ProviderBuilder> Selector<'_, T, P> {

let (owner, name) = parse_owner(self.query);
let repo = if owner.is_empty() {
db.must_get_fuzzy(remote, &name)?
let (repo, _) = self.fuzzy_get_repo(db, remote, &name)?;
repo
} else {
db.must_get(remote, &owner, &name)?
};
Expand Down
Loading

0 comments on commit 0153e79

Please # to comment.