Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support external subcommands: rg, diff, git-show (etc.) #1769

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

th1000s
Copy link
Collaborator

@th1000s th1000s commented Jul 22, 2024

  • Support external subcommands: rg, diff, git-show (etc.)

The possible command line now is:

delta <delta-args> [SUBCMD <subcmd-args>]

If the entire command line fails to parse because SUBCMD is unknown,
then try (until the next arg fails) parsing only,
and then parse and call SUBCMD.., output is piped into delta.
Other subcommands also take precedence over the diff/git-diff mode
(delta a b, where e.g. a=show and b=HEAD), and any diff call gets
converted into an external subcommand first.

Available are:
delta rg .. => rg --json .. | delta
delta a b .. => git diff a b .. | delta
delta show .. => git <color-on> show .. | delta

And various other git-CMDS: add, blame, checkout, diff, grep, log, reflog and stash.

The piping is not done by the shell, but delta, so the subcommands
are now child processes of delta.


  • Set calling process directly because delta started it

This info then takes precedence over whatever
start_determining_calling_process_in_thread() finds or rather
doesn't find.
(The simple yet generous SeqCst is used on purpose for the atomic operations.)


Looking at the Call { Delta, .. } enum from the --help PR, and how the diff sub-command works (both how -@ is needed to add extra args, and how its ouput is fed back into delta when running as a child process) I had the idea of generalizing that.

Deltas rg integration is great, but rg --json .. | delta is a bit clunky, now it is just delta rg ... This also allows using delta without touching any git configs ("try before you edit anything"), as a proof of concept delta show is implemented here.

This is a bit ... creative with the clap command line parser, but it is all quite contained.

@dandavison
Copy link
Owner

Nice, very interesting!

  • I'm sure you considered this but is supporting delta git $subcommand a possibility also?
  • Are there any parse ambiguity challenges, e.g. if someone tries to diff a file named rg etc?

