import * as vscode from 'vscode' ; import { BrowserPreview } from './editorPreview/browserPreview' ; import { Disposable } from './utils/dispose' ; import { Server } from './server/serverManager' ; import { INIT_PANEL_TITLE , DONT_SHOW_AGAIN , OUTPUT_CHANNEL_NAME , } from './utils/constants' ; import { ServerStartedStatus , ServerTaskProvider , } from './task/serverTaskProvider' ; import { Settings , SETTINGS_SECTION_ID , SettingUtil , } from './utils/settingsUtil' ; import TelemetryReporter from 'vscode-extension-telemetry' ; import { EndpointManager } from './infoManagers/endpointManager' ; import { WorkspaceManager } from './infoManagers/workspaceManager' ; import { ConnectionManager } from './infoManagers/connectionManager' ; import { PathUtil } from './utils/pathUtil' ; description export interface serverMsg { method : string ; url : string ; status : number ; } description export interface launchInfo { external : boolean ; file : string ; relative : boolean ; debug : boolean ; panel ? : vscode . WebviewPanel ; } export class Manager extends Disposable { public currentPanel : BrowserPreview | undefined ; private readonly _server : Server ; private readonly _serverTaskProvider : ServerTaskProvider ; private readonly _endpointManager : EndpointManager ; private readonly _workspaceManager : WorkspaceManager ; private readonly _connectionManager : ConnectionManager ; private readonly _outputChannel : vscode . OutputChannel ; private _pendingLaunchInfo : launchInfo | undefined ; private _runTaskWithExternalPreview : boolean ; private _previewActive = false ; private _currentTimeout : NodeJS . Timeout | undefined ; private _notifiedAboutLooseFiles = false ; public get workspace ( ) : vscode . WorkspaceFolder | undefined { return this . _workspaceManager . workspace ; } public get workspacePath ( ) : string | undefined { return this . _workspaceManager . workspacePath ; } constructor ( private readonly _extensionUri , private readonly _reporter , userDataDir ) { super ( ) ; this . _outputChannel = vscode . window . createOutputChannel ( OUTPUT_CHANNEL_NAME ) ; this . _workspaceManager = this . _register ( new WorkspaceManager ( ) ) ; this . _endpointManager = this . _register ( new EndpointManager ( this . _workspaceManager ) ) ; const serverPort = SettingUtil . GetConfig ( _extensionUri ) . portNumber ; const serverWSPort = serverPort ; const serverHost = SettingUtil . GetConfig ( _extensionUri ) . hostIP ; this . _connectionManager = this . _register ( new ConnectionManager ( serverPort , serverWSPort , serverHost ) ) ; this . _server = this . _register ( new Server ( _extensionUri , _reporter , this . _endpointManager , this . _workspaceManager , this . _connectionManager , userDataDir ) ) ; this . _serverTaskProvider = new ServerTaskProvider ( this . _reporter , this . _endpointManager , this . _workspaceManager , this . _connectionManager ) ; this . _runTaskWithExternalPreview = SettingUtil . GetConfig ( _extensionUri ) . runTaskWithExternalPreview ; this . _register ( vscode . tasks . registerTaskProvider ( ServerTaskProvider . CustomBuildScriptType , this . _serverTaskProvider ) ) ; this . _register ( this . _server . onNewReqProcessed ( ( e ) => { this . _serverTaskProvider . sendServerInfoToTerminal ( e ) ; } ) ) ; this . _register ( this . _serverTaskProvider . onRequestToOpenServer ( ( ) => { this . openServer ( true ) ; } ) ) ; this . _register ( this . _serverTaskProvider . onRequestToCloseServer ( ( ) => { if ( this . _previewActive ) { this . _serverTaskProvider . serverStop ( false ) ; } else { this . closeServer ( ) ; this . _serverTaskProvider . serverStop ( true ) ; } } ) ) ; this . _connectionManager . onConnected ( ( e ) => { this . _serverTaskProvider . serverStarted ( e . httpURI , ServerStartedStatus . JUST_STARTED ) ; if ( this . _pendingLaunchInfo ) { if ( this . _pendingLaunchInfo . external ) { this . launchFileInExternalBrowser ( this . _pendingLaunchInfo . file , this . _pendingLaunchInfo . relative , this . _pendingLaunchInfo . debug ) ; } else { this . launchFileInEmbeddedPreview ( this . _pendingLaunchInfo . file , this . _pendingLaunchInfo . relative , this . _pendingLaunchInfo . panel ) ; } this . _pendingLaunchInfo = undefined ; } } ) ; vscode . workspace . onDidChangeConfiguration ( ( e ) => { if ( e . affectsConfiguration ( SETTINGS_SECTION_ID ) ) { this . _server . updateConfigurations ( ) ; this . _connectionManager . pendingPort = SettingUtil . GetConfig ( this . _extensionUri ) . portNumber ; this . _connectionManager . pendingHost = SettingUtil . GetConfig ( this . _extensionUri ) . hostIP ; this . _runTaskWithExternalPreview = SettingUtil . GetConfig ( this . _extensionUri ) . runTaskWithExternalPreview ; } } ) ; this . _serverTaskProvider . onRequestOpenEditorToSide ( ( uri ) => { if ( this . _previewActive && this . currentPanel ) { const avoidColumn = this . currentPanel . panel . viewColumn ?? vscode . ViewColumn . One ; const column = avoidColumn == vscode . ViewColumn . One ? avoidColumn + 1 : avoidColumn - 1 ; vscode . commands . executeCommand ( 'vscode.open' , uri , { viewColumn : column , } ) ; } else { vscode . commands . executeCommand ( 'vscode.open' , uri ) ; } } ) ; } dispose ( ) { this . _server . closeServer ( ) ; } param { vscode . WebviewPanel } panel param { string } file param { boolean } relative param { boolean } debug public createOrShowEmbeddedPreview ( panel = undefined , file = '/' , relative = true , debug = false ) { if ( ! this . _server . isRunning ) { this . _pendingLaunchInfo = { external : false , panel : panel , file : file , relative : relative , debug : debug , } ; this . openServer ( ) ; } else { this . launchFileInEmbeddedPreview ( file , relative , panel ) ; } } param { string } file param { boolean } relative param { boolean } debug public showPreviewInBrowser ( file = '/' , relative = true , debug = false ) { if ( ! this . _serverTaskProvider . isRunning ) { if ( ! this . _server . isRunning ) { this . _pendingLaunchInfo = { external : true , file : file , relative : relative , debug : debug , } ; } else { this . launchFileInExternalBrowser ( file , relative , debug ) ; } if ( this . _workspaceManager . numPaths > 0 && this . _runTaskWithExternalPreview ) { this . _serverTaskProvider . extRunTask ( SettingUtil . GetConfig ( this . _extensionUri ) . browserPreviewLaunchServerLogging ) ; } else { const serverOn = this . openServer ( ) ; if ( ! serverOn ) { return ; } } } else { this . launchFileInExternalBrowser ( file , relative , debug ) ; } } param { boolean } fromTask returns { boolean } public openServer ( fromTask = false ) { if ( ! this . _server . isRunning ) { return this . _server . openServer ( this . _serverPort ) ; } else if ( fromTask ) { this . _connectionManager . resolveExternalHTTPUri ( ) . then ( ( uri ) => { this . _serverTaskProvider . serverStarted ( uri , ServerStartedStatus . STARTED_BY_EMBEDDED_PREV ) ; } ) ; } return true ; } returns { boolean } public closeServer ( ) { if ( this . _server . isRunning ) { this . _server . closeServer ( ) ; if ( this . currentPanel ) { this . currentPanel . close ( ) ; } if ( this . _serverTaskProvider . isRunning ) { this . _serverTaskProvider . serverStop ( true ) ; } this . _connectionManager . disconnected ( ) ; return true ; } return false ; } returns { WebviewOptions } public getWebviewOptions ( ) { const options = { enableScripts : true , localResourceRoots : [ vscode . Uri . joinPath ( this . _extensionUri , 'media' ) , vscode . Uri . joinPath ( this . _extensionUri , 'node_modules' , '@vscode' , 'codicons' , 'dist' ) , ] , } ; return options ; } returns { vscode . WebviewPanelOptions } public getWebviewPanelOptions ( ) { return { retainContextWhenHidden : true , } ; } param { string } file returns { string } public encodeEndpoint ( file ) { return this . _endpointManager . encodeLooseFileEndpoint ( file ) ; } param { string } endpoint returns { string | undefined } public decodeEndpoint ( endpoint ) { return this . _endpointManager . decodeLooseFileEndpoint ( endpoint ) ; } param { string } file returns { boolean } public absPathInDefaultWorkspace ( file ) { return this . _workspaceManager . absPathInDefaultWorkspace ( file ) ; } param { string } file returns { boolean } public pathExistsRelativeToWorkspace ( file ) { return this . _workspaceManager . pathExistsRelativeToDefaultWorkspace ( file ) ; } param { string } file returns { string } public getFileRelativeToDefaultWorkspace ( file ) { return this . _workspaceManager . getFileRelativeToDefaultWorkspace ( file ) ; } returns { number } private get _serverPort ( ) : number { return this . _connectionManager . httpPort ; } param { string } file param { boolean } relative param { vscode . WebviewPanel | undefined } panel private launchFileInEmbeddedPreview ( file , relative , panel ) { file = this . transformNonRelativeFile ( relative , file ) ; if ( this . currentPanel ) { this . currentPanel . reveal ( vscode . ViewColumn . Beside , file ) ; return ; } if ( ! panel ) { panel = vscode . window . createWebviewPanel ( BrowserPreview . viewType , INIT_PANEL_TITLE , vscode . ViewColumn . Beside , { ... this . getWebviewOptions ( ) , ... this . getWebviewPanelOptions ( ) , } ) ; } this . startEmbeddedPreview ( panel , file ) ; } param { string } file param { boolean } relative param { boolean } debug private launchFileInExternalBrowser ( file , relative , debug ) { const relFile = PathUtil . ConvertToUnixPath ( this . transformNonRelativeFile ( relative , file ) ) ; const url = `http://${ this . _connectionManager . host }:${ this . _serverPort }${ relFile }` ; if ( debug ) { vscode . commands . executeCommand ( 'extension.js-debug.debugLink' , url ) ; } else { vscode . env . openExternal ( vscode . Uri . parse ( url ) ) ; } } param { vscode . WebviewPanel } panel param { string } file private startEmbeddedPreview ( panel , file ) { if ( this . _currentTimeout ) { clearTimeout ( this . _currentTimeout ) ; } this . currentPanel = this . _register ( new BrowserPreview ( file , panel , this . _extensionUri , this . _reporter , this . _workspaceManager , this . _connectionManager , this . _outputChannel ) ) ; this . _previewActive = true ; this . _register ( this . currentPanel . onDispose ( ( ) => { this . currentPanel = undefined ; const closeServerDelay = SettingUtil . GetConfig ( this . _extensionUri ) . serverKeepAliveAfterEmbeddedPreviewClose ; this . _currentTimeout = setTimeout ( ( ) => { if ( this . _server . isRunning && ! this . _serverTaskProvider . isRunning && this . workspace && this . _runTaskWithExternalPreview ) { this . closeServer ( ) ; } this . _previewActive = false ; } , Math . floor ( closeServerDelay * 1000 * 60 ) ) ; } ) ) ; } param { boolean } relative param { string } file returns { string } private transformNonRelativeFile ( relative , file ) { if ( ! relative ) { if ( ! this . _workspaceManager . absPathInDefaultWorkspace ( file ) ) { if ( ! this . _workspaceManager . absPathInAnyWorkspace ( file ) ) { this . notifyLooseFileOpen ( ) ; } file = this . encodeEndpoint ( file ) ; } else { file = this . _workspaceManager . getFileRelativeToDefaultWorkspace ( file ) ?? '' ; } } return file ; } description private notifyLooseFileOpen ( ) { this . _reporter . sendTelemetryEvent ( 'preview.fileOutOfWorkspace' ) ; if ( ! this . _notifiedAboutLooseFiles && SettingUtil . GetConfig ( this . _extensionUri ) . notifyOnOpenLooseFile ) { vscode . window . showWarningMessage ( 'Previewing a file that is not a child of the server root. To see fully correct relative file links, please open a workspace at the project root.' , DONT_SHOW_AGAIN ) . then ( ( selection ) => { if ( selection == DONT_SHOW_AGAIN ) { SettingUtil . UpdateSettings ( Settings . notifyOnOpenLooseFile , false ) ; } } ) ; } this . _notifiedAboutLooseFiles = true ; } }