Skip to content

Commit

Permalink
completion: suggest file paths incrementally
Browse files Browse the repository at this point in the history
If there are multiple files in a subdirectory that are candidates for
completion, only complete the common directory prefix to reduce the number of
completion candidates shown at once.

This matches the normal shell completion of file paths.
  • Loading branch information
senekor committed Nov 17, 2024
1 parent 57a5f7f commit 41fb2f8
Show file tree
Hide file tree
Showing 13 changed files with 133 additions and 44 deletions.
4 changes: 2 additions & 2 deletions cli/src/commands/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use clap_complete::ArgValueCandidates;
use clap_complete::ArgValueCompleter;
use jj_lib::backend::Signature;
use jj_lib::object_id::ObjectId;
use jj_lib::repo::Repo;
Expand Down Expand Up @@ -42,7 +42,7 @@ pub(crate) struct CommitArgs {
#[arg(long = "message", short, value_name = "MESSAGE")]
message_paragraphs: Vec<String>,
/// Put these paths in the first commit
#[arg(add = ArgValueCandidates::new(complete::modified_files))]
#[arg(add = ArgValueCompleter::new(complete::modified_files))]
paths: Vec<String>,
/// Reset the author to the configured user
///
Expand Down
3 changes: 2 additions & 1 deletion cli/src/commands/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

use clap_complete::ArgValueCandidates;
use clap_complete::ArgValueCompleter;
use itertools::Itertools;
use jj_lib::copies::CopyRecords;
use jj_lib::repo::Repo;
Expand Down Expand Up @@ -57,7 +58,7 @@ pub(crate) struct DiffArgs {
#[arg(long, conflicts_with = "revision", add = ArgValueCandidates::new(complete::all_revisions))]
to: Option<RevisionArg>,
/// Restrict the diff to these paths
#[arg(add = ArgValueCandidates::new(complete::modified_revision_or_range_files))]
#[arg(add = ArgValueCompleter::new(complete::modified_revision_or_range_files))]
paths: Vec<String>,
#[command(flatten)]
format: DiffFormatArgs,
Expand Down
3 changes: 2 additions & 1 deletion cli/src/commands/file/annotate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

use clap_complete::ArgValueCandidates;
use clap_complete::ArgValueCompleter;
use jj_lib::annotate::get_annotation_for_file;
use jj_lib::annotate::FileAnnotation;
use jj_lib::commit::Commit;
Expand All @@ -37,7 +38,7 @@ use crate::ui::Ui;
#[derive(clap::Args, Clone, Debug)]
pub(crate) struct FileAnnotateArgs {
/// the file to annotate
#[arg(add = ArgValueCandidates::new(complete::all_revision_files))]
#[arg(add = ArgValueCompleter::new(complete::all_revision_files))]
path: String,
/// an optional revision to start at
#[arg(long, short, add = ArgValueCandidates::new(complete::all_revisions))]
Expand Down
3 changes: 2 additions & 1 deletion cli/src/commands/file/chmod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

use clap_complete::ArgValueCandidates;
use clap_complete::ArgValueCompleter;
use jj_lib::backend::TreeValue;
use jj_lib::merged_tree::MergedTreeBuilder;
use jj_lib::object_id::ObjectId;
Expand Down Expand Up @@ -52,7 +53,7 @@ pub(crate) struct FileChmodArgs {
)]
revision: RevisionArg,
/// Paths to change the executable bit for
#[arg(required = true, add = ArgValueCandidates::new(complete::all_revision_files))]
#[arg(required = true, add = ArgValueCompleter::new(complete::all_revision_files))]
paths: Vec<String>,
}

Expand Down
3 changes: 2 additions & 1 deletion cli/src/commands/file/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use std::io;
use std::io::Write;

use clap_complete::ArgValueCandidates;
use clap_complete::ArgValueCompleter;
use jj_lib::backend::BackendResult;
use jj_lib::conflicts::materialize_merge_result;
use jj_lib::conflicts::materialize_tree_value;
Expand Down Expand Up @@ -51,7 +52,7 @@ pub(crate) struct FileShowArgs {
)]
revision: RevisionArg,
/// Paths to print
#[arg(required = true, add = ArgValueCandidates::new(complete::all_revision_files))]
#[arg(required = true, add = ArgValueCompleter::new(complete::all_revision_files))]
paths: Vec<String>,
}

Expand Down
4 changes: 2 additions & 2 deletions cli/src/commands/file/untrack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

use std::io::Write;

use clap_complete::ArgValueCandidates;
use clap_complete::ArgValueCompleter;
use itertools::Itertools;
use jj_lib::merge::Merge;
use jj_lib::merged_tree::MergedTreeBuilder;
Expand All @@ -35,7 +35,7 @@ pub(crate) struct FileUntrackArgs {
///
/// The paths could be ignored via a .gitignore or .git/info/exclude (in
/// colocated repos).
#[arg(required = true, add = ArgValueCandidates::new(complete::all_revision_files))]
#[arg(required = true, add = ArgValueCompleter::new(complete::all_revision_files))]
paths: Vec<String>,
}

Expand Down
3 changes: 2 additions & 1 deletion cli/src/commands/interdiff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use std::slice;

use clap::ArgGroup;
use clap_complete::ArgValueCandidates;
use clap_complete::ArgValueCompleter;
use tracing::instrument;

use crate::cli_util::CommandHelper;
Expand All @@ -42,7 +43,7 @@ pub(crate) struct InterdiffArgs {
#[arg(long, add = ArgValueCandidates::new(complete::all_revisions))]
to: Option<RevisionArg>,
/// Restrict the diff to these paths
#[arg(add = ArgValueCandidates::new(complete::interdiff_files))]
#[arg(add = ArgValueCompleter::new(complete::interdiff_files))]
paths: Vec<String>,
#[command(flatten)]
format: DiffFormatArgs,
Expand Down
3 changes: 2 additions & 1 deletion cli/src/commands/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use std::io::Write;

use clap_complete::ArgValueCandidates;
use clap_complete::ArgValueCompleter;
use itertools::Itertools;
use jj_lib::object_id::ObjectId;
use tracing::instrument;
Expand Down Expand Up @@ -62,7 +63,7 @@ pub(crate) struct ResolveArgs {
/// will attempt to resolve the first conflict we can find. You can use
/// the `--list` argument to find paths to use here.
// TODO: Find the conflict we can resolve even if it's not the first one.
#[arg(add = ArgValueCandidates::new(complete::revision_conflicted_files))]
#[arg(add = ArgValueCompleter::new(complete::revision_conflicted_files))]
paths: Vec<String>,
}

Expand Down
3 changes: 2 additions & 1 deletion cli/src/commands/restore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use std::io::Write;

use clap_complete::ArgValueCandidates;
use clap_complete::ArgValueCompleter;
use jj_lib::object_id::ObjectId;
use jj_lib::rewrite::restore_tree;
use tracing::instrument;
Expand Down Expand Up @@ -45,7 +46,7 @@ use crate::ui::Ui;
#[derive(clap::Args, Clone, Debug)]
pub(crate) struct RestoreArgs {
/// Restore only these paths (instead of all paths)
#[arg(add = ArgValueCandidates::new(complete::modified_range_files))]
#[arg(add = ArgValueCompleter::new(complete::modified_range_files))]
paths: Vec<String>,
/// Revision to restore from (source)
#[arg(long, add = ArgValueCandidates::new(complete::all_revisions))]
Expand Down
3 changes: 2 additions & 1 deletion cli/src/commands/split.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use std::io::Write;

use clap_complete::ArgValueCandidates;
use clap_complete::ArgValueCompleter;
use jj_lib::object_id::ObjectId;
use jj_lib::repo::Repo;
use tracing::instrument;
Expand Down Expand Up @@ -66,7 +67,7 @@ pub(crate) struct SplitArgs {
#[arg(long, short, alias = "siblings")]
parallel: bool,
/// Put these paths in the first commit
#[arg(add = ArgValueCandidates::new(complete::modified_revision_files))]
#[arg(add = ArgValueCompleter::new(complete::modified_revision_files))]
paths: Vec<String>,
}

Expand Down
3 changes: 2 additions & 1 deletion cli/src/commands/squash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

use clap_complete::ArgValueCandidates;
use clap_complete::ArgValueCompleter;
use itertools::Itertools as _;
use jj_lib::commit::Commit;
use jj_lib::commit::CommitIteratorExt;
Expand Down Expand Up @@ -92,7 +93,7 @@ pub(crate) struct SquashArgs {
/// Move only changes to these paths (instead of all paths)
#[arg(
conflicts_with_all = ["interactive", "tool"],
add = ArgValueCandidates::new(complete::squash_revision_files),
add = ArgValueCompleter::new(complete::squash_revision_files),
)]
paths: Vec<String>,
/// The source revision will not be abandoned
Expand Down
100 changes: 75 additions & 25 deletions cli/src/complete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,17 @@ mod parse {
}
}

fn all_files_from_rev(rev: String) -> Vec<CompletionCandidate> {
fn dir_prefix_from<'a>(path: &'a str, current: &str) -> Option<&'a str> {
let remainder = path.strip_prefix(current)?;
remainder
.split_once('/')
.map(|(next, _)| path.split_at(current.len() + next.len() + 1).0)
}

fn all_files_from_rev(rev: String, current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
let Some(current) = current.to_str() else {
return Vec::new();
};
with_jj(|mut jj, _| {
let output = jj
.arg("file")
Expand All @@ -407,14 +417,31 @@ fn all_files_from_rev(rev: String) -> Vec<CompletionCandidate> {
.map_err(user_error)?;
let stdout = String::from_utf8_lossy(&output.stdout);

Ok(stdout.lines().map(CompletionCandidate::new).collect())
Ok(stdout
.lines()
.filter_map(|path| {
if !path.starts_with(current) {
return None;
}
if let Some(dir_path) = dir_prefix_from(path, current) {
return Some(CompletionCandidate::new(dir_path));
}

Some(CompletionCandidate::new(path))
})
.dedup() // directories may occur multiple times
.collect())
})
}

fn modified_files_from_rev(
rev: (String, Option<String>),
interdiff: bool,
current: &std::ffi::OsStr,
) -> Vec<CompletionCandidate> {
let Some(current) = current.to_str() else {
return Vec::new();
};
with_jj(|mut jj, _| {
let cmd = jj
.arg(if interdiff { "interdiff" } else { "diff" })
Expand All @@ -428,10 +455,18 @@ fn modified_files_from_rev(

Ok(stdout
.lines()
.map(|line| {
.filter_map(|line| {
let (mode, path) = line
.split_once(' ')
.expect("diff --summary should contain a space between mode and path");

if !path.starts_with(current) {
return None;
}
if let Some(dir_path) = dir_prefix_from(path, current) {
return Some(CompletionCandidate::new(dir_path));
}

let help = match mode {
"M" => "Modified".into(),
"D" => "Deleted".into(),
Expand All @@ -440,13 +475,17 @@ fn modified_files_from_rev(
"C" => "Copied".into(),
_ => format!("unknown mode: '{mode}'"),
};
CompletionCandidate::new(path).help(Some(help.into()))
Some(CompletionCandidate::new(path).help(Some(help.into())))
})
.dedup() // directories may occur multiple times
.collect())
})
}

fn conflicted_files_from_rev(rev: &str) -> Vec<CompletionCandidate> {
fn conflicted_files_from_rev(rev: &str, current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
let Some(current) = current.to_str() else {
return Vec::new();
};
with_jj(|mut jj, _| {
let output = jj
.arg("resolve")
Expand All @@ -459,54 +498,65 @@ fn conflicted_files_from_rev(rev: &str) -> Vec<CompletionCandidate> {

Ok(stdout
.lines()
.filter_map(|line| line.split_whitespace().next())
.map(CompletionCandidate::new)
.filter_map(|line| {
let path = line.split_whitespace().next()?;

if !path.starts_with(current) {
return None;
}
if let Some(dir_path) = dir_prefix_from(path, current) {
return Some(CompletionCandidate::new(dir_path));
}

Some(CompletionCandidate::new(path))
})
.dedup() // directories may occur multiple times
.collect())
})
}

pub fn modified_files() -> Vec<CompletionCandidate> {
modified_files_from_rev(("@".into(), None), false)
pub fn modified_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
modified_files_from_rev(("@".into(), None), false, current)
}

pub fn all_revision_files() -> Vec<CompletionCandidate> {
all_files_from_rev(parse::revision_or_wc())
pub fn all_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
all_files_from_rev(parse::revision_or_wc(), current)
}

pub fn modified_revision_files() -> Vec<CompletionCandidate> {
modified_files_from_rev((parse::revision_or_wc(), None), false)
pub fn modified_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
modified_files_from_rev((parse::revision_or_wc(), None), false, current)
}

pub fn modified_range_files() -> Vec<CompletionCandidate> {
pub fn modified_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
match parse::range() {
Some((from, to)) => modified_files_from_rev((from, Some(to)), false),
None => modified_files_from_rev(("@".into(), None), false),
Some((from, to)) => modified_files_from_rev((from, Some(to)), false, current),
None => modified_files_from_rev(("@".into(), None), false, current),
}
}

pub fn modified_revision_or_range_files() -> Vec<CompletionCandidate> {
pub fn modified_revision_or_range_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
if let Some(rev) = parse::revision() {
return modified_files_from_rev((rev, None), false);
return modified_files_from_rev((rev, None), false, current);
}
modified_range_files()
modified_range_files(current)
}

pub fn revision_conflicted_files() -> Vec<CompletionCandidate> {
conflicted_files_from_rev(&parse::revision_or_wc())
pub fn revision_conflicted_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
conflicted_files_from_rev(&parse::revision_or_wc(), current)
}

/// Specific function for completing file paths for `jj squash`
pub fn squash_revision_files() -> Vec<CompletionCandidate> {
pub fn squash_revision_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
let rev = parse::squash_revision().unwrap_or_else(|| "@".into());
modified_files_from_rev((rev, None), false)
modified_files_from_rev((rev, None), false, current)
}

/// Specific function for completing file paths for `jj interdiff`
pub fn interdiff_files() -> Vec<CompletionCandidate> {
pub fn interdiff_files(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
let Some((from, to)) = parse::range() else {
return Vec::new();
};
modified_files_from_rev((from, Some(to)), true)
modified_files_from_rev((from, Some(to)), true, current)
}

/// Shell out to jj during dynamic completion generation
Expand Down
Loading

0 comments on commit 41fb2f8

Please sign in to comment.