@@ -3,16 +3,18 @@ import type { WebSocket } from 'ws'
33import type { ResolvedMeta } from '../api'
44import type { VitestPackage } from './pkg'
55import type { ExtensionWorkerProcess } from './types'
6+ import type { WsConnectionMetadata } from './ws'
67import { createServer } from 'node:http'
78import { pathToFileURL } from 'node:url'
9+ import { stripVTControlCharacters } from 'node:util'
810import getPort from 'get-port'
911import * as vscode from 'vscode'
1012import { WebSocketServer } from 'ws'
1113import { getConfig } from '../config'
1214import { workerPath } from '../constants'
1315import { createErrorLogger , log } from '../log'
14- import { formatPkg , showVitestError } from '../utils'
15- import { waitForWsConnection , WsConnectionMetadata } from './ws'
16+ import { formatPkg } from '../utils'
17+ import { waitForWsConnection } from './ws'
1618
1719export async function createVitestTerminalProcess ( pkg : VitestPackage ) : Promise < ResolvedMeta > {
1820 const pnpLoader = pkg . loader
@@ -42,27 +44,80 @@ export async function createVitestTerminalProcess(pkg: VitestPackage): Promise<R
4244 NODE_ENV : env . NODE_ENV ?? process . env . NODE_ENV ?? 'test' ,
4345 } ,
4446 } )
47+ // TODO: make sure it is desposed even if it throws, the same for child_process
48+
49+ const shellIntegration = await new Promise < vscode . TerminalShellIntegration | undefined > ( ( resolve ) => {
50+ const disposable = vscode . window . onDidChangeTerminalShellIntegration ( ( e ) => {
51+ const timeout = setTimeout ( ( ) => {
52+ disposable . dispose ( )
53+ resolve ( undefined )
54+ } , 3_000 )
55+
56+ if ( e . terminal === terminal ) {
57+ disposable . dispose ( )
58+ clearTimeout ( timeout )
59+ resolve ( e . shellIntegration )
60+ }
61+ } )
62+ } )
63+
64+ const processId = await terminal . processId
65+ if ( terminal . exitStatus && terminal . exitStatus . code != null ) {
66+ throw new Error ( `Terminal was ${ getExitReason ( terminal . exitStatus . reason ) } with code ${ terminal . exitStatus . code } ` )
67+ }
68+
4569 let command = 'node'
4670 if ( pnpLoader && pnp ) {
4771 command += ` --require ${ pnp } --experimental-loader ${ pathToFileURL ( pnpLoader ) . toString ( ) } `
4872 }
49- command += ` ${ workerPath } `
50- log . info ( '[API]' , `Initiated ws connection via ${ wsAddress } ` )
51- log . info ( '[API]' , `Starting ${ formatPkg ( pkg ) } in the terminal: ${ command } ` )
52- terminal . sendText ( command , true )
73+ command += ` ${ workerPath } ;`
74+
75+ log . info ( '[TERMINAL]' , `Initiated ws connection via ${ wsAddress } ` )
76+ log . info ( '[TERMINAL]' , `Starting ${ formatPkg ( pkg ) } in the terminal: ${ command } ` )
77+
78+ if ( shellIntegration ) {
79+ log . info ( '[TERMINAL] Shell integration is initiated.' )
80+ let execution : vscode . TerminalShellExecution
81+ const onWriteShell = vscode . window . onDidStartTerminalShellExecution ( async ( e ) => {
82+ if ( e . execution !== execution ) {
83+ return
84+ }
85+
86+ log . info ( '[TERMINAL] Reporting the shell output.' )
87+ for await ( const line of e . execution . read ( ) ) {
88+ log . worker ( 'info' , stripVTControlCharacters ( line ) )
89+ onWriteShell . dispose ( )
90+ }
91+ } )
92+
93+ const onEndShell = vscode . window . onDidEndTerminalShellExecution ( ( e ) => {
94+ if ( e . execution === execution ) {
95+ log . info ( '[TERMINAL] The shell execution was finished.' )
96+ onWriteShell . dispose ( )
97+ onEndShell . dispose ( )
98+ }
99+ } )
100+ execution = shellIntegration . executeCommand ( command )
101+ }
102+ else {
103+ log . info ( '[TERMINAL] Shell integration is not initiated, fallback to `terminal.sendText`.' )
104+ terminal . sendText ( command , true )
105+ }
106+
53107 const meta = await new Promise < WsConnectionMetadata > ( ( resolve , reject ) => {
54108 const timeout = setTimeout ( ( ) => {
55109 terminal . show ( false )
56110 reject ( new Error ( `The extension could not connect to the terminal in 5 seconds. See the "vitest" terminal output for more details.` ) )
57- } , 5000 )
58- waitForWsConnection ( wss , pkg , false , 'terminal' ) . then ( resolve , reject ) . finally ( ( ) => {
111+ } , 5_000 )
112+ wss . once ( 'connection' , ( ) => {
59113 clearTimeout ( timeout )
60114 } )
115+ waitForWsConnection ( wss , pkg , false , 'terminal' , ! ! shellIntegration ) . then ( resolve , reject )
61116 } )
62- const processId = ( await terminal . processId ) ?? Math . random ( )
117+
63118 log . info ( '[API]' , `${ formatPkg ( pkg ) } terminal process ${ processId } created` )
64119 const vitestProcess = new ExtensionTerminalProcess (
65- processId ,
120+ processId ?? Math . random ( ) ,
66121 terminal ,
67122 server ,
68123 meta . ws ,
@@ -77,6 +132,22 @@ export async function createVitestTerminalProcess(pkg: VitestPackage): Promise<R
77132 }
78133}
79134
135+ function getExitReason ( reason : vscode . TerminalExitReason ) {
136+ switch ( reason ) {
137+ case vscode . TerminalExitReason . Extension :
138+ return 'clsoed by extension'
139+ case vscode . TerminalExitReason . Process :
140+ return 'closed by the process'
141+ case vscode . TerminalExitReason . Shutdown :
142+ return 'reloaded or closed'
143+ case vscode . TerminalExitReason . User :
144+ return 'closed by the user'
145+ case vscode . TerminalExitReason . Unknown :
146+ default :
147+ return 'unexpectedly closed'
148+ }
149+ }
150+
80151export class ExtensionTerminalProcess implements ExtensionWorkerProcess {
81152 private _onDidExit = new vscode . EventEmitter < number | null > ( )
82153
0 commit comments