Skip to content

Commit 4875aa2

Browse files
princejwesleycjihrig
authored andcommitted
repl: Add editor mode support
```js > node > .editor // Entering editor mode (^D to finish, ^C to cancel) function test(){console.log('tested!')} test(); // ^D tested! undefined > ``` PR-URL: #7275 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Evan Lucas <[email protected]>
1 parent b20518a commit 4875aa2

File tree

4 files changed

+206
-3
lines changed

4 files changed

+206
-3
lines changed

‎doc/api/repl.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ The following special commands are supported by all REPL instances:
3838
`> .save ./file/to/save.js`
3939
*`.load` - Load a file into the current REPL session.
4040
`> .load ./file/to/load.js`
41+
*`.editor` - Enter editor mode (`<ctrl>-D` to finish, `<ctrl>-C` to cancel)
42+
43+
```js
44+
> .editor
45+
// Entering editor mode (^D to finish, ^C to cancel)
46+
functionwelcome(name){
47+
return`Hello ${name}!`;
48+
}
49+
50+
welcome('Node.js User');
51+
52+
// ^D
53+
'Hello Node.js User!'
54+
>
55+
```
4156

4257
The following key combinations in the REPL have these special effects:
4358

‎lib/repl.js‎

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ function REPLServer(prompt,
223223
self.underscoreAssigned=false;
224224
self.last=undefined;
225225
self.breakEvalOnSigint=!!breakEvalOnSigint;
226+
self.editorMode=false;
226227

227228
self._inTemplateLiteral=false;
228229

@@ -394,7 +395,12 @@ function REPLServer(prompt,
394395
// Figure out which "complete" function to use.
395396
self.completer=(typeofoptions.completer==='function')
396397
? options.completer
397-
: complete;
398+
: completer;
399+
400+
functioncompleter(text,cb){
401+
complete.call(self,text,self.editorMode
402+
? self.completeOnEditorMode(cb) : cb);
403+
}
398404

399405
Interface.call(this,{
400406
input: self.inputStream,
@@ -428,9 +434,11 @@ function REPLServer(prompt,
428434
});
429435

430436
varsawSIGINT=false;
437+
varsawCtrlD=false;
431438
self.on('SIGINT',function(){
432439
varempty=self.line.length===0;
433440
self.clearLine();
441+
self.turnOffEditorMode();
434442

435443
if(!(self.bufferedCommand&&self.bufferedCommand.length>0)&&empty){
436444
if(sawSIGINT){
@@ -454,6 +462,11 @@ function REPLServer(prompt,
454462
debug('line %j',cmd);
455463
sawSIGINT=false;
456464

465+
if(self.editorMode){
466+
self.bufferedCommand+=cmd+'\n';
467+
return;
468+
}
469+
457470
// leading whitespaces in template literals should not be trimmed.
458471
if(self._inTemplateLiteral){
459472
self._inTemplateLiteral=false;
@@ -499,7 +512,8 @@ function REPLServer(prompt,
499512

500513
// If error was SyntaxError and not JSON.parse error
501514
if(e){
502-
if(einstanceofRecoverable&&!self.lineParser.shouldFail){
515+
if(einstanceofRecoverable&&!self.lineParser.shouldFail&&
516+
!sawCtrlD){
503517
// Start buffering data like that:
504518
//{
505519
// ... x: 1
@@ -515,6 +529,7 @@ function REPLServer(prompt,
515529
// Clear buffer if no SyntaxErrors
516530
self.lineParser.reset();
517531
self.bufferedCommand='';
532+
sawCtrlD=false;
518533

519534
// If we got any output - print it (if no error)
520535
if(!e&&
@@ -555,9 +570,55 @@ function REPLServer(prompt,
555570
});
556571

557572
self.on('SIGCONT',function(){
558-
self.displayPrompt(true);
573+
if(self.editorMode){
574+
self.outputStream.write(`${self._initialPrompt}.editor\n`);
575+
self.outputStream.write(
576+
'// Entering editor mode (^D to finish, ^C to cancel)\n');
577+
self.outputStream.write(`${self.bufferedCommand}\n`);
578+
self.prompt(true);
579+
}else{
580+
self.displayPrompt(true);
581+
}
559582
});
560583

584+
// Wrap readline tty to enable editor mode
585+
constttyWrite=self._ttyWrite.bind(self);
586+
self._ttyWrite=(d,key)=>{
587+
if(!self.editorMode||!self.terminal){
588+
ttyWrite(d,key);
589+
return;
590+
}
591+
592+
// editor mode
593+
if(key.ctrl&&!key.shift){
594+
switch(key.name){
595+
case'd': // End editor mode
596+
self.turnOffEditorMode();
597+
sawCtrlD=true;
598+
ttyWrite(d,{name: 'return'});
599+
break;
600+
case'n': // Override next history item
601+
case'p': // Override previous history item
602+
break;
603+
default:
604+
ttyWrite(d,key);
605+
}
606+
}else{
607+
switch(key.name){
608+
case'up': // Override previous history item
609+
case'down': // Override next history item
610+
break;
611+
case'tab':
612+
// prevent double tab behavior
613+
self._previousKey=null;
614+
ttyWrite(d,key);
615+
break;
616+
default:
617+
ttyWrite(d,key);
618+
}
619+
}
620+
};
621+
561622
self.displayPrompt();
562623
}
563624
inherits(REPLServer,Interface);
@@ -680,6 +741,12 @@ REPLServer.prototype.setPrompt = function setPrompt(prompt){
680741
REPLServer.super_.prototype.setPrompt.call(this,prompt);
681742
};
682743

744+
REPLServer.prototype.turnOffEditorMode=function(){
745+
this.editorMode=false;
746+
this.setPrompt(this._initialPrompt);
747+
};
748+
749+
683750
// A stream to push an array into a REPL
684751
// used in REPLServer.complete
685752
functionArrayStream(){
@@ -987,6 +1054,39 @@ function complete(line, callback){
9871054
}
9881055
}
9891056

1057+
functionlongestCommonPrefix(arr=[]){
1058+
constcnt=arr.length;
1059+
if(cnt===0)return'';
1060+
if(cnt===1)returnarr[0];
1061+
1062+
constfirst=arr[0];
1063+
// complexity: O(m * n)
1064+
for(letm=0;m<first.length;m++){
1065+
constc=first[m];
1066+
for(letn=1;n<cnt;n++){
1067+
constentry=arr[n];
1068+
if(m>=entry.length||c!==entry[m]){
1069+
returnfirst.substring(0,m);
1070+
}
1071+
}
1072+
}
1073+
returnfirst;
1074+
}
1075+
1076+
REPLServer.prototype.completeOnEditorMode=(callback)=>(err,results)=>{
1077+
if(err)returncallback(err);
1078+
1079+
const[completions,completeOn='']=results;
1080+
constprefixLength=completeOn.length;
1081+
1082+
if(prefixLength===0)returncallback(null,[[],completeOn]);
1083+
1084+
constisNotEmpty=(v)=>v.length>0;
1085+
consttrimCompleteOnPrefix=(v)=>v.substring(prefixLength);
1086+
constdata=completions.filter(isNotEmpty).map(trimCompleteOnPrefix);
1087+
1088+
callback(null,[[`${completeOn}${longestCommonPrefix(data)}`],completeOn]);
1089+
};
9901090

9911091
/**
9921092
* Used to parse and execute the Node REPL commands.
@@ -1189,6 +1289,17 @@ function defineDefaultCommands(repl){
11891289
this.displayPrompt();
11901290
}
11911291
});
1292+
1293+
repl.defineCommand('editor',{
1294+
help: 'Entering editor mode (^D to finish, ^C to cancel)',
1295+
action(){
1296+
if(!this.terminal)return;
1297+
this.editorMode=true;
1298+
REPLServer.super_.prototype.setPrompt.call(this,'');
1299+
this.outputStream.write(
1300+
'// Entering editor mode (^D to finish, ^C to cancel)\n');
1301+
}
1302+
});
11921303
}
11931304

11941305
functionregexpEscape(s){

‎test/parallel/test-repl-.editor.js‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use strict';
2+
3+
constcommon=require('../common');
4+
constassert=require('assert');
5+
constrepl=require('repl');
6+
7+
// \u001b[1G - Moves the cursor to 1st column
8+
// \u001b[0J - Clear screen
9+
// \u001b[3G - Moves the cursor to 3rd column
10+
constterminalCode='\u001b[1G\u001b[0J> \u001b[3G';
11+
12+
functionrun(input,output,event){
13+
conststream=newcommon.ArrayStream();
14+
letfound='';
15+
16+
stream.write=(msg)=>found+=msg.replace('\r','');
17+
18+
constexpected=`${terminalCode}.editor\n`+
19+
'// Entering editor mode (^D to finish, ^C to cancel)\n'+
20+
`${input}${output}\n${terminalCode}`;
21+
22+
constreplServer=repl.start({
23+
prompt: '> ',
24+
terminal: true,
25+
input: stream,
26+
output: stream,
27+
useColors: false
28+
});
29+
30+
stream.emit('data','.editor\n');
31+
stream.emit('data',input);
32+
replServer.write('',event);
33+
replServer.close();
34+
assert.strictEqual(found,expected);
35+
}
36+
37+
consttests=[
38+
{
39+
input: '',
40+
output: '\n(To exit, press ^C again or type .exit)',
41+
event: {ctrl: true,name: 'c'}
42+
},
43+
{
44+
input: 'var i = 1;',
45+
output: '',
46+
event: {ctrl: true,name: 'c'}
47+
},
48+
{
49+
input: 'var i = 1;\ni + 3',
50+
output: '\n4',
51+
event: {ctrl: true,name: 'd'}
52+
}
53+
];
54+
55+
tests.forEach(({input, output, event})=>run(input,output,event));

‎test/parallel/test-repl-tab-complete.js‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,3 +348,25 @@ testCustomCompleterAsyncMode.complete('a', common.mustCall((error, data) =>{
348348
'a'
349349
]);
350350
}));
351+
352+
// tab completion in editor mode
353+
consteditorStream=newcommon.ArrayStream();
354+
consteditor=repl.start({
355+
stream: editorStream,
356+
terminal: true,
357+
useColors: false
358+
});
359+
360+
editorStream.run(['.clear']);
361+
editorStream.run(['.editor']);
362+
363+
editor.completer('co',common.mustCall((error,data)=>{
364+
assert.deepStrictEqual(data,[['con'],'co']);
365+
}));
366+
367+
editorStream.run(['.clear']);
368+
editorStream.run(['.editor']);
369+
370+
editor.completer('var log = console.l',common.mustCall((error,data)=>{
371+
assert.deepStrictEqual(data,[['console.log'],'console.l']);
372+
}));

0 commit comments

Comments
(0)