@@ -86,6 +86,13 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
86
86
/** The WebSocket used to talk to the server */
87
87
private _socket : WebSocket ;
88
88
89
+ /** The number of columns in the terminal */
90
+ private _cols : number ;
91
+
92
+ /** The `RegExp` used to strip ANSI color escape codes from a string */
93
+ // eslint-disable-next-line no-control-regex
94
+ private _colorsRegex = / \x1b [ ^ m ] * ?m / g;
95
+
89
96
constructor ( private readonly _api : AtelierAPI ) { }
90
97
91
98
/** Hide the cursor, write `data` to the terminal, then show the cursor again. */
@@ -157,7 +164,45 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
157
164
return result ;
158
165
}
159
166
160
- open ( ) : void {
167
+ /**
168
+ * Move the cursor based on user changes (typing/deleting characters, arrow keys) or
169
+ * changes to the width of the terminal window
170
+ */
171
+ private _moveCursor ( cursorColDelta = 0 , colsDelta = 0 ) : void {
172
+ if ( cursorColDelta == 0 && colsDelta == 0 ) return ;
173
+ // Calculate the row/column number of the current position
174
+ const currCol = this . _cursorCol % this . _cols ;
175
+ const currRow = ( this . _cursorCol - currCol ) / this . _cols ;
176
+ // Make the adjustment
177
+ if ( cursorColDelta != 0 ) {
178
+ this . _cursorCol += cursorColDelta ;
179
+ } else {
180
+ this . _cols += colsDelta ;
181
+ }
182
+ // Calculate the row/column number of the new position
183
+ const newCol = this . _cursorCol % this . _cols ;
184
+ const newRow = ( this . _cursorCol - newCol ) / this . _cols ;
185
+ // Move the cursor
186
+ const rowDelta = newRow - currRow ;
187
+ const colDelta = newCol - currCol ;
188
+ const rowStr = rowDelta ? ( rowDelta > 0 ? `\x1b[${ rowDelta } B` : `\x1b[${ Math . abs ( rowDelta ) } A` ) : "" ;
189
+ const colStr = colDelta ? ( colDelta > 0 ? `\x1b[${ colDelta } C` : `\x1b[${ Math . abs ( colDelta ) } D` ) : "" ;
190
+ this . _hideCursorWrite ( `${ rowStr } ${ colStr } ` ) ;
191
+ }
192
+
193
+ /**
194
+ * Move the cursor to the last line of the input (prompt or read)
195
+ * so any output doesn't overwrite the end of the input
196
+ */
197
+ private _moveCursorToLastLine ( ) : void {
198
+ const currRow = ( this . _cursorCol - ( this . _cursorCol % this . _cols ) ) / this . _cols ;
199
+ const newRow = Math . ceil ( ( this . _margin + this . _input . split ( "\r\n" ) . pop ( ) . length + 1 ) / this . _cols ) - 1 ;
200
+ const rowDelta = newRow - currRow ;
201
+ if ( rowDelta ) this . _hideCursorWrite ( `\x1b[${ rowDelta } B` ) ;
202
+ }
203
+
204
+ open ( initialDimensions ?: vscode . TerminalDimensions ) : void {
205
+ this . _cols = initialDimensions ?. columns ?? 100000 ;
161
206
try {
162
207
// Open the WebSocket
163
208
this . _socket = new WebSocket ( this . _api . terminalUrl ( ) , {
@@ -244,7 +289,7 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
244
289
message . text
245
290
} \x1b]633;B\x07`
246
291
) ;
247
- this . _margin = this . _cursorCol = message . text . length ;
292
+ this . _margin = this . _cursorCol = message . text . replace ( this . _colorsRegex , "" ) . length ;
248
293
this . _prompt = message . text ;
249
294
this . _promptExitCode = ";0" ;
250
295
}
@@ -264,17 +309,18 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
264
309
break ;
265
310
case "color" : {
266
311
// Replace the input with the syntax colored text, keeping the cursor at the same spot
267
- const lines = message . text . split ( "\r\n" ) . length ;
268
- if ( lines > 1 ) {
269
- this . _hideCursorWrite (
270
- `\x1b7\x1b[${ lines - 1 } A\r\x1b[0J${ this . _prompt } ${ message . text . replace (
271
- / \r \n / g,
272
- `\r\n${ this . _multiLinePrompt } `
273
- ) } \x1b8`
274
- ) ;
275
- } else {
276
- this . _hideCursorWrite ( `\x1b7\x1b[2K\r${ this . _prompt } ${ message . text } \x1b8` ) ;
312
+ let cursorLine = Math . ceil ( ( this . _cursorCol + 1 ) / this . _cols ) - 1 ;
313
+ if ( message . text . includes ( "\r\n" ) ) {
314
+ const lines = message . text . replace ( this . _colorsRegex , "" ) . split ( "\r\n" ) ;
315
+ lines . pop ( ) ;
316
+ cursorLine += lines . reduce ( ( sum , line ) => sum + Math . ceil ( ( line . length + 1 ) / this . _cols ) , 0 ) ;
277
317
}
318
+ this . _hideCursorWrite (
319
+ `\x1b7${ cursorLine > 0 ? `\x1b[${ cursorLine } A` : "" } \r\x1b[0J${ this . _prompt } ${ message . text . replace (
320
+ / \r \n / g,
321
+ `\r\n${ this . _multiLinePrompt } `
322
+ ) } \x1b8`
323
+ ) ;
278
324
break ;
279
325
}
280
326
}
@@ -326,6 +372,8 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
326
372
// Reset first line tracker
327
373
this . _firstOutputLineSincePrompt = false ;
328
374
}
375
+ // Move cursor to the last line of the input
376
+ this . _moveCursorToLastLine ( ) ;
329
377
330
378
// Send the input to the server for processing
331
379
this . _socket . send ( JSON . stringify ( { type : this . _state , input : this . _input } ) ) ;
@@ -351,12 +399,12 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
351
399
return ;
352
400
}
353
401
const inputArr = this . _input . split ( "\r\n" ) ;
402
+ const trailingText = inputArr [ inputArr . length - 1 ] . slice ( this . _cursorCol - this . _margin ) ;
354
403
inputArr [ inputArr . length - 1 ] =
355
- inputArr [ inputArr . length - 1 ] . slice ( 0 , this . _cursorCol - this . _margin - 1 ) +
356
- inputArr [ inputArr . length - 1 ] . slice ( this . _cursorCol - this . _margin ) ;
404
+ inputArr [ inputArr . length - 1 ] . slice ( 0 , this . _cursorCol - this . _margin - 1 ) + trailingText ;
357
405
this . _input = inputArr . join ( "\r\n" ) ;
358
- this . _cursorCol -- ;
359
- this . _hideCursorWrite ( actions . cursorBack + actions . deleteChar ) ;
406
+ this . _moveCursor ( - 1 ) ;
407
+ this . _hideCursorWrite ( `\x1b7\x1b[0J ${ trailingText } \x1b8` ) ;
360
408
if ( this . _input != "" && this . _state == "prompt" && this . _syntaxColoringEnabled ( ) ) {
361
409
// Syntax color input
362
410
this . _socket . send ( JSON . stringify ( { type : "color" , input : this . _input } ) ) ;
@@ -371,11 +419,11 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
371
419
}
372
420
const inputArr = this . _input . split ( "\r\n" ) ;
373
421
if ( this . _margin + inputArr [ inputArr . length - 1 ] . length - this . _cursorCol > 0 ) {
422
+ const trailingText = inputArr [ inputArr . length - 1 ] . slice ( this . _cursorCol - this . _margin + 1 ) ;
374
423
inputArr [ inputArr . length - 1 ] =
375
- inputArr [ inputArr . length - 1 ] . slice ( 0 , this . _cursorCol - this . _margin ) +
376
- inputArr [ inputArr . length - 1 ] . slice ( this . _cursorCol - this . _margin + 1 ) ;
424
+ inputArr [ inputArr . length - 1 ] . slice ( 0 , this . _cursorCol - this . _margin ) + trailingText ;
377
425
this . _input = inputArr . join ( "\r\n" ) ;
378
- this . _hideCursorWrite ( actions . deleteChar ) ;
426
+ this . _hideCursorWrite ( `\x1b7\x1b[0J ${ trailingText } \x1b8` ) ;
379
427
if ( this . _input != "" && this . _state == "prompt" && this . _syntaxColoringEnabled ( ) ) {
380
428
// Syntax color input
381
429
this . _socket . send ( JSON . stringify ( { type : "color" , input : this . _input } ) ) ;
@@ -401,7 +449,6 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
401
449
// Scroll back one more input
402
450
this . _historyIdx -- ;
403
451
}
404
- const oldInput = this . _input ;
405
452
if ( this . _historyIdx >= 0 ) {
406
453
this . _input = this . _history [ this . _historyIdx ] ;
407
454
} else if ( this . _historyIdx == - 1 ) {
@@ -411,8 +458,10 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
411
458
// If we hit the end, leave the input blank
412
459
this . _input = "" ;
413
460
}
461
+ // Move cursor to start of input, clear everything, then write new input
462
+ this . _moveCursor ( this . _margin - this . _cursorCol ) ;
463
+ this . _hideCursorWrite ( `\x1b[0J${ this . _input } ` ) ;
414
464
this . _cursorCol = this . _margin + this . _input . length ;
415
- this . _hideCursorWrite ( `${ oldInput . length ? `\x1b[${ oldInput . length } D\x1b[0K` : "" } ${ this . _input } ` ) ;
416
465
if ( this . _input != "" && this . _syntaxColoringEnabled ( ) ) {
417
466
// Syntax color input
418
467
this . _socket . send ( JSON . stringify ( { type : "color" , input : this . _input } ) ) ;
@@ -436,15 +485,16 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
436
485
} else {
437
486
this . _historyIdx ++ ;
438
487
}
439
- const oldInput = this . _input ;
440
488
if ( this . _historyIdx != - 1 ) {
441
489
this . _input = this . _history [ this . _historyIdx ] ;
442
490
} else {
443
491
// If we hit the beginning, leave the input blank
444
492
this . _input = "" ;
445
493
}
494
+ // Move cursor to start of input, clear everything, then write new input
495
+ this . _moveCursor ( this . _margin - this . _cursorCol ) ;
496
+ this . _hideCursorWrite ( `\x1b[0J${ this . _input } ` ) ;
446
497
this . _cursorCol = this . _margin + this . _input . length ;
447
- this . _hideCursorWrite ( `${ oldInput . length ? `\x1b[${ oldInput . length } D\x1b[0K` : "" } ${ this . _input } ` ) ;
448
498
if ( this . _input != "" && this . _syntaxColoringEnabled ( ) ) {
449
499
// Syntax color input
450
500
this . _socket . send ( JSON . stringify ( { type : "color" , input : this . _input } ) ) ;
@@ -457,9 +507,14 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
457
507
return ;
458
508
}
459
509
if ( this . _cursorCol > this . _margin ) {
460
- // Move the cursor back one column
510
+ if ( this . _cursorCol % this . _cols == 0 ) {
511
+ // Move the cursor to the end of the previous line
512
+ this . _hideCursorWrite ( `${ actions . cursorUp } \x1b[${ this . _cols } G` ) ;
513
+ } else {
514
+ // Move the cursor back one column
515
+ this . _hideCursorWrite ( actions . cursorBack ) ;
516
+ }
461
517
this . _cursorCol -- ;
462
- this . _hideCursorWrite ( actions . cursorBack ) ;
463
518
}
464
519
return ;
465
520
}
@@ -468,10 +523,15 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
468
523
// User can't move cursor
469
524
return ;
470
525
}
471
- if ( this . _cursorCol < this . _margin + this . _input . length ) {
472
- // Move the cursor forward one column
526
+ if ( this . _cursorCol < this . _margin + this . _input . split ( "\r\n" ) . pop ( ) . length ) {
473
527
this . _cursorCol ++ ;
474
- this . _hideCursorWrite ( actions . cursorForward ) ;
528
+ if ( this . _cursorCol % this . _cols == 0 ) {
529
+ // Move the cursor to the beginning of the next line
530
+ this . _hideCursorWrite ( "\x1b[1E" ) ;
531
+ } else {
532
+ // Move the cursor forward one column
533
+ this . _hideCursorWrite ( actions . cursorForward ) ;
534
+ }
475
535
}
476
536
return ;
477
537
}
@@ -490,31 +550,31 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
490
550
case keys . home :
491
551
case keys . ctrlA : {
492
552
if ( this . _state == "prompt" && this . _cursorCol - this . _margin > 0 ) {
493
- // Move the cursor to the beginning of the line
494
- this . _hideCursorWrite ( `\x1b[${ this . _cursorCol - this . _margin } D` ) ;
495
- this . _cursorCol = this . _margin ;
553
+ // Move the cursor to the beginning of the input
554
+ this . _moveCursor ( this . _margin - this . _cursorCol ) ;
496
555
}
497
556
return ;
498
557
}
499
558
case keys . end :
500
559
case keys . ctrlE : {
501
560
if ( this . _state == "prompt" ) {
502
- // Move the cursor to the end of the line
503
- const inputArr = this . _input . split ( "\r\n" ) ;
504
- if ( this . _margin + inputArr [ inputArr . length - 1 ] . length - this . _cursorCol > 0 ) {
505
- this . _hideCursorWrite ( `\x1b[${ this . _margin + inputArr [ inputArr . length - 1 ] . length - this . _cursorCol } C` ) ;
506
- this . _cursorCol = this . _margin + inputArr [ inputArr . length - 1 ] . length ;
561
+ // Move the cursor to the end of the input
562
+ const lineLength = this . _input . split ( "\r\n" ) . pop ( ) . length ;
563
+ if ( lineLength > this . _cursorCol ) {
564
+ this . _moveCursor ( lineLength - this . _cursorCol ) ;
507
565
}
508
566
}
509
567
return ;
510
568
}
511
569
case keys . ctrlU : {
512
570
if ( this . _state == "prompt" ) {
513
- // Erase the line if the cursor is at the end
571
+ // Erase the input if the cursor is at the end of it
514
572
const inputArr = this . _input . split ( "\r\n" ) ;
515
573
if ( this . _cursorCol == this . _margin + inputArr [ inputArr . length - 1 ] . length ) {
516
- this . _hideCursorWrite ( `\x1b[2K\r${ inputArr . length > 1 ? this . _multiLinePrompt : this . _prompt } ` ) ;
517
- this . _cursorCol = this . _margin ;
574
+ // Move the cursor to the beginning of the input
575
+ this . _moveCursor ( this . _margin - this . _cursorCol ) ;
576
+ // Erase everyhting to the right of the cursor
577
+ this . _hideCursorWrite ( "\x1b[0J" ) ;
518
578
inputArr [ inputArr . length - 1 ] = "" ;
519
579
this . _input = inputArr . join ( "\r\n" ) ;
520
580
if ( this . _input != "" && this . _syntaxColoringEnabled ( ) ) {
@@ -545,21 +605,66 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
545
605
// Replace all single \r with \r\n (prompt) or space (read)
546
606
char = char . replace ( / \r / g, this . _state == "prompt" ? "\r\n" : " " ) ;
547
607
const inputArr = this . _input . split ( "\r\n" ) ;
608
+ let eraseAfterCursor = "" ,
609
+ trailingText = "" ;
548
610
if ( this . _cursorCol < this . _margin + inputArr [ inputArr . length - 1 ] . length ) {
549
611
// Insert the new char(s)
612
+ trailingText = inputArr [ inputArr . length - 1 ] . slice ( this . _cursorCol - this . _margin ) ;
550
613
inputArr [ inputArr . length - 1 ] = `${ inputArr [ inputArr . length - 1 ] . slice (
551
614
0 ,
552
615
this . _cursorCol - this . _margin
553
- ) } ${ char } ${ inputArr [ inputArr . length - 1 ] . slice ( this . _cursorCol - this . _margin ) } `;
616
+ ) } ${ char } ${ trailingText } `;
554
617
this . _input = inputArr . join ( "\r\n" ) ;
555
- this . _cursorCol += char . length ;
556
- this . _hideCursorWrite ( `\x1b[4h${ char . replace ( / \r \n / g, `\r\n${ this . _multiLinePrompt } ` ) } \x1b[4l` ) ;
618
+ eraseAfterCursor = "\x1b[0J" ;
557
619
} else {
558
620
// Append the new char(s)
559
621
this . _input += char ;
622
+ }
623
+ const currCol = this . _cursorCol % this . _cols ;
624
+ const currRow = ( this . _cursorCol - currCol ) / this . _cols ;
625
+ const originalCol = this . _cursorCol ;
626
+ let newRow : number ;
627
+ if ( char . includes ( "\r\n" ) ) {
628
+ char = char . replace ( / \r \n / g, `\r\n${ this . _multiLinePrompt } ` ) ;
629
+ this . _margin = this . _multiLinePrompt . length ;
630
+ const charLines = char . split ( "\r\n" ) ;
631
+ newRow =
632
+ charLines . reduce (
633
+ ( sum , line , i ) => sum + Math . ceil ( ( ( i == 0 ? this . _cursorCol : 0 ) + line . length + 1 ) / this . _cols ) ,
634
+ 0
635
+ ) - 1 ;
636
+ this . _cursorCol = charLines [ charLines . length - 1 ] . length ;
637
+ } else {
638
+ newRow = Math . ceil ( ( this . _cursorCol + char . length + 1 ) / this . _cols ) - 1 ;
560
639
this . _cursorCol += char . length ;
561
- this . _hideCursorWrite ( char . replace ( / \r \n / g, `\r\n${ this . _multiLinePrompt } ` ) ) ;
562
640
}
641
+ const rowDelta = newRow - currRow ;
642
+ const colDelta = ( this . _cursorCol % this . _cols ) - currCol ;
643
+ const rowStr = rowDelta ? ( rowDelta > 0 ? `\x1b[${ rowDelta } B` : `\x1b[${ Math . abs ( rowDelta ) } A` ) : "" ;
644
+ const colStr = colDelta ? ( colDelta > 0 ? `\x1b[${ colDelta } C` : `\x1b[${ Math . abs ( colDelta ) } D` ) : "" ;
645
+ char += trailingText ;
646
+ const spaceOnCurrentLine = this . _cols - ( originalCol % this . _cols ) ;
647
+ if ( this . _state == "read" && char . length >= spaceOnCurrentLine ) {
648
+ // There's no auto-line wrapping when in read mode, so we must move the cursor manually
649
+ // Extract all the characters that fit on the cursor's line
650
+ const firstLine = char . slice ( 0 , spaceOnCurrentLine ) ;
651
+ const otherLines = char . slice ( spaceOnCurrentLine ) ;
652
+ const lines : string [ ] = [ ] ;
653
+ if ( otherLines . length ) {
654
+ // Split the rest into an array of lines that fit in the viewport
655
+ for ( let line = 0 , i = 0 ; line < Math . ceil ( otherLines . length / this . _cols ) ; line ++ , i += this . _cols ) {
656
+ lines [ line ] = otherLines . slice ( i , i + this . _cols ) ;
657
+ }
658
+ } else {
659
+ // Add a blank "line" to move the cursor to the next viewport row
660
+ lines . push ( "" ) ;
661
+ }
662
+ // Join the lines with the cursor escape code
663
+ lines . unshift ( firstLine ) ;
664
+ char = lines . join ( "\r\n" ) ;
665
+ }
666
+ // Save the cursor position, write the text, restore the cursor position, then move the cursor manually
667
+ this . _hideCursorWrite ( `\x1b7${ eraseAfterCursor } ${ char } \x1b8${ rowStr } ${ colStr } ` ) ;
563
668
if ( submit ) {
564
669
if ( this . _state == "prompt" ) {
565
670
// Reset historyIdx
@@ -579,6 +684,8 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
579
684
// Reset first line tracker
580
685
this . _firstOutputLineSincePrompt = false ;
581
686
}
687
+ // Move cursor to the last line of the input
688
+ this . _moveCursorToLastLine ( ) ;
582
689
583
690
// Send the input to the server for processing
584
691
this . _socket . send ( JSON . stringify ( { type : this . _state , input : this . _input } ) ) ;
@@ -597,6 +704,33 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
597
704
}
598
705
}
599
706
}
707
+
708
+ setDimensions ( dimensions : vscode . TerminalDimensions ) : void {
709
+ if ( this . _state != "eval" && this . _input != "" ) {
710
+ // Move the cursor to the correct new position
711
+ this . _moveCursor ( undefined , dimensions . columns - this . _cols ) ;
712
+ // Save the cursor position, move the cursor to just after the margin,
713
+ // clear the screen from that point, write the input, then restore the cursor
714
+ let cursorLine = Math . ceil ( ( this . _cursorCol + 1 ) / this . _cols ) - 1 ;
715
+ if ( this . _input . includes ( "\r\n" ) ) {
716
+ const lines = this . _input . split ( "\r\n" ) ;
717
+ lines . pop ( ) ;
718
+ cursorLine += lines . reduce ( ( sum , line ) => sum + Math . ceil ( ( line . length + 1 ) / this . _cols ) , 0 ) ;
719
+ }
720
+ this . _hideCursorWrite (
721
+ `\x1b7${ cursorLine > 0 ? `\x1b[${ cursorLine } A` : "" } \r\x1b[${ this . _margin } C\x1b[0J${ this . _input . replace (
722
+ / \r \n / g,
723
+ `\r\n${ this . _multiLinePrompt } `
724
+ ) } \x1b8`
725
+ ) ;
726
+ if ( this . _state == "prompt" && this . _syntaxColoringEnabled ( ) ) {
727
+ // Syntax color input
728
+ this . _socket . send ( JSON . stringify ( { type : "color" , input : this . _input } ) ) ;
729
+ }
730
+ } else {
731
+ this . _cols = dimensions . columns ;
732
+ }
733
+ }
600
734
}
601
735
602
736
function reportError ( msg : string , throwErrors = false ) {
0 commit comments