-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
Command expansion v2 #11164
base: master
Are you sure you want to change the base?
Command expansion v2 #11164
Conversation
92beac3
to
21f1bb9
Compare
726f874
to
c3eff3e
Compare
I think this is good for review (lints should be good now) |
Rather than tying this to Just like Perform all the replacements for the variables first and then evaluate the I am working on #11149 to try to simplify the handling of args, and the way this is currently implemented would require a lot of changes for whichever would get merged second. Mostly with this pr's logic needing a complete rewrite. |
Yup but isn't shellword parsing done at each keypress for completion,... ? Would replacing before parsing using shellwords mean executing %sh at each keypress ? (I mean, each key typed after the %sh{...} |
Yeah, currently this pr touches Unless im missing something, this can be done with interpreting the args as the text input, |
It worked like this in the original PR but shellwords messed up with spaces inside variables expansion (eg: %sh{there are some spaces}) so I added a lil exception inside shellword to ignore spaces (actually, ignore everything) inside of a %{} but the expansion is only done when the command is ran |
Ah, I see. With the changes I am making for the args I changed the pub enum MappableCommand {
Typable {
name: String,
args: String,
doc: String,
},
Static {
name: &'static str,
fun: fn(cx: &mut Context),
doc: &'static str,
},
} Instead of the current pub fn execute(&self, cx: &mut Context) {
match &self {
Self::Typable { name, args, doc: _ } => {
if let Some(command) = typed::TYPABLE_COMMAND_MAP.get(name.as_str()) {
let mut cx = compositor::Context {
editor: cx.editor,
jobs: cx.jobs,
scroll: None,
};
if let Err(err) =
(command.fun)(&mut cx, Args::from(args), PromptEvent::Validate)
{
cx.editor.set_error(format!("{err}"));
}
}
}
Self::Static { fun, .. } => (fun)(cx),
}
} If we were to treat the expanding as a fancy replace, we can just replace the This could then be some rough form of the replacing: fn expand_variables<'a>(editor: &Editor, args: &'a str) -> anyhow::Result<Cow<'a, str>> {
let (view, doc) = current_ref!(editor);
let mut expanded = String::with_capacity(args.len());
let mut var = Tendril::new_const();
let mut chars = args.chars().peekable();
while let Some(c) = chars.next() {
if c == '%' {
if let Some('{') = chars.peek() {
chars.next(); // consume '{'
while let Some(&ch) = chars.peek() {
if ch == '}' {
chars.next(); // consume '}'
break;
}
var.push(ch);
chars.next();
}
match var.as_str() {
"basename" => {
let replacement = doc
.path()
.and_then(|it| it.file_name().and_then(|it| it.to_str()))
.unwrap();
expanded.push_str(replacement);
}
"filename" => {
let replacement = doc
.path()
.and_then(|path| path.parent())
.unwrap()
.to_str()
.unwrap();
expanded.push_str(replacement);
}
"dirname" => {
let replacement = doc
.path()
.and_then(|p| p.parent())
.and_then(std::path::Path::to_str)
.unwrap();
expanded.push_str(replacement);
}
"cwd" => {
let dir = helix_stdx::env::current_working_dir();
let replacement = dir.to_str().unwrap();
expanded.push_str(replacement);
}
"linenumber" => {
let replacement = (doc
.selection(view.id)
.primary()
.cursor_line(doc.text().slice(..))
+ 1)
.to_string();
expanded.push_str(&replacement);
}
"selection" => {
let replacement = doc
.selection(view.id)
.primary()
.fragment(doc.text().slice(..));
expanded.push_str(&replacement);
}
unknown => bail!("unknown variable `{unknown}`"),
}
// Clear for potential further variables to expand.
var.clear();
} else {
expanded.push(c);
}
} else {
expanded.push(c);
}
}
//... `%sh` stuff
Ok(expanded.into())
} to use it like : match expand_variables(cx.editor, args) {
Ok(args) => {
if let Err(err) =
(command.fun)(&mut cx, Args::from(&args), PromptEvent::Validate)
{
cx.editor.set_error(format!("{err}"));
}
}
Err(err) => cx.editor.set_error(format!("{err}")),
}; |
Make sense, it's much clearer this way to me Shall I update this PR to be rebased on yours or shall we directly integrate command expansions inside your refactoring ? |
I'm not sure what changes will be proposed for my |
Don't completions need this ? |
You mean for the |
Nope, I am talking about the code inside the (If the user type And if shellwords is messing up spaces inside of expansions I think it can mess up the completion part. But there is no reason a variable name would contain space like |
Yeah, I believe you can provide a |editor: &Editor, input: &str| {
let shellwords = Shellwords::from(input);
let command = shellwords.command();
if command.is_empty()
|| (shellwords.args().next().is_none() && !shellwords.ends_with_whitespace())
{
fuzzy_match(
input,
TYPABLE_COMMAND_LIST.iter().map(|command| command.name),
false,
)
.into_iter()
.map(|(name, _)| (0.., name.into()))
.collect()
} else {
// Otherwise, use the command's completer and the last shellword
// as completion input.
let (word, len) = shellwords
.args()
.last()
.map_or(("", 0), |last| (last, last.len()));
TYPABLE_COMMAND_MAP
.get(command)
.map(|tc| tc.completer_for_argument_number(argument_number_of(&shellwords)))
.map_or_else(Vec::new, |completer| {
completer(editor, word)
.into_iter()
.map(|(range, file)| {
let file = shellwords::escape(file);
// offset ranges to input
let offset = input.len() - len;
let range = (range.start + offset)..;
(range, file)
})
.collect()
})
}
}, // completion For instance this is how |
And perhaps its actually not desired to parse in a way that respects whitespace? With the If you wrote like let (word, len) = shellwords
.args()
// Special case for `%sh{...}` completions so that final `}` is excluded from matches.
// NOTE: User would have to be aware of how to enter input so that final `}` is not touching
// anything else.
.filter(|arg| *arg != "}")
.last()
.map_or(("", 0), |last| (last, last.len())); |
Is there a way we can increase the pop-up size? I'm attempting to get git log in a large pop-up instead of small one. |
Wouldn't more advanced solutions better suit your usecase ? (like a floating pane in zellij containing it (you can open it from helix) or something like that ?) But it should be possible, but not easy as this PR do not touch the TUI code part. We are using code already written and well integrated into helix. I don't know how hard it can be, and I think it's quite out of the scope of this PR. |
I did a test with and it is returning;
So it didn't evaluate the shell script. |
Yep, atm the PR does not handle completions inside of %sh, but I am waiting an answer from core maintainers about which direction we should follow with @RoloEdits as I don't want to waste my time writing code on this if it's going to be removed and re-written anyway haha |
But you should get the desired behavior using |
This was just an example. What I need is:
I moved back to the previous PR. It was working there. |
I don't think the core maintainers will allow this patch. Such kind of extensions will be covered with the Scheme language paradigm. I'd a similar issue where I wanted the current document's indentation size passed to formatter. Most likely when #10389 is fully ready probably. Though I wish patches like yours are merged along with #11149 |
If this works as-is, please just get it out in its current form so we can benefit from this feature on the next release. Any improvements to what this already does should then take the form of new (smaller and much more reviewable) PRs opened against master. Preventing this much-needed feature from possibly making the next release build over something as minor as autocompletion or filename suggestion niceties would be a shame. 🥺 |
With #11950 merged, there is now a conflict in the docs page. Here is a suggestion for a merged version that puts the expansions at the end of the typable commands section, shortens the heading, and adds an entry to the TOC. # Commands
- [Typable commands](#typable-commands)
- [Using variables](#using-variables-in-typable-commands)
- [Static commands](#static-commands)
## Typable commands
Typable commands are used from command mode and may take arguments. Command mode can be activated by pressing `:`. The built-in typable commands are:
{{#include ./generated/typable-cmd.md}}
### Using variables in typable commands
Helix provides several variables that can be used when typing commands or creating custom shortcuts. These variables are listed below:
| Variable | Description |
| --- | --- |
| `%{basename}` or `%{b}` | The name and extension of the currently focused file. |
| `%{dirname}` or `%{d}` | The absolute path of the parent directory of the currently focused file. |
| `%{cwd}` | The absolute path of the current working directory of Helix. |
| `%{repo}` | The absolute path of the VCS repository helix is opened in. Fallback to `cwd` if not inside a VCS repository|
| `%{filename}` or `%{f}` | The absolute path of the currently focused file. |
| `%{filename:rel}` | The relative path of the file according to `cwd` (will give absolute path if the file is not a child of the current working directory) |
| `%{filename:repo_rel}` | The relative path of the file according to `repo` (will give absolute path if the file is not a child of the VCS directory or the cwd) |
| `%{ext}` | The extension of the current file |
| `%{lang}` | The language of the current file |
| `%{linenumber}` | The line number where the primary cursor is positioned. |
| `%{cursorcolumn}` | The position of the primary cursor inside the current line. |
| `%{selection}` | The text selected by the primary cursor. |
| `%sh{cmd}` | Executes `cmd` with the default shell and returns the command output, if any. |
#### Example
```toml
[keys.normal]
# Print blame info for the line where the main cursor is.
C-b = ":echo %sh{git blame -L %{linenumber} %{filename}}"
```
## Static Commands
Static commands take no arguments and can be bound to keys. Static commands can also be executed from the command picker (`<space>?`). The built-in static commands are:
{{#include ./generated/static-cmd.md}} |
done 🎉 ! |
refactor: no longer special case for bracket lists refactor: no longer special case end space This was a hold over from before the `raw` function was added to `Args`. perf: remove `bytes` field to save 16 bytes From 56 bytes to 40, saving 16 bytes. perf: move `in_quotes` field to local variable perf: move `quote` field to local variable refactor: remove `is_finished` state from `Args` test: change example command to `read` `yank-join` now uses `raw` and thus would not be parsed with the `next` function so no longer applicable. refactor: remove unneeded range end for index refactor: remove backtracking escape check Instead, it can be tracked as the parser scans through the first time. refactor: clean up code and add more comments feat(shellwords): add test for empty values
refactor(commands): ignore unit pattern match refactor(commands): use `if let` over `matches!` refactor(commands): ignore unit pattern match perf(commands):remove unnecessary ref for a &str refactor(commands): change `MappableCommand` `args` from `Vec<String>` to String refactor(commands): use `Args::raw` over `fold`ing refactor: use `rest` in `set_option` command refactor: use `rest` in `toggle_option` command chore(dap): add TODOs to switch to `Args` perf: change `ok_or` to `ok_or_else` to make fn call lazy feat(shellwords): add `args_count` to `Args` fix(commands): default to `line_ending` for `yank_join` when args are empty
Stated here, it looks like #11149 is slated to be merged to main sometime after the next release. Now that its certain that the changes there are going to be allowed, it might be worth trying to base the changes here on that branch to stay ahead of the changes. There should only be minor changes left so I don't think there will be any huge breaking changes, specially if the approach taken for expanding the variables is done as we discussed before, treating it as a fancy replace. I'm willing to assist in any work that needs to be done to get it transitioned over. I have a branch I made that holds the concepts of what could be done here that could be used a base reference. The main change is that it would no longer be operating on a And I know it was previously stated that it should be under fn expand(shell: &[String], input: &str) -> anyhow::Result<Cow<str>> {
// ...
} If no expansion is needed then return the input: Working around a lot of command based things recently, #11149 #12288 , I have also started to look at #4423 as well, which would depend on the features added in those, as well as the changes here, specifically around handling arguments being passed. An example of what I was thinking for that would be like: [commands]
"wcd!" = [":write --force %{arg}", ":cd %sh{ %{arg} | path dirname }"]
# :wcd! %{repo}/sub/sub/file.txt Getting this merged as is takes priority, but I'd also like not to heavily block future work, if at all possible. I dont think anything will, but just so the thought is out there, id thought i'd share. As for some feedback on names, I wonder how people would feel about these changes?:
I still feel like |
Yay some good news here 😁 Thanks for this large explanation, I'll be working on this next week |
e9fd739
to
77482cd
Compare
Everything should be working like previously but rebased on your changes. I'll see later to change the variable names as this is "minor", I just wanna be sure everything is working fine before. I had to remove the filename_impl code to enable filename completion when typing a command. The issue was that it was causing a panic when the Tab key was pressed. This happened because the code attempted to replace text that didn't exist yet—the path to complete—since the path was being referenced by a variable, not directly present in the string. To handle this properly, additional code would be needed. I believe it's better to address this in a separate PR |
77482cd
to
01ab58e
Compare
5326f1e
to
c5a06ef
Compare
I like those changes |
I think these are more descriptive:
Reasons:
|
But in all honesty, discussions over these details may actually delay this PR so it won't be in the next release. What about if we merge it as-is, and then make further enhancements down the line if necessary? |
No matter what, this PR won't be included in the next release, as it depends on #11149, which will be merged after the next release. I really like I do think that: filename Is great. But yes I think everyone will have a different opinion and those names could even change after this PR so it's not that important |
Yes, if you look at https://github.com/helix-editor/helix/blob/changelog/CHANGELOG.md it says the first of the year. As not even one maintainer has left any reviews here yet, it wont be included for sure. Any big features will have to wait till next release cycle. And also, naming is hard. To me these can be succinct as the context is already set. These aren't from an unknown scope. Its all around the current buffer. (current buffer) file etc. Im sure this can change later on, but as far as I know, only one other variable is being worked on that is not in some way tied to the current buffer(with them being either a leaf or a stem to the buffer), that being |
I don't think the names should be completely self-descriptive. After all, there's documentation with the full meaning if one's unsure about the meaning of any. And conciseness has value, although I'm not sure how much these substitutions will be used "live" instead of living in a keybinding/command in the config file. |
Hi !
We talked about command expansions inside of #6979, but I created a new PR as the original PR reached a "stopping" point as the PR became inactive, mainly because the OP is currently busy.
Into this new PR , i rebased #6979 on master, and made some changes as @ksdrar told me to ;) (I think whitespaces are fixed as i handle completions inside of the shellwords parsing phase)
If you think creating a new PR is not a good idea i can of course close this one, but I think it would be great to finally get this feature !
Current variables:
%{basename}
or%{b}
%{dirname}
or%{d}
%{cwd}
%{repo}
cwd
if not inside a VCS repository%{filename}
or%{f}
%{filename:rel}
cwd
(will give absolute path if the file is not a child of the current working directory)%{filename:repo_rel}
repo
(will give absolute path if the file is not a child of the VCS directory or the cwd)%{ext}
%{lang}
%{linenumber}
%{cursorcolumn}
%{selection}
%sh{cmd}
cmd
with the default shell and returns the command output, if any.