Some miscellaneous comments about subcommands; not sure how relevant to this PR:

  • there are various existing options that perhaps should be subcommands themselves, such as --show-colors, --show-config, --show-syntax-themes, --show-themes, --parse-ansi. Ought we to consider making those subcommands before 1.0?
  • Would be nice to have auto-generated shell completion for all this (I believe the trend nowadays is for that to be another subcommand, i.e. eval $(delta completion bash) Use https://crates.io/crates/clap_complete?).
  • Also maybe eval $(delta toggle side-by-side) and eval $(delta toggle line-numbers) mutating $DELTA_FEATURES?

@th1000s
Copy link
Collaborator Author

th1000s commented Sep 30, 2024

I'm sure you considered this but is supporting delta git $subcommand a possibility also?

I used --color=always, which is not accepted by all subcommands - but by calling git -c color.ui=always .. any subcommand should become callable. Just check if git-$subcommand is in $PATH.

Are there any parse ambiguity challenges, e.g. if someone tries to diff a file named rg etc?

Yes, and in that case I would give priority to the subcommand. By using ./rg this can ways be overwritten.

Some miscellaneous comments about subcommands; not sure how relevant to this PR:

there are various existing options that perhaps should be subcommands themselves, such as --show-colors, --show-config, --show-syntax-themes, --show-themes, --parse-ansi. Ought we to consider making those subcommands before 1.0?

I think these are not used that often, I would leave them as flags (these being located in src/subcommands/ notwithstanding).

Would be nice to have auto-generated shell completion for all this (I believe the trend nowadays is for that to be another subcommand, i.e. eval $(delta completion bash) Use https://crates.io/crates/clap_complete?).

Already present, see --generate-completion :) But that could also become a subcommand like that.

Also maybe eval $(delta toggle side-by-side) and eval $(delta toggle line-numbers) mutating $DELTA_FEATURES?

Sure, should be possible as well.

@th1000s th1000s changed the title Generalize delta subcommands: rg, diff (implicit), show Generalize subcommands: rg, git-show (etc.), diff Nov 5, 2024
@th1000s
Copy link
Collaborator Author

th1000s commented Nov 5, 2024

I'm sure you considered this but is supporting delta git $subcommand a possibility also?

Now also possible!

However enabling all git-X commands directly would mean checking if "git $X" is a valid git subcommand first (while parsing the command line, repeatedly), so I use a specific list:

pub const SUBCOMMANDS: &[&str] = &[RG, "show", "log", "diff", "grep", "blame", GIT];

Are there any other useful commands? Also, can you check why the rg test fails on macOS?

@dandavison
Copy link
Owner

"show", "log", "diff", "grep", "blame"
Are there any other useful commands?

The ones that are coming to mind are add for git add -p, and reflog and stash for git reflog -p and git stash list -p.

@dandavison
Copy link
Owner

Also, can you check why the rg test fails on macOS?

It fails because of

if grep_cli::resolve_binary("rg").is_err() {

https://docs.rs/grep-cli/latest/grep_cli/fn.resolve_binary.html

On non-Windows, this is a no-op.

This API has always seemed very surprising to me. I've opened BurntSushi/ripgrep#2928 to ask about it and maybe make the docstring clearer.

@dandavison
Copy link
Owner

I use zsh and have eval "$(delta --generate-completion zsh 2>/dev/null)" in my shell config. It looks like there might be a shell completion challenge relating to this PR: normally, a command line like rg pattern xxx<TAB> completes xxx as a file path. But delta rg pattern xxx<TAB> is not doing that.

@th1000s th1000s marked this pull request as ready for review November 11, 2024 23:14
// start subprocesses:
// diff (fileA, fileB), and generic subcommands
pub mod diff;
mod generic_subcmd;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That name is a bit ... generic I think. Ideas?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

external!

@th1000s
Copy link
Collaborator Author

th1000s commented Nov 11, 2024

The ones that are coming to mind are add for git add -p, and reflog and stash for git reflog -p and git stash list -p.

Done.

[The rg test fails on macOS fail] because of
https://docs.rs/grep-cli/latest/grep_cli/fn.resolve_binary.html

Oh, thanks! Never would have suspected that, re-implemented some of try_resolve_binary() (which is not public!) in an almost-one-liner for the test.

shell completion

Indeed, I have also asked myself that for simpler stuff like writing my own git-foo command, the completion has to embed itself into the existing git completion.

@dandavison
Copy link
Owner

The ones that are coming to mind are add for git add -p, and reflog and stash for git reflog -p and git stash list -p.

And one more: I recently learned of the existence of checkout -p (from #1908)

@dandavison
Copy link
Owner

there are various existing options that perhaps should be subcommands themselves, such as --show-colors, --show-config, --show-syntax-themes, --show-themes, --parse-ansi. Ought we to consider making those subcommands before 1.0?

I think these are not used that often, I would leave them as flags (these being located in src/subcommands/ notwithstanding).

Also maybe eval $(delta toggle side-by-side) and eval $(delta toggle line-numbers) mutating $DELTA_FEATURES?

Sure, should be possible as well.

Just to mention one more -- I'm attracted to adding a "doctor" command that users can run to obtain a standard suite of diagnostics describing their environment, something like the direction started in #1193. Would something like that fit with the work in this branch as a delta doctor subcommand with doctor-specific arguments?

The possible command line now is:

  delta <delta-args> [SUBCMD <subcmd-args>]

If the entire command line fails to parse because SUBCMD is unknown,
then try (until the next arg fails) parsing <delta-args> only,
and then parse and call SUBCMD.., output is piped into delta.
Other subcommands also take precedence over the diff/git-diff mode
(`delta a b`, where e.g. a=show and b=HEAD), and any diff call gets
converted into an external subcommand first.

Available are:
  delta rg ..     => rg --json .. | delta
  delta a b ..    => git diff a b .. | delta
  delta show ..   => git <color-on> show .. | delta

And various other git-CMDS: add, blame, checkout, diff, grep, log, reflog and stash.

The piping is not done by the shell, but delta, so the subcommands
are now child processes of delta.
This info then takes precedence over whatever
start_determining_calling_process_in_thread() finds or rather
doesn't find.
(The simple yet generous SeqCst is used on purpose for the atomic operations.)
@th1000s th1000s changed the title Generalize subcommands: rg, git-show (etc.), diff Support external subcommands: rg, diff, git-show (etc.) Nov 16, 2024
@th1000s
Copy link
Collaborator Author

th1000s commented Nov 16, 2024

checkout -p

Added.

The doctor or other subcommands can be added via clap directly, or --show-color can be become a show-color subcommand if you prefer that. These here are external (renamed it to that!) subcommands which have to side-step clap and pipe their output back into the parent process.

Copy link
Owner

@dandavison dandavison left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gone through the code changes now; all looks very nice. Please excuse/ignore the code golf suggestions -- that was just helping me get into the code.

Before we proceed let's revisit the overall argument here. For rg this is clearly an improvement: otherwise to make delta-rg integration ergonomic you have to write a shell wrapper.

Another possibility along the lines of the rg integration might be visualizing difftastic JSON output.

But what's your thinking about the utility of this for git xxx commands? delta integrates with git transparently via git's external pager mechanism, and many shell completion projects exist giving intricate completion over git commands. Is there a clear advantage to being able to invoke git commands prefixed by delta, especially given that it wouldn't have shell completion? Also, if we were to do it, are we sure that we want to inject 9 (currently) git command names into delta's top-level subcommand namespace? Or should it only support delta git xxx? delta diff in particular seems to have potentially confusing semantics given the presence of the delta a b diff. Sorry I've been slow to bring up these hesitations.

Comment on lines +1284 to +1285
if let Some(subcmd) = &minus_file {
if let Some(arg) = subcmd.to_str() {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is basically just code golf, done while trying to get into the substance of the PR. Feel free to ignore.

Suggested change
if let Some(subcmd) = &minus_file {
if let Some(arg) = subcmd.to_str() {
if let Some(arg) = minus_file.as_ref().and_then(|p| p.to_str()) {

}
Ok(matches) => {
// subcommands take precedence over diffs
let minus_file = matches.get_one::<PathBuf>("minus_file").map(PathBuf::from);
Copy link
Owner

@dandavison dandavison Nov 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is our support for delta file_a file_b going to cause trouble when introducing standard clap subcommands?

Comment on lines +59 to +64
for (i, arg) in args.iter().enumerate() {
let arg = if let Some(arg) = arg.to_str() {
arg
} else {
continue;
};
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, more code golf suggestions:

Suggested change
for (i, arg) in args.iter().enumerate() {
let arg = if let Some(arg) = arg.to_str() {
arg
} else {
continue;
};
for (i, arg) in args.iter().filter_map(|a| a.to_str()).enumerate() {

SubCmdKind::Rg
} else {
SubCmdKind::Git(
args[i..]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This constructs Git("git") in the case of delta git show. Is that intentional / does it matter?

.to_string(),
)
};
subcmd.extend(args[i + 1..].iter().map(|arg| arg.into()));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More ignorable code golf:

There might be an attractive alternative way to write the above using .split_first()? The best I've come up with is

                    let invalid_placeholder = OsString::from("?");
                    let (subsubcmd, rest) = args[i..]
                        .split_first()
                        .unwrap_or((&invalid_placeholder, &[]));
                    let kind = if arg == RG {
                        SubCmdKind::Rg
                    } else {
                        SubCmdKind::Git(subsubcmd.to_string_lossy().to_string())
                    };
                    subcmd.extend(rest.iter().cloned());

}
}

pub fn extract(args: &[OsString], orig_error: Error) -> (ArgMatches, SubCommand) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A docstring could be good. My attempt:

Suggested change
pub fn extract(args: &[OsString], orig_error: Error) -> (ArgMatches, SubCommand) {
/// Find the first arg that is a registered external subcommand and return a
/// tuple containing:
/// 0. The args prior to that point
/// 1. A SubCommand representing the external subcommand and its subsequent args
pub fn extract(args: &[OsString], orig_error: Error) -> (ArgMatches, SubCommand) {

@@ -1283,31 +1274,55 @@ impl Opt {
Call::Help(help)
}
Err(e) => {
e.exit();
// Calls `e.exit()` if error persists.
let (matches, subcmd) = subcommands::extract(args, e);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clever...

}
let mut cmd = cmd.unwrap();

let cmd_stdout = cmd.stdout.as_mut().expect("Failed to open stdout");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a so-far-upheld tradition of using unwrap_or_else(|| panic(...)) because of my personal belief that expect() is confusing as reader and author, but I'd certainly understand if you wished to discontinue that tradition.

@dandavison
Copy link
Owner

dandavison commented Nov 21, 2024

This is slightly off-topic because I think this would be an internal rather than an external subcommand, but I just found myself doing delta /dev/null path/to/myfile in order to "cat" a file but with delta's hyperlinked line numbers (which I have set up to open in my IDE). So perhaps that suggests delta cat might be another entry in our subcommand namespace, with the green background color disabled, and the minus line number column removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants