kkawakam / rustyline

Readline Implementation in Rust
https://crates.io/crates/rustyline/
MIT License
1.52k stars 175 forks source link

Could `Validator` also take the cursor position? #712

Closed segeljakt closed 1 year ago

segeljakt commented 1 year ago

If I give this input to my rustyline REPL (| is cursor):

>> if b { f() } else { f() }|

I have currently implemented that <Enter> evaluates the statement. I did this validating that the current line ends with }, and if so, yields ValidationResult::Valid and otherwise ValidationResult::Incomplete. However, if my cursor is here:

>> if b { f() } else {|}

I would like <Enter> to insert a newline:

>> >> if b { f() } else {
|}

This requires me to know the position of the cursor, but since validate only takes an input this is not possible:

impl Validator for MyValidator {
    fn validate(&self, ctx: &mut ValidationContext) -> Result<ValidationResult> {
        let input: &str = ctx.input();
        // ...
    }
}

Would it be feasible to extend Validator with this functionality?

gwenn commented 1 year ago

I almost sure that you can already do it using a ConditionalEventHandler without changing the Validator API. See ConditionalEventHandler And EventContext.

segeljakt commented 1 year ago

Oh, nice. Thanks

gwenn commented 1 year ago

Tell me if you have any trouble but the idea is to return either AcceptLine or Newline (or None) depending on the input / cursor / ...

segeljakt commented 1 year ago

Alright I managed to get it working 👍

pub(crate) struct BraceValidator;

impl ConditionalEventHandler for BraceValidator {
    fn handle(
        &self,
        evt: &rustyline::Event,
        n: rustyline::RepeatCount,
        positive: bool,
        ctx: &rustyline::EventContext,
    ) -> Option<rustyline::Cmd> {
        if ctx.line().ends_with('}') && ctx.pos() == ctx.line().len() {
            return Some(rustyline::Cmd::AcceptLine);
        } else {
            return None;
        }
    }
}

fn main() {
    let mut rl = Editor::<Repl, FileHistory>::new()?;

    rl.bind_sequence(
        KeyEvent::new('\r', Modifiers::NONE),
        EventHandler::Conditional(Box::new(BraceValidator)),
    );
}
segeljakt commented 1 year ago

Is KeyEvent::new('\r', Modifiers::NONE) the right event to use?

gwenn commented 1 year ago

Good question !

segeljakt commented 1 year ago

It works on macOS but I'm not sure how other platforms handle it. Maybe it's the same

gwenn commented 1 year ago

In fact, there is a simpler solution than the ConditionalEventHandler: https://docs.rs/rustyline/latest/rustyline/enum.Cmd.html#variant.AcceptOrInsertLine => accept_in_the_middle should be false. (Sorry I forgot...) And you want to overwrite these bindings: https://github.com/kkawakam/rustyline/blob/aecbf41252b22c4e19785342073c5695a56c4d2b/src/keymap.rs#L1050-L1052

gwenn commented 1 year ago

See https://github.com/edgedb/edgedb-cli/blob/b4b47021710a2bc425ba1a676eec704681b7fae7/src/migrations/prompt.rs#L80C1-L81

gwenn commented 1 year ago

And you should be able to use https://docs.rs/rustyline/latest/rustyline/enum.KeyCode.html#variant.Enter instead of '\r'...

gwenn commented 1 year ago

Sorry for the messy API.

segeljakt commented 1 year ago

Your solution works really nice, thanks 👍