@@ -86,6 +86,13 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
8686 /** The WebSocket used to talk to the server */
8787 private _socket : WebSocket ;
8888
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+
8996 constructor ( private readonly _api : AtelierAPI ) { }
9097
9198 /** Hide the cursor, write `data` to the terminal, then show the cursor again. */
@@ -157,7 +164,45 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
157164 return result ;
158165 }
159166
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 ;
161206 try {
162207 // Open the WebSocket
163208 this . _socket = new WebSocket ( this . _api . terminalUrl ( ) , {
@@ -244,7 +289,7 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
244289 message . text
245290 } \x1b]633;B\x07`
246291 ) ;
247- this . _margin = this . _cursorCol = message . text . length ;
292+ this . _margin = this . _cursorCol = message . text . replace ( this . _colorsRegex , "" ) . length ;
248293 this . _prompt = message . text ;
249294 this . _promptExitCode = ";0" ;
250295 }
@@ -264,17 +309,18 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
264309 break ;
265310 case "color" : {
266311 // 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 ) ;
277317 }
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+ ) ;
278324 break ;
279325 }
280326 }
@@ -326,6 +372,8 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
326372 // Reset first line tracker
327373 this . _firstOutputLineSincePrompt = false ;
328374 }
375+ // Move cursor to the last line of the input
376+ this . _moveCursorToLastLine ( ) ;
329377
330378 // Send the input to the server for processing
331379 this . _socket . send ( JSON . stringify ( { type : this . _state , input : this . _input } ) ) ;
@@ -351,12 +399,12 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
351399 return ;
352400 }
353401 const inputArr = this . _input . split ( "\r\n" ) ;
402+ const trailingText = inputArr [ inputArr . length - 1 ] . slice ( this . _cursorCol - this . _margin ) ;
354403 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 ;
357405 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` ) ;
360408 if ( this . _input != "" && this . _state == "prompt" && this . _syntaxColoringEnabled ( ) ) {
361409 // Syntax color input
362410 this . _socket . send ( JSON . stringify ( { type : "color" , input : this . _input } ) ) ;
@@ -371,11 +419,11 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
371419 }
372420 const inputArr = this . _input . split ( "\r\n" ) ;
373421 if ( this . _margin + inputArr [ inputArr . length - 1 ] . length - this . _cursorCol > 0 ) {
422+ const trailingText = inputArr [ inputArr . length - 1 ] . slice ( this . _cursorCol - this . _margin + 1 ) ;
374423 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 ;
377425 this . _input = inputArr . join ( "\r\n" ) ;
378- this . _hideCursorWrite ( actions . deleteChar ) ;
426+ this . _hideCursorWrite ( `\x1b7\x1b[0J ${ trailingText } \x1b8` ) ;
379427 if ( this . _input != "" && this . _state == "prompt" && this . _syntaxColoringEnabled ( ) ) {
380428 // Syntax color input
381429 this . _socket . send ( JSON . stringify ( { type : "color" , input : this . _input } ) ) ;
@@ -401,7 +449,6 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
401449 // Scroll back one more input
402450 this . _historyIdx -- ;
403451 }
404- const oldInput = this . _input ;
405452 if ( this . _historyIdx >= 0 ) {
406453 this . _input = this . _history [ this . _historyIdx ] ;
407454 } else if ( this . _historyIdx == - 1 ) {
@@ -411,8 +458,10 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
411458 // If we hit the end, leave the input blank
412459 this . _input = "" ;
413460 }
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 } ` ) ;
414464 this . _cursorCol = this . _margin + this . _input . length ;
415- this . _hideCursorWrite ( `${ oldInput . length ? `\x1b[${ oldInput . length } D\x1b[0K` : "" } ${ this . _input } ` ) ;
416465 if ( this . _input != "" && this . _syntaxColoringEnabled ( ) ) {
417466 // Syntax color input
418467 this . _socket . send ( JSON . stringify ( { type : "color" , input : this . _input } ) ) ;
@@ -436,15 +485,16 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
436485 } else {
437486 this . _historyIdx ++ ;
438487 }
439- const oldInput = this . _input ;
440488 if ( this . _historyIdx != - 1 ) {
441489 this . _input = this . _history [ this . _historyIdx ] ;
442490 } else {
443491 // If we hit the beginning, leave the input blank
444492 this . _input = "" ;
445493 }
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 } ` ) ;
446497 this . _cursorCol = this . _margin + this . _input . length ;
447- this . _hideCursorWrite ( `${ oldInput . length ? `\x1b[${ oldInput . length } D\x1b[0K` : "" } ${ this . _input } ` ) ;
448498 if ( this . _input != "" && this . _syntaxColoringEnabled ( ) ) {
449499 // Syntax color input
450500 this . _socket . send ( JSON . stringify ( { type : "color" , input : this . _input } ) ) ;
@@ -457,9 +507,14 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
457507 return ;
458508 }
459509 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+ }
461517 this . _cursorCol -- ;
462- this . _hideCursorWrite ( actions . cursorBack ) ;
463518 }
464519 return ;
465520 }
@@ -468,10 +523,15 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
468523 // User can't move cursor
469524 return ;
470525 }
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 ) {
473527 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+ }
475535 }
476536 return ;
477537 }
@@ -490,31 +550,31 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
490550 case keys . home :
491551 case keys . ctrlA : {
492552 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 ) ;
496555 }
497556 return ;
498557 }
499558 case keys . end :
500559 case keys . ctrlE : {
501560 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 ) ;
507565 }
508566 }
509567 return ;
510568 }
511569 case keys . ctrlU : {
512570 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
514572 const inputArr = this . _input . split ( "\r\n" ) ;
515573 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" ) ;
518578 inputArr [ inputArr . length - 1 ] = "" ;
519579 this . _input = inputArr . join ( "\r\n" ) ;
520580 if ( this . _input != "" && this . _syntaxColoringEnabled ( ) ) {
@@ -545,21 +605,66 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
545605 // Replace all single \r with \r\n (prompt) or space (read)
546606 char = char . replace ( / \r / g, this . _state == "prompt" ? "\r\n" : " " ) ;
547607 const inputArr = this . _input . split ( "\r\n" ) ;
608+ let eraseAfterCursor = "" ,
609+ trailingText = "" ;
548610 if ( this . _cursorCol < this . _margin + inputArr [ inputArr . length - 1 ] . length ) {
549611 // Insert the new char(s)
612+ trailingText = inputArr [ inputArr . length - 1 ] . slice ( this . _cursorCol - this . _margin ) ;
550613 inputArr [ inputArr . length - 1 ] = `${ inputArr [ inputArr . length - 1 ] . slice (
551614 0 ,
552615 this . _cursorCol - this . _margin
553- ) } ${ char } ${ inputArr [ inputArr . length - 1 ] . slice ( this . _cursorCol - this . _margin ) } `;
616+ ) } ${ char } ${ trailingText } `;
554617 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" ;
557619 } else {
558620 // Append the new char(s)
559621 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 ;
560639 this . _cursorCol += char . length ;
561- this . _hideCursorWrite ( char . replace ( / \r \n / g, `\r\n${ this . _multiLinePrompt } ` ) ) ;
562640 }
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 } ` ) ;
563668 if ( submit ) {
564669 if ( this . _state == "prompt" ) {
565670 // Reset historyIdx
@@ -579,6 +684,8 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
579684 // Reset first line tracker
580685 this . _firstOutputLineSincePrompt = false ;
581686 }
687+ // Move cursor to the last line of the input
688+ this . _moveCursorToLastLine ( ) ;
582689
583690 // Send the input to the server for processing
584691 this . _socket . send ( JSON . stringify ( { type : this . _state , input : this . _input } ) ) ;
@@ -597,6 +704,33 @@ class WebSocketTerminal implements vscode.Pseudoterminal {
597704 }
598705 }
599706 }
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+ }
600734}
601735
602736function reportError ( msg : string , throwErrors = false ) {
0 commit comments