@@ -28,6 +28,10 @@ const {
2828 moveCursor,
2929} = require ( 'readline' ) ;
3030
31+ const {
32+ commonPrefix
33+ } = require ( 'internal/readline/utils' ) ;
34+
3135const { inspect } = require ( 'util' ) ;
3236
3337const debug = require ( 'internal/util/debuglog' ) . debuglog ( 'repl' ) ;
@@ -119,24 +123,103 @@ function isRecoverableError(e, code) {
119123function setupPreview ( repl , contextSymbol , bufferSymbol , active ) {
120124 // Simple terminals can't handle previews.
121125 if ( process . env . TERM === 'dumb' || ! active ) {
122- return { showInputPreview ( ) { } , clearPreview ( ) { } } ;
126+ return { showPreview ( ) { } , clearPreview ( ) { } } ;
123127 }
124128
125- let preview = null ;
126- let lastPreview = '' ;
129+ let inputPreview = null ;
130+ let lastInputPreview = '' ;
131+
132+ let previewCompletionCounter = 0 ;
133+ let completionPreview = null ;
127134
128135 const clearPreview = ( ) => {
129- if ( preview !== null ) {
136+ if ( inputPreview !== null ) {
130137 moveCursor ( repl . output , 0 , 1 ) ;
131138 clearLine ( repl . output ) ;
132139 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 ;
135154 }
136155 } ;
137156
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+
138221 // This returns a code preview for arbitrary input code.
139- function getPreviewInput ( input , callback ) {
222+ function getInputPreview ( input , callback ) {
140223 // For similar reasons as `defaultEval`, wrap expressions starting with a
141224 // curly brace with parenthesis.
142225 if ( input . startsWith ( '{' ) && ! input . endsWith ( ';' ) ) {
@@ -184,23 +267,41 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) {
184267 } , ( ) => callback ( new ERR_INSPECTOR_NOT_AVAILABLE ( ) ) ) ;
185268 }
186269
187- const showInputPreview = ( ) => {
270+ const showPreview = ( ) => {
188271 // Prevent duplicated previews after a refresh.
189- if ( preview !== null ) {
272+ if ( inputPreview !== null ) {
190273 return ;
191274 }
192275
193276 const line = repl . line . trim ( ) ;
194277
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 ] ) {
197298 return ;
198299 }
199300
200- getPreviewInput ( line , ( error , inspected ) => {
301+ getInputPreview ( line , ( error , inspected ) => {
201302 // Ignore the output if the value is identical to the current line and the
202303 // former preview is not identical to this preview.
203- if ( ( line === inspected && lastPreview !== inspected ) ||
304+ if ( ( line === inspected && lastInputPreview !== inspected ) ||
204305 inspected === null ) {
205306 return ;
206307 }
@@ -215,7 +316,7 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) {
215316 return ;
216317 }
217318
218- preview = inspected ;
319+ inputPreview = inspected ;
219320
220321 // Limit the output to maximum 250 characters. Otherwise it becomes a)
221322 // difficult to read and b) non terminal REPLs would visualize the whole
@@ -235,21 +336,50 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) {
235336
236337 repl . output . write ( `\n${ result } ` ) ;
237338 moveCursor ( repl . output , 0 , - 1 ) ;
238- cursorTo ( repl . output , repl . cursor + repl . _prompt . length ) ;
339+ cursorTo ( repl . output , repl . _prompt . length + repl . cursor ) ;
239340 } ) ;
240341 } ;
241342
343+ // -------------------------------------------------------------------------//
344+ // Replace multiple interface functions. This is required to fully support //
345+ // previews without changing readlines behavior. //
346+ // -------------------------------------------------------------------------//
347+
242348 // Refresh prints the whole screen again and the preview will be removed
243349 // during that procedure. Print the preview again. This also makes sure
244350 // the preview is always correct after resizing the terminal window.
245- const tmpRefresh = repl . _refreshLine . bind ( repl ) ;
351+ const originalRefresh = repl . _refreshLine . bind ( repl ) ;
246352 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 ;
250380 } ;
251381
252- return { showInputPreview , clearPreview } ;
382+ return { showPreview , clearPreview } ;
253383}
254384
255385module . exports = {
0 commit comments