Skip to content

Commit

Permalink
completion: teach rename about local bookmarks
Browse files Browse the repository at this point in the history
  • Loading branch information
senekor committed Nov 10, 2024
1 parent 5cbd348 commit c9e751a
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 4 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

* New command `jj util exec` that can be used for arbitrary aliases.

* A preview of improved shell completions was added. Please refer to the
[documentation](https://martinvonz.github.io/jj/latest/install-and-setup/#command-line-completion)
to activate them.

### Fixed bugs

## [0.23.0] - 2024-11-06
Expand Down
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ clap = { version = "4.5.20", features = [
"wrap_help",
"string",
] }
clap_complete = "4.5.37"
clap_complete = { version = "4.5.37", features = ["unstable-dynamic"] }
clap_complete_nushell = "4.5.4"
clap-markdown = "0.1.4"
clap_mangen = "0.2.10"
Expand Down
15 changes: 12 additions & 3 deletions cli/src/cli_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ use std::cell::OnceCell;
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::env;
use std::env::ArgsOs;
use std::env::VarError;
use std::ffi::OsString;
use std::fmt;
Expand Down Expand Up @@ -2128,7 +2127,7 @@ impl WorkspaceCommandTransaction<'_> {
}
}

fn find_workspace_dir(cwd: &Path) -> &Path {
pub fn find_workspace_dir(cwd: &Path) -> &Path {
cwd.ancestors()
.find(|path| path.join(".jj").is_dir())
.unwrap_or(cwd)
Expand Down Expand Up @@ -3057,7 +3056,7 @@ fn handle_early_args(
pub fn expand_args(
ui: &Ui,
app: &Command,
args_os: ArgsOs,
args_os: impl IntoIterator<Item = OsString>,
config: &config::Config,
) -> Result<Vec<String>, CommandError> {
let mut string_args: Vec<String> = vec![];
Expand Down Expand Up @@ -3379,6 +3378,16 @@ impl CliRunner {
#[must_use]
#[instrument(skip(self))]
pub fn run(mut self) -> ExitCode {
match clap_complete::CompleteEnv::with_factory(|| self.app.clone())
.try_complete(env::args_os(), None)
{
Ok(true) => return ExitCode::SUCCESS,
Err(e) => {
eprintln!("failed to generate completions: {e}");
return ExitCode::FAILURE;
}
Ok(false) => {}
};
let builder = config::Config::builder().add_source(crate::config::default_config());
let config = self
.extra_configs
Expand Down
3 changes: 3 additions & 0 deletions cli/src/commands/bookmark/rename.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use clap_complete::ArgValueCandidates;
use jj_lib::op_store::RefTarget;

use super::has_tracked_remote_bookmarks;
use crate::cli_util::CommandHelper;
use crate::command_error::user_error;
use crate::command_error::CommandError;
use crate::complete;
use crate::ui::Ui;

/// Rename `old` bookmark name to `new` bookmark name
Expand All @@ -26,6 +28,7 @@ use crate::ui::Ui;
#[derive(clap::Args, Clone, Debug)]
pub struct BookmarkRenameArgs {
/// The old name of the bookmark
#[arg(add = ArgValueCandidates::new(complete::local_bookmarks))]
old: String,

/// The new name of the bookmark
Expand Down
126 changes: 126 additions & 0 deletions cli/src/complete.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright 2024 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use clap::FromArgMatches as _;
use clap_complete::CompletionCandidate;
use jj_lib::workspace::DefaultWorkspaceLoaderFactory;
use jj_lib::workspace::WorkspaceLoaderFactory as _;

use crate::cli_util::expand_args;
use crate::cli_util::find_workspace_dir;
use crate::cli_util::GlobalArgs;
use crate::command_error::user_error;
use crate::command_error::CommandError;
use crate::config::default_config;
use crate::config::LayeredConfigs;
use crate::ui::Ui;

pub fn local_bookmarks() -> Vec<CompletionCandidate> {
with_jj(|mut jj| {
let output = jj
.arg("bookmark")
.arg("list")
.arg("--template")
.arg(r#"if(!remote, name ++ "\n")"#)
.output()
.map_err(user_error)?;

Ok(String::from_utf8_lossy(&output.stdout)
.lines()
.map(CompletionCandidate::new)
.collect())
})
}

/// Shell out to jj during dynamic completion generation
///
/// In case of errors, print them and early return an empty vector.
fn with_jj<F>(completion_fn: F) -> Vec<CompletionCandidate>
where
F: FnOnce(std::process::Command) -> Result<Vec<CompletionCandidate>, CommandError>,
{
get_jj_command()
.and_then(completion_fn)
.unwrap_or_else(|e| {
eprintln!("{}", e.error);
Vec::new()
})
}

/// Shell out to jj during dynamic completion generation
///
/// This is necessary because dynamic completion code needs to be aware of
/// global configuration like custom storage backends. Dynamic completion
/// code via clap_complete doesn't accept arguments, so they cannot be passed
/// that way. Another solution would've been to use global mutable state, to
/// give completion code access to custom backends. Shelling out was chosen as
/// the preferred method, because it's more maintainable and the performance
/// requirements of completions aren't very high.
fn get_jj_command() -> Result<std::process::Command, CommandError> {
let current_exe = std::env::current_exe().map_err(user_error)?;
let mut command = std::process::Command::new(current_exe);

// Snapshotting could make completions much slower in some situations
// and be undesired by the user.
command.arg("--ignore-working-copy");
command.arg("--color=never");
command.arg("--no-pager");

// Parse some of the global args we care about for passing along to the
// child process. This shouldn't fail, since none of the global args are
// required.
let app = crate::commands::default_app();
let config = config::Config::builder()
.add_source(default_config())
.build()
.expect("default config should be valid");
let mut layered_configs = LayeredConfigs::from_environment(config);
let ui = Ui::with_config(&layered_configs.merge()).expect("default config should be valid");
let cwd = std::env::current_dir()
.and_then(|cwd| cwd.canonicalize())
.map_err(user_error)?;
let maybe_cwd_workspace_loader = DefaultWorkspaceLoaderFactory.create(find_workspace_dir(&cwd));
layered_configs.read_user_config().map_err(user_error)?;
if let Ok(loader) = &maybe_cwd_workspace_loader {
layered_configs
.read_repo_config(loader.repo_path())
.map_err(user_error)?;
}
let config = layered_configs.merge();
// skip 2 because of the clap_complete prelude: jj -- jj <actual args...>
let args = std::env::args_os().skip(2);
let args = expand_args(&ui, &app, args, &config)?;
let args = app
.clone()
.disable_version_flag(true)
.disable_help_flag(true)
.ignore_errors(true)
.try_get_matches_from(args)?;
let args: GlobalArgs = GlobalArgs::from_arg_matches(&args)?;

if let Some(repository) = args.repository {
command.arg("--repository");
command.arg(repository);
}
if let Some(at_operation) = args.at_operation {
command.arg("--at-operation");
command.arg(at_operation);
}
for config_toml in args.early_args.config_toml {
command.arg("--config-toml");
command.arg(config_toml);
}

Ok(command)
}
1 change: 1 addition & 0 deletions cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub mod cli_util;
pub mod command_error;
pub mod commands;
pub mod commit_templater;
pub mod complete;
pub mod config;
pub mod description_util;
pub mod diff_util;
Expand Down
1 change: 1 addition & 0 deletions cli/tests/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mod test_builtin_aliases;
mod test_checkout;
mod test_commit_command;
mod test_commit_template;
mod test_completion;
mod test_concurrent_operations;
mod test_config_command;
mod test_copy_detection;
Expand Down
78 changes: 78 additions & 0 deletions cli/tests/test_completion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2024 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::common::TestEnvironment;

#[test]
fn test_bookmark_rename() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]);
let repo_path = test_env.env_root().join("repo");

test_env.jj_cmd_ok(&repo_path, &["bookmark", "create", "aaa"]);
test_env.jj_cmd_ok(&repo_path, &["bookmark", "create", "bbb"]);

let mut test_env = test_env;
// Every shell hook is a little different, e.g. the zsh hooks add some
// additional environment variables. But this is irrelevant for the purpose
// of testing our own logic, so it's fine to test a single shell only.
test_env.add_env_var("COMPLETE", "fish");
let test_env = test_env;

let stdout = test_env.jj_cmd_success(&repo_path, &["--", "jj", "bookmark", "rename", ""]);
insta::assert_snapshot!(stdout, @r"
aaa
bbb
--repository Path to repository to operate on
--ignore-working-copy Don't snapshot the working copy, and don't update it
--ignore-immutable Allow rewriting immutable commits
--at-operation Operation to load the repo at
--debug Enable debug logging
--color When to colorize output (always, never, debug, auto)
--quiet Silence non-primary command output
--no-pager Disable the pager
--config-toml Additional configuration options (can be repeated)
--help Print help (see more with '--help')
");

let stdout = test_env.jj_cmd_success(&repo_path, &["--", "jj", "bookmark", "rename", "a"]);
insta::assert_snapshot!(stdout, @"aaa");
}

#[test]
fn test_global_arg_repository_is_respected() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]);
let repo_path = test_env.env_root().join("repo");

test_env.jj_cmd_ok(&repo_path, &["bookmark", "create", "aaa"]);

let mut test_env = test_env;
test_env.add_env_var("COMPLETE", "fish");
let test_env = test_env;

let stdout = test_env.jj_cmd_success(
test_env.env_root(),
&[
"--",
"jj",
"--repository",
"repo",
"bookmark",
"rename",
"a",
],
);
insta::assert_snapshot!(stdout, @"aaa");
}
29 changes: 29 additions & 0 deletions docs/install-and-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,25 @@ To set up command-line completion, source the output of
`jj util completion bash/zsh/fish`. Exactly how to source it
depends on your shell.

Improved completions are currently in the works, these will complete things
like bookmark names as well. You can activate them with the alternative "dynamic"
instructions below. Please let us know if you encounter any issues, so we can
ensure a smooth transition once we default to these new completions. If you
have ideas about specific completions that could be added, please share them
[here](https://github.com/martinvonz/jj/issues/4763).

### Bash

```shell
source <(jj util completion bash)
```

dynamic:

```shell
echo "source <(COMPLETE=bash jj)" >> ~/.bashrc
```

### Zsh

```shell
Expand All @@ -224,21 +237,37 @@ compinit
source <(jj util completion zsh)
```

dynamic:

```shell
echo "source <(COMPLETE=zsh jj)" >> ~/.zshrc
```

### Fish

```shell
jj util completion fish | source
```

dynamic:

```shell
echo "source (COMPLETE=fish jj | psub)" >> ~/.config/fish/config.fish
```

### Nushell

```nu
jj util completion nushell | save completions-jj.nu
use completions-jj.nu * # Or `source completions-jj.nu`
```

(dynamic completions not available yet)

### Xonsh

```shell
source-bash $(jj util completion)
```

(dynamic completions not available yet)

0 comments on commit c9e751a

Please sign in to comment.