Juanjo Labrador
Juanjo Labrador

Reputation: 41

NodeJS readline autocomplete several words

I'm making a simple CLI app with NodeJS using the Readline module. I want to autocomplete the user's input. For this, I'm using the autocompletion function of the module:

function completer(line) {
   const completions = '.help .error .exit .quit .q'.split(' ');
   const hits = completions.filter((c) => c.startsWith(line));
   // show all completions if none found
   return [hits.length ? hits : completions, line];
}

With this function, I'm able to complete one command but no several commands in the same line:

For example:

(CLI App) > .e<tab>
            .error .exit

(CLI App) > .err<tab>
(CLI App) > .error

(CLI App) > .error .ex<tab>
            .help .error .exit .quit .q

I modified the completer function to get only the autocompletion's suggestions of the current command that the user is writing:

function completer(line) {
   const completions = '.help .error .exit .quit .q'.split(' ');
   const hits = completions.filter((c) => c.startsWith(line.split(' ').slice(-1)));

   return [hits.length ? hits : completions, line];
}

and I get the correct suggestions but the user input doesn't change:

(CLI App) > .e<tab>
            .error .exit

(CLI App) > .err<tab>
(CLI App) > .error

(CLI App) > .error .ex<tab>
            .exit
(CLI App) > .error .ex

Is there any way to solve this? Any assistance you can give would be very appreciated.

Thanks.

Upvotes: 2

Views: 2563

Answers (2)

tmajest
tmajest

Reputation: 360

I wanted to simplify the accepted answer since I found a slightly different solution.

I found that you don't need to reset the line or cursor if you change the last argument in the result array. Instead of returning the entire line, you can return the word or command that you are currently completing and the readline completer will take care of everything else for you:

function completer(line) {
  const parts = line.trim().split(/\s+/);
  const completions = '.help .error .exit .quit .q'.split(' ');
  const command = parts.slice(-1)[0];

  const hits = completions.filter((c) => c.startsWith(command));
  return [hits.length ? hits : completions, command];
}

Upvotes: 0

Juanjo Labrador
Juanjo Labrador

Reputation: 41

Using the Chris's tip I got a solution: replace the last part of the line with the hit (when I only have one).

I calculate the length of the last part of the line (the actual command that I want to autocomplete) to move the cursor to the begin of this command. Then, I get all the line minus the current command and I concat the hit. Finally, I set the cursor at the end of the line.

I tried to use methods from the docs without luck: readline.cursorTo(stream, x, y) and readline.moveCursor(stream, dx, dy) don't work for me.

Thereadline.clearLine(stream, dir) method clear all the line and no 'to the right from cursor' (the behaviour that I want), despite of it's present in the doc.

function completer(line) {
    const completions = '.help .error .exit .quit .q'.split(' ');
    let cmds = line.split(' ');
    const hits = completions.filter((c) => c.startsWith(cmds.slice(-1)));

    if ((cmds.length > 1) && (hits.length === 1)) {
        let lastCmd = cmds.slice(-1)[0];
        let pos = lastCmd.length;
        rl.line = line.slice(0, -pos).concat(hits[0]);
        rl.cursor = rl.line.length + 1;
    }

    return [hits.length ? hits.sort() : completions.sort(), line];
}    

Upvotes: 2

Related Questions