Skip to content

feat: add support for fish-like abbreviations#1060

Merged
fdncred merged 7 commits into
nushell:mainfrom
casedami:feat/abbreviations
May 13, 2026
Merged

feat: add support for fish-like abbreviations#1060
fdncred merged 7 commits into
nushell:mainfrom
casedami:feat/abbreviations

Conversation

@casedami
Copy link
Copy Markdown
Contributor

@casedami casedami commented Apr 21, 2026

Following up on my comment in #556 i figured i would go ahead and get something working. This would be the initial step to getting abbreviations in nushell and would require a parsing implementation abbreviations to be stored in nushell config and then passed to reedline. Open to any comments or opinions as far as implementation goes but this is a feature that i would personally love to have in nushell.

I've essentially added a check during the following events that will check if the word before the cursor is an abbreviation and expand it if true:

  • Submit and SubmitOrNewline
  • Enter
  • Edit if the first command in the event is to insert a space

As for adding this to nushell, here's a simple example of how this might look in the nushell config:

diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs
index c1fea4150..239153226 100644
--- a/crates/nu-cli/src/repl.rs
+++ b/crates/nu-cli/src/repl.rs
@@ -414,6 +414,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
         )))
         .with_quick_completions(config.completions.quick)
         .with_partial_completions(config.completions.partial)
+        .with_abbreviations(config.abbreviations)
         .with_ansi_colors(config.use_ansi_coloring.get(engine_state))
         .with_cwd(Some(
             engine_state
diff --git a/crates/nu-protocol/src/config/mod.rs b/crates/nu-protocol/src/config/mod.rs
index 37fa177a5..9769160e9 100644
--- a/crates/nu-protocol/src/config/mod.rs
+++ b/crates/nu-protocol/src/config/mod.rs
@@ -63,6 +63,7 @@ pub struct Config {
     pub hinter: HinterConfig,
     pub history: HistoryConfig,
     pub keybindings: Vec<ParsedKeybinding>,
+    pub abbreviations: HashMap<String, String>,
     pub menus: Vec<ParsedMenu>,
     pub hooks: Hooks,
     pub rm: RmConfig,
@@ -135,6 +136,8 @@ impl Default for Config {

             keybindings: Vec::new(),

+            abbreviations: HashMap::new(),
+
             error_style: ErrorStyle::default(),
             error_lines: 1,
             display_errors: DisplayErrors::default(),
@@ -218,6 +221,10 @@ impl UpdateFromValue for Config {
                     Ok(keybindings) => self.keybindings = keybindings,
                     Err(err) => errors.error(err.into()),
                 },
+                "abbreviations" => match HashMap::from_value(val.clone()) {
+                    Ok(abbreviations) => self.abbreviations = abbreviations,
+                    Err(err) => errors.error(err.into()),
+                },
                 "hooks" => self.hooks.update(val, path, errors),
                 "datetime_format" => self.datetime_format.update(val, path, errors),
                 "error_style" => self.error_style.update(val, path, errors),

Note, that the is_inside_string_literal function could potentially be used for a bug fix for

As an aside, here's how im currently getting abbreviations with this hacky solution

let abbrevs = {
    ga: 'git add'
    gb: 'git branch'
    gc: 'git commit -v'
    gd: 'git diff'
    gdt: 'git difftool -d'
    gm: 'git merge'
    gr: 'git rebase'
    gR: 'git restore'
    gwa: 'git worktree add'
    gwr: 'git worktree remove'
    gwl: 'git worktree list'
    gx: 'git switch'
    la: 'ls -a'
    ll: 'ls -l'
    fg: 'job unfreeze'
    ou: 'overlay use'
    oh: 'overlay hide'
    ol: 'overlay list'
}

$env.config = {
    menus: [
        {
            name: abbr_menu
            only_buffer_difference: false
            marker: none
            type: {
                layout: columnar
                columns: 1
                col_width: 20
                col_padding: 2
            }
            style: {text: green, selected_text: green_reverse, description_text: yellow}
            source: {|buffer, position|
                let match = $abbrevs | columns | where $it == $buffer
                if ($match | is-empty) {
                    {value: $buffer}
                } else {
                    {
                        value: ($abbrevs | get $match.0)
                    }
                }
            }
        }
    ]
}

@casedami
Copy link
Copy Markdown
Contributor Author

@fdncred any feedback on this? the implementation is a little crude, it could be modified to more closely resemble how keybindings are defined

@fdncred
Copy link
Copy Markdown
Contributor

fdncred commented May 11, 2026

Sorry, it's hard to fine people to review reedline PRs. It would be good to have an example with it too.

@casedami casedami closed this May 11, 2026
@casedami casedami reopened this May 11, 2026
@casedami
Copy link
Copy Markdown
Contributor Author

recording.mp4

no worries. i'm assuming you mean an example of it in action and i've included one here. just showing that it works anywhere in the buffer and doesn't expand when it appears as a subword.

to produce this, i modified the basic example like so:

diff --git a/examples/basic.rs b/examples/basic.rs
index 825d954..dc0c680 100644
--- a/examples/basic.rs
+++ b/examples/basic.rs
@@ -4,11 +4,13 @@
 // You can browse the local (non persistent) history using Up/Down or Ctrl n/p.

 use reedline::{DefaultPrompt, Reedline, Signal};
-use std::io;
+use std::{collections::HashMap, io};

 fn main() -> io::Result<()> {
+    let mut abbrevs = HashMap::new();
+    abbrevs.insert(String::from("ll"), String::from("ls -l"));
     // Create a new Reedline engine with a local History that is not synchronized to a file.
-    let mut line_editor = Reedline::create();
+    let mut line_editor = Reedline::create().with_abbreviations(abbrevs);
     let prompt = DefaultPrompt::default();

     loop {

@fdncred
Copy link
Copy Markdown
Contributor

fdncred commented May 11, 2026

I mean there should be a special example in the reedline examples folder. Let's not modify an existing one please. you can copy it and change it but let's keep the examples the way they are except for your new one.

What is the trigger to expand the abbreviation? Is it just the space after ll or are you hitting some key combination?

@casedami
Copy link
Copy Markdown
Contributor Author

can do. yes the trigger is space after ll as well as enter

@fdncred
Copy link
Copy Markdown
Contributor

fdncred commented May 11, 2026

I think it's cool. Let's see what copilot thinks of your changes.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds first-pass “fish-like abbreviations” support to the Reedline engine, allowing configured abbreviation keys (e.g. ll) to expand into longer commands during common editing/submit flows.

Changes:

  • Add an abbreviations: HashMap<String, String> store to Reedline plus a with_abbreviations builder.
  • Attempt abbreviation expansion on Enter/Submit/SubmitOrNewline, and after an Edit that inserts a space.
  • Add a string-literal detection helper and an example demonstrating abbreviation setup.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
src/engine.rs Stores abbreviations, exposes builder, and triggers expansion during submit/space insertion events.
src/core_editor/editor.rs Adds is_inside_string_literal helper used to suppress expansion inside quotes.
examples/abbreviations.rs New example showing how to configure and use abbreviations.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/engine.rs Outdated
Comment thread src/engine.rs
Comment thread src/engine.rs Outdated
Comment thread src/core_editor/editor.rs Outdated
Comment thread src/engine.rs
Comment thread src/core_editor/editor.rs Outdated
Comment thread src/core_editor/editor.rs
@fdncred fdncred merged commit 7f99c6b into nushell:main May 13, 2026
7 checks passed
@fdncred
Copy link
Copy Markdown
Contributor

fdncred commented May 13, 2026

Thanks! Are you interested in adding the ability to use this functionality in nushell?

@casedami
Copy link
Copy Markdown
Contributor Author

Yeah I can work on that! Thanks!

@fdncred
Copy link
Copy Markdown
Contributor

fdncred commented May 13, 2026

Make sure to do cargo update -p reedline on the nushell repo, then inspect the cargo.lock and reject any changes that aren't reedline. Cargo likes to downgrade windows stuff for some reason. Thanks!

Comment thread src/core_editor/editor.rs
}

/// Check if `position` (a byte offset) is inside an unclosed string literal.
pub fn is_inside_string_literal(&self, position: usize) -> bool {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hi @casedami, I'm afraid reedline isn't the right place for such checking. I mean different downstream shells might have different syntaxes of string literal. For example, in nushell, there's another form of string literal in backticks.

Copy link
Copy Markdown
Contributor

@blindFS blindFS May 13, 2026

Choose a reason for hiding this comment

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

Besides, this task is actually much more difficult than it seems because of string interpolation

Copy link
Copy Markdown
Contributor Author

@casedami casedami May 14, 2026

Choose a reason for hiding this comment

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

That's a good point, hadn't considered downstream shell usage. If you're more comfortable removing this func and leaving this as a problem to tackle in another way down the line then I can submit a PR that removes this. Would also apply to #1073

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@casedami Sorry, I didn't go over the linked issues in detail, according to my limited knowledge, the "ideal" way for such matter would be a protocol to query downstream apps for the string literal checking result, and they might be able to reuse the cached parsing results from the highlighting task.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I mean, this func should be removed one way or another. I just don't know what impact it will land on the abbreviation feature, if just more false positives, acceptable IMHO.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Basically the change to the abbreviation feature would be that they now expand even when you're typing inside a string literal, so overall it's fine to remove it I would say

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Basically the change to the abbreviation feature would be that they now expand even when you're typing inside a string literal, so overall it's fine to remove it I would say

Sure, let's do this please

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.

4 participants