Skip to content

Commit c41ae78

Browse files
committed
fix paste brackets mode + cleanup + code comments
1 parent ff3c697 commit c41ae78

File tree

1 file changed

+98
-71
lines changed

1 file changed

+98
-71
lines changed

bin/lips.js

Lines changed: 98 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -433,14 +433,6 @@ if (options.version || options.V) {
433433
setupHistory(rl, terminal ? env.LIPS_REPL_HISTORY : '', run_repl);
434434
}
435435

436-
function unify_prompt(a, b) {
437-
var result = a;
438-
if (a.length < b.length) {
439-
result += ' '.repeat(b.length - a.length);
440-
}
441-
return result;
442-
}
443-
444436
function is_open(token) {
445437
return ['(', '['].includes(token);
446438
}
@@ -449,25 +441,83 @@ function is_close(token) {
449441
return [')', ']'].includes(token);
450442
}
451443

452-
function strip_ansi(string) {
453-
return string.replace(/\x1b\[[0-9;]*m/g, '');
444+
// find matching open parentheses. The last token is always
445+
// a closing parenthesis
446+
function matched_token(code) {
447+
let count = 0;
448+
// true to tokenize return tokens with meta data
449+
return tokenize(code, true).reverse().find(token => {
450+
if (is_open(token.token)) {
451+
count--;
452+
} else if (is_close(token.token)) {
453+
count++;
454+
}
455+
return is_open(token.token) && count === 0;
456+
});
457+
}
458+
459+
// hightlight the open parentheses based on token metadata
460+
function mark_paren(code, token) {
461+
const re = /(\x1b\[[0-9;]*m)/;
462+
let str_len = 0, found;
463+
return code.split(re).reduce((acc, str) => {
464+
let result
465+
if (str.match(re)) {
466+
result = str;
467+
} else if (found) {
468+
result = str;
469+
} else if (str_len + str.length <= token.offset) {
470+
result = str;
471+
str_len += str.length;
472+
} else {
473+
const pos = token.offset - str_len;
474+
const before = str.substring(0, pos);
475+
const after = str.substring(pos + 1);
476+
result = `${before}\x1b[7m(\x1b[m${after}`;
477+
found = true;
478+
}
479+
return acc + result;
480+
}, '');
481+
}
482+
483+
// function accept ANSI (syntax highlighted code) and
484+
// return ANSI escapes to overwrite the code
485+
// this is needed to make sure that syntax hightlighting
486+
// is always correct
487+
function ansi_rewrite_above(ansi_code) {
488+
const lines = ansi_code.split('\n');
489+
const stdout = lines.map((line, i) => {
490+
const prefix = i === 0 ? prompt : continue_prompt;
491+
return '\x1b[K' + prefix + line;
492+
}).join('\n');
493+
const len = lines.length;
494+
// overwrite all lines to get rid of any artifacts left my stdin
495+
// mostly because of parenthesis matching
496+
return `\x1b[${len}F${stdout}\n`;
454497
}
455498

456499
function run_repl(err, rl) {
457500
const dynamic = options.d || options.dynamic;
458501
let cmd = '';
459502
let multiline = false;
460503
let resolve;
504+
const brackets_re = /\x1b\[(200|201)~/g;
461505
// we use promise loop to fix issue when copy paste list of S-Expression
462506
let prev_eval = Promise.resolve();
463507
if (process.stdin.isTTY) {
464508
rl.prompt();
465509
}
466510
let prev_line;
467511
const is_emacs = process.env.INSIDE_EMACS;
512+
function is_brackets_mode() {
513+
return cmd.match(brackets_re);
514+
}
468515
function continue_multiline(code) {
469516
multiline = true;
470-
if (cmd.match(/\x1b\[200~/) || !supports_paste_brackets) {
517+
// we don't do indentation for paste bracket mode
518+
// indentation will also not work for old Node.js
519+
// becase it's too problematice to make it right
520+
if (is_brackets_mode() || !supports_paste_brackets) {
471521
rl.prompt();
472522
if (is_emacs) {
473523
rl.setPrompt('');
@@ -495,98 +545,75 @@ function run_repl(err, rl) {
495545
}
496546
}
497547
}
498-
function matched_token(code) {
499-
let count = 0;
500-
return tokenize(code, true).reverse().find(token => {
501-
if (is_open(token.token)) {
502-
count--;
503-
} else if (is_close(token.token)) {
504-
count++;
505-
}
506-
return is_open(token.token) && count === 0;
507-
});
508-
}
509-
function mark_paren(code, token) {
510-
const re = /(\x1b\[[0-9;]*m)/;
511-
let str_len = 0, found;
512-
return code.split(re).reduce((acc, str) => {
513-
let result
514-
if (str.match(re)) {
515-
result = str;
516-
} else if (found) {
517-
result = str;
518-
} else if (str_len + str.length <= token.offset) {
519-
result = str;
520-
str_len += str.length;
521-
} else {
522-
const pos = token.offset - str_len;
523-
const before = str.substring(0, pos);
524-
const after = str.substring(pos + 1);
525-
result = `${before}\x1b[7m(\x1b[m${after}`;
526-
found = true;
527-
}
528-
return acc + result;
529-
}, '');
530-
}
531-
function ansi_rewrite_above(ansi_code) {
532-
const lines = ansi_code.split('\n');
533-
const stdout = lines.map((line, i) => {
534-
const prefix = i === 0 ? prompt : continue_prompt;
535-
return '\x1b[K' + prefix + line;
536-
}).join('\n');
537-
const len = lines.length;
538-
// overwrite all lines to get rid of any artifacts left my stdin
539-
// mostly because of parenthesis matching
540-
return `\x1b[${len}F${stdout}\n`;
548+
function char_before_cursor() {
549+
return rl.line[rl.cursor - 1];
541550
}
542551
rl._writeToOutput = function _writeToOutput(string) {
543552
try {
544553
const prefix = multiline ? continue_prompt : prompt;
545554
const current_line = prefix + rl.line;
546555
let code = scheme(string);
547-
let code_above = cmd && scheme(cmd.substring(0, cmd.length - 1));
548-
if (rl.line[rl.cursor - 1] === ')') {
549-
const substring = rl.line.substring(0, rl.cursor);
550-
const input = prefix + substring;
551-
const token = matched_token(input);
552-
if (token) {
553-
code = mark_paren(code, token);
554-
} else if (cmd) {
555-
const input = cmd + rl.line;
556+
if (!is_brackets_mode()) {
557+
// we remove traling newline from cmd
558+
let code_above = cmd && scheme(cmd.substring(0, cmd.length - 1));
559+
if (char_before_cursor() === ')') {
560+
const substring = rl.line.substring(0, rl.cursor);
561+
const input = prefix + substring;
556562
const token = matched_token(input);
557563
if (token) {
558-
code_above = mark_paren(code_above, token);
564+
code = mark_paren(code, token);
565+
} else if (cmd) {
566+
const input = cmd + rl.line;
567+
// we match paren above the current line
568+
// but we need whole code with rl.line
569+
// so we need to ignore rl.line
570+
const token = matched_token(input);
571+
if (token) {
572+
// code_above don't include the current line that
573+
// is added after user press enter
574+
code_above = mark_paren(code_above, token);
575+
}
559576
}
560577
}
578+
// ignore the process when user press enter
579+
if (code_above && !string.match(/^[\n\r]+$/)) {
580+
// overwrite lines above the cursor this is side effect
581+
process.stdout.write(ansi_rewrite_above(code_above));
582+
}
561583
}
562-
// ignore the process when user press enter
563-
if (code_above && !string.match(/^[\n\r]+$/)) {
564-
process.stdout.write(ansi_rewrite_above(code_above));
565-
}
584+
// this always need to be executed inside rl._writeToOutput
585+
// even if nothing changes, this make sure that the input
586+
// stay intact while editing the command line
566587
rl.output.write(code);
567588
} catch(e) {
568589
console.error(e);
569590
}
570591
};
571592
process.stdin.on('keypress', (c, k) => {
572593
setTimeout(function() {
573-
rl._refreshLine(); // force refresh colors
594+
// we force triggering rl._writeToOutput function
595+
// so we have the change to syntax highlight the command line
596+
// this nees to happen on next tick so the string have time
597+
// to updated with a given key
598+
rl._refreshLine();
574599
}, 0);
575600
});
576601
bootstrap(interp).then(function() {
577602
if (supports_paste_brackets) {
603+
// this make sure that the paste brackets ANSI escape
604+
// is added to cmd so they can be processed in 'line' event
578605
process.stdin.on('keypress', (c, k) => {
579606
if (k?.name?.match(/^paste-/)) {
580607
cmd += k.sequence;
581608
}
582609
});
610+
// enable paste bracket mode by the terminal
583611
process.stdout.write('\x1b[?2004h');
584612
}
585-
const re = /\x1b\[(200|201)~/g;
586613
rl.on('line', function(line) {
587614
try {
588615
cmd += line;
589-
const code = cmd.replace(re, '');
616+
const code = cmd.replace(brackets_re, '');
590617
if (cmd.match(/\x1b\[201~$/)) {
591618
cmd = code;
592619
}

0 commit comments

Comments
 (0)