Search code examples
javascriptnode.jsreadline

How to modify cursor line in Node.js Readline?


I have REPL for my language in Node.js and I'm auto indenting lines when user type text by hand and press enter so he have text indented. The problem is that when he copy/paste text that have indents it have double indent.

The problem is that I'm adding indent before the newline happen. So my idea to fix this issue is to some how delete my spaces (rewrite the line) after user press enter and he already have spaces. But I don't know how to do that. Is it possible maybe using some ANSI codes.

my full REPL looks like this:

if (process.stdin.isTTY) {
    console.log(banner);
}
var prompt = 'lips> ';
var continuePrompt = '... ';
var terminal = !!process.stdin.isTTY && !(process.env.EMACS || process.env.INSIDE_EMACS);
const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
    prompt: prompt,
    terminal
});
if (process.stdin.isTTY) {
    rl.prompt();
}
var code = '';
var multiline = false;
var resolve;
var newline;
// we use promise loop to fix issue when copy paste list of S-Expression
var prev_eval = Promise.resolve();
boostrap(interp).then(function() {
    rl.on('line', function(line) {
        code += line.replace(/^\s+/, '') + '\n';
        try {
            if (balanced_parenthesis(code)) {
                rl.pause();
                prev_eval = prev_eval.then(function() {
                    var result = run(code, interp);
                    code = '';
                    return result;
                }).then(function(result) {
                    if (process.stdin.isTTY) {
                        print(result);
                        if (newline) {
                            // readline don't work with not endend lines
                            // it ignore those so we end then ourselfs
                            process.stdout.write("\n");
                            newline = false;
                        }
                        if (multiline) {
                            rl.setPrompt(prompt);
                            multiline = false;
                        }
                        rl.prompt();
                    }
                    rl.resume();
                }).catch(function() {
                    if (process.stdin.isTTY) {
                        if (multiline) {
                            rl.setPrompt(prompt);
                            multiline = false;
                        }
                        rl.prompt();
                    }
                });
            } else {
                multiline = true;
                var ind = indent(code, 2, prompt.length - continuePrompt.length);
                rl.setPrompt(continuePrompt);
                rl.prompt();
                var spaces = new Array(ind + 1).join(' ');
                if (terminal) {
                    rl.write(spaces);
                } else {
                    process.stdout.write(spaces);
                    code += spaces;
                }
            }
        } catch (e) {
            console.error(e.message);
            code = '';
            rl.prompt();
        }
    });
});

I'm using rl.write(spaces); in multiline command after user type his line and I need to check if

I think that I need is to modify the line that have cursor in Node.js Readline.

I've tried:

    rl.on('line', function(line) {
        if (line.match(/^\s/)) {
            rl.write(null, { ctrl: true, name: 'u' });
            rl.write((multiline ? continuePrompt : prompt) + line);
        }
        line = line.replace(/^\s+/, '');
        code += line + '\n';

and

    rl.on('line', function(line) {
        var have_spaces = line.match(/^\s/);
        line = line.replace(/^\s+/, '');
        if (have_spaces) {
            rl.pause();
            process.stdout.write('\x1b[1K\x1b[0K' + line);
            rl.resume();
        }
        code += line + '\n';

but both just echo in next line after I press enter, not the line that have cursor in. How to modify line that have cursor? I happy with library that will do that, I was not able to find any that will be helpful.


Solution

  • Here is what I've got, to exactly answer the question in title, you can modify the input using keypress event:

    rl._writeToOutput = function _writeToOutput(string) {
        rl.output.write(scheme(string));
    };
    process.stdin.on('keypress', (c, k) => {
        setTimeout(function() {
            rl._refreshLine(); // force refresh colors
        }, 0);
    });
    

    this highlight the line and refresh the line on each keypress to force colors.

    And for indent I've solved my issue using this code and ANSI escape codes.

    var terminal = !!process.stdin.isTTY && !(process.env.EMACS || process.env.INSIDE_EMACS);
    
    bootstrap(interp).then(function() {
        rl.on('line', function(line) {
            code += line;
            const lines = code.split('\n');
            const cols = process.stdout.columns;
            // fix formatting for previous lines that was echo
            // ReadLine will not handle those
            if (terminal && lines.length > 2) {
                var count = 0;
                // correction when line wrapps in original line
                // that will be overwritten
                lines.map(line => {
                    if (line.length > cols) {
                        count += Math.ceil(line.length / cols) - 1;
                    }
                });
                var f = new Formatter(code);
                code = f.format();
                const stdout = scheme(code).split('\n').map((line, i) => {
                    var prefix;
                    if (i === 0) {
                        prefix = unify_prompt(prompt, continuePrompt);
                    } else {
                        prefix = unify_prompt(continuePrompt, prompt);
                    }
                    return '\x1b[K' + prefix + line;
                }).join('\n');
                let num = lines.length + count;
                const format = `\x1b[${num}F${stdout}\n`;
                process.stdout.write(format);
            }
            code += '\n';
            ...
    

    There is only one issue with this code when lines are longer then the width of the terminal the updating of lines are broken, and when you copy paste text last line is not indented properly (it have double indent) until you press enter.