@@ -28,6 +28,10 @@ const {
28
28
moveCursor,
29
29
} = require ( 'readline' ) ;
30
30
31
+ const {
32
+ commonPrefix
33
+ } = require ( 'internal/readline/utils' ) ;
34
+
31
35
const { inspect } = require ( 'util' ) ;
32
36
33
37
const debug = require ( 'internal/util/debuglog' ) . debuglog ( 'repl' ) ;
@@ -119,24 +123,103 @@ function isRecoverableError(e, code) {
119
123
function setupPreview ( repl , contextSymbol , bufferSymbol , active ) {
120
124
// Simple terminals can't handle previews.
121
125
if ( process . env . TERM === 'dumb' || ! active ) {
122
- return { showInputPreview ( ) { } , clearPreview ( ) { } } ;
126
+ return { showPreview ( ) { } , clearPreview ( ) { } } ;
123
127
}
124
128
125
- let preview = null ;
126
- let lastPreview = '' ;
129
+ let inputPreview = null ;
130
+ let lastInputPreview = '' ;
131
+
132
+ let previewCompletionCounter = 0 ;
133
+ let completionPreview = null ;
127
134
128
135
const clearPreview = ( ) => {
129
- if ( preview !== null ) {
136
+ if ( inputPreview !== null ) {
130
137
moveCursor ( repl . output , 0 , 1 ) ;
131
138
clearLine ( repl . output ) ;
132
139
moveCursor ( repl . output , 0 , - 1 ) ;
133
- lastPreview = preview ;
134
- preview = null ;
140
+ lastInputPreview = inputPreview ;
141
+ inputPreview = null ;
142
+ }
143
+ if ( completionPreview !== null ) {
144
+ // Prevent cursor moves if not necessary!
145
+ const move = repl . line . length !== repl . cursor ;
146
+ if ( move ) {
147
+ cursorTo ( repl . output , repl . _prompt . length + repl . line . length ) ;
148
+ }
149
+ clearLine ( repl . output , 1 ) ;
150
+ if ( move ) {
151
+ cursorTo ( repl . output , repl . _prompt . length + repl . cursor ) ;
152
+ }
153
+ completionPreview = null ;
135
154
}
136
155
} ;
137
156
157
+ function showCompletionPreview ( line , insertPreview ) {
158
+ previewCompletionCounter ++ ;
159
+
160
+ const count = previewCompletionCounter ;
161
+
162
+ repl . completer ( line , ( error , data ) => {
163
+ // Tab completion might be async and the result might already be outdated.
164
+ if ( count !== previewCompletionCounter ) {
165
+ return ;
166
+ }
167
+
168
+ if ( error ) {
169
+ debug ( 'Error while generating completion preview' , error ) ;
170
+ return ;
171
+ }
172
+
173
+ // Result and the text that was completed.
174
+ const [ rawCompletions , completeOn ] = data ;
175
+
176
+ if ( ! rawCompletions || rawCompletions . length === 0 ) {
177
+ return ;
178
+ }
179
+
180
+ // If there is a common prefix to all matches, then apply that portion.
181
+ const completions = rawCompletions . filter ( ( e ) => e ) ;
182
+ const prefix = commonPrefix ( completions ) ;
183
+
184
+ // No common prefix found.
185
+ if ( prefix . length <= completeOn . length ) {
186
+ return ;
187
+ }
188
+
189
+ const suffix = prefix . slice ( completeOn . length ) ;
190
+
191
+ const totalLength = repl . line . length +
192
+ repl . _prompt . length +
193
+ suffix . length +
194
+ ( repl . useColors ? 0 : 4 ) ;
195
+
196
+ // TODO(BridgeAR): Fix me. This should not be necessary. See similar
197
+ // comment in `showPreview()`.
198
+ if ( totalLength > repl . columns ) {
199
+ return ;
200
+ }
201
+
202
+ if ( insertPreview ) {
203
+ repl . _insertString ( suffix ) ;
204
+ return ;
205
+ }
206
+
207
+ completionPreview = suffix ;
208
+
209
+ const result = repl . useColors ?
210
+ `\u001b[90m${ suffix } \u001b[39m` :
211
+ ` // ${ suffix } ` ;
212
+
213
+ if ( repl . line . length !== repl . cursor ) {
214
+ cursorTo ( repl . output , repl . _prompt . length + repl . line . length ) ;
215
+ }
216
+ repl . output . write ( result ) ;
217
+ cursorTo ( repl . output , repl . _prompt . length + repl . cursor ) ;
218
+ } ) ;
219
+ }
220
+
138
221
// This returns a code preview for arbitrary input code.
139
- function getPreviewInput ( input , callback ) {
222
+ function getInputPreview ( input , callback ) {
140
223
// For similar reasons as `defaultEval`, wrap expressions starting with a
141
224
// curly brace with parenthesis.
142
225
if ( input . startsWith ( '{' ) && ! input . endsWith ( ';' ) ) {
@@ -184,23 +267,41 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) {
184
267
} , ( ) => callback ( new ERR_INSPECTOR_NOT_AVAILABLE ( ) ) ) ;
185
268
}
186
269
187
- const showInputPreview = ( ) => {
270
+ const showPreview = ( ) => {
188
271
// Prevent duplicated previews after a refresh.
189
- if ( preview !== null ) {
272
+ if ( inputPreview !== null ) {
190
273
return ;
191
274
}
192
275
193
276
const line = repl . line . trim ( ) ;
194
277
195
- // Do not preview if the command is buffered or if the line is empty.
196
- if ( repl [ bufferSymbol ] || line === '' ) {
278
+ // Do not preview in case the line only contains whitespace.
279
+ if ( line === '' ) {
280
+ return ;
281
+ }
282
+
283
+ // Add the autocompletion preview.
284
+ // TODO(BridgeAR): Trigger the input preview after the completion preview.
285
+ // That way it's possible to trigger the input prefix including the
286
+ // potential completion suffix. To do so, we also have to change the
287
+ // behavior of `enter` and `escape`:
288
+ // Enter should automatically add the suffix to the current line as long as
289
+ // escape was not pressed. We might even remove the preview in case any
290
+ // cursor movement is triggered.
291
+ if ( typeof repl . completer === 'function' ) {
292
+ const insertPreview = false ;
293
+ showCompletionPreview ( repl . line , insertPreview ) ;
294
+ }
295
+
296
+ // Do not preview if the command is buffered.
297
+ if ( repl [ bufferSymbol ] ) {
197
298
return ;
198
299
}
199
300
200
- getPreviewInput ( line , ( error , inspected ) => {
301
+ getInputPreview ( line , ( error , inspected ) => {
201
302
// Ignore the output if the value is identical to the current line and the
202
303
// former preview is not identical to this preview.
203
- if ( ( line === inspected && lastPreview !== inspected ) ||
304
+ if ( ( line === inspected && lastInputPreview !== inspected ) ||
204
305
inspected === null ) {
205
306
return ;
206
307
}
@@ -215,7 +316,7 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) {
215
316
return ;
216
317
}
217
318
218
- preview = inspected ;
319
+ inputPreview = inspected ;
219
320
220
321
// Limit the output to maximum 250 characters. Otherwise it becomes a)
221
322
// difficult to read and b) non terminal REPLs would visualize the whole
@@ -235,21 +336,50 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) {
235
336
236
337
repl . output . write ( `\n${ result } ` ) ;
237
338
moveCursor ( repl . output , 0 , - 1 ) ;
238
- cursorTo ( repl . output , repl . cursor + repl . _prompt . length ) ;
339
+ cursorTo ( repl . output , repl . _prompt . length + repl . cursor ) ;
239
340
} ) ;
240
341
} ;
241
342
343
+ // -------------------------------------------------------------------------//
344
+ // Replace multiple interface functions. This is required to fully support //
345
+ // previews without changing readlines behavior. //
346
+ // -------------------------------------------------------------------------//
347
+
242
348
// Refresh prints the whole screen again and the preview will be removed
243
349
// during that procedure. Print the preview again. This also makes sure
244
350
// the preview is always correct after resizing the terminal window.
245
- const tmpRefresh = repl . _refreshLine . bind ( repl ) ;
351
+ const originalRefresh = repl . _refreshLine . bind ( repl ) ;
246
352
repl . _refreshLine = ( ) => {
247
- preview = null ;
248
- tmpRefresh ( ) ;
249
- showInputPreview ( ) ;
353
+ inputPreview = null ;
354
+ originalRefresh ( ) ;
355
+ showPreview ( ) ;
356
+ } ;
357
+
358
+ let insertCompletionPreview = true ;
359
+ // Insert the longest common suffix of the current input in case the user
360
+ // moves to the right while already being at the current input end.
361
+ const originalMoveCursor = repl . _moveCursor . bind ( repl ) ;
362
+ repl . _moveCursor = ( dx ) => {
363
+ const currentCursor = repl . cursor ;
364
+ originalMoveCursor ( dx ) ;
365
+ if ( currentCursor + dx > repl . line . length &&
366
+ typeof repl . completer === 'function' &&
367
+ insertCompletionPreview ) {
368
+ const insertPreview = true ;
369
+ showCompletionPreview ( repl . line , insertPreview ) ;
370
+ }
371
+ } ;
372
+
373
+ // This is the only function that interferes with the completion insertion.
374
+ // Monkey patch it to prevent inserting the completion when it shouldn't be.
375
+ const originalClearLine = repl . clearLine . bind ( repl ) ;
376
+ repl . clearLine = ( ) => {
377
+ insertCompletionPreview = false ;
378
+ originalClearLine ( ) ;
379
+ insertCompletionPreview = true ;
250
380
} ;
251
381
252
- return { showInputPreview , clearPreview } ;
382
+ return { showPreview , clearPreview } ;
253
383
}
254
384
255
385
module . exports = {
0 commit comments