import * as vscode from "vscode" ; import * as uuid from "uuid" ; const util = require ( "util" ) ; const path = require ( "path" ) ; const zmq = require ( "zeromq" ) ; import * as child_process from "child_process" ; import { readFileSync , writeFile } from "fs" ; import { deserializeMarkup } from "./serializer" ; import { tex2svg } from "./load-mathjax" ; interface ExecutionItem { id : string ; execution : vscode . NotebookCellExecution ; started ? : boolean ; hasOutput ? : boolean ; } class ExecutionQueue { private queue : ExecutionItem [ ] = [ ] ; constructor ( ) { } empty ( ) { return this . queue . length === 0 ; } clear ( ) { this . queue . map ( item => { this . end ( item . id , false ) ; } ) ; this . queue = [ ] ; } push ( execution ) { const id = uuid . v4 ( ) ; this . queue . push ( { id , execution } ) ; return id ; } private findIndex ( id ) { return this . queue . findIndex ( item => ( item . id === id ) ) ; } at ( index ) { return this . queue [ index ] || null ; } find ( id ) { return this . at ( this . findIndex ( id ) ) ; } remove ( id ) { const index = this . findIndex ( id ) ; if ( index >= 0 ) { this . queue . splice ( index , 1 ) ; } } start ( id ) { const execution = this . find ( id ) ; if ( execution ) { execution . execution . start ( Date . now ( ) ) ; execution . started = true ; } } end ( id , succeed ) { const execution = this . find ( id ) ; if ( execution ) { if ( ! ( execution ?. started ) ) { execution . execution . start ( Date . now ( ) ) ; } execution . execution . end ( succeed , Date . now ( ) ) ; this . remove ( id ) ; } } pendingExecution ( ) { if ( this . queue . length > 0 && ! ( this . queue [ 0 ] ?. started ) ) { return this . queue [ 0 ] ; } else { return null ; } } } class KernelStatusBarItem { private item : vscode . StatusBarItem ; private readonly baseText = " Wolfram Kernel" ; constructor ( ) { this . item = vscode . window . createStatusBarItem ( "wolfram-language-notebook-kernel-status" , vscode . StatusBarAlignment . Right , 100 ) ; this . item . name = "Wolfram Kernel" ; this . item . command = "wolframLanguageNotebook.manageKernels" ; this . setDisconnected ( ) ; this . item . show ( ) ; } dispose ( ) { this . item . dispose ( ) ; } show ( ) { this . item . show ( ) ; } hide ( ) { this . item . hide ( ) ; } setDisconnected ( ) { this . item . text = "$(close)" + this . baseText ; this . item . tooltip = "Currently not connected to a kernel" ; } setConnecting ( ) { this . item . text = "$(loading~spin)" + this . baseText ; this . item . tooltip = "Connecting to the kernel" ; } setConnected ( tooltip = "" , isRemote = false ) { this . item . text = ( isRemote ? "$(remote)" : "$(check)" ) + this . baseText ; this . item . tooltip = tooltip || "Kernel connected" ; } } class ExportNotebookStatusBarItem { private item : vscode . StatusBarItem ; constructor ( ) { this . item = vscode . window . createStatusBarItem ( "wolfram-language-export-notebook-status" , vscode . StatusBarAlignment . Right , 101 ) ; this . item . name = "Export Notebook" ; this . item . text = "$(loading~spin) Generating Notebook" ; this . item . command = "wolframLanguageNotebook.manageKernels" ; this . item . hide ( ) ; } show ( ) { this . item . show ( ) ; } hide ( ) { this . item . hide ( ) ; } } export class WLNotebookController { readonly id = "wolfram-language-notebook-controller" ; readonly notebookType = "wolfram-language-notebook" ; readonly label = 'Wolfram Language' ; readonly supportedLanguages = [ "wolfram" ] ; private readonly controller : vscode . NotebookController ; private selectedNotebooks : Set < vscode . NotebookDocument > = new Set ( ) ; private notebookRendererMessaging : vscode . NotebookRendererMessaging ; private statusBarKernelItem = new KernelStatusBarItem ( ) ; private statusBarExportItem = new ExportNotebookStatusBarItem ( ) ; private outputChannel = vscode . window . createOutputChannel ( "Wolfram Language Notebook" ) ; private extensionPath : string = "" ; private disposables : any [ ] = [ ] ; private kernel : any ; private socket : any ; private restartAfterExitKernel = false ; private connectingtoKernel = false ; private executionQueue = new ExecutionQueue ( ) ; constructor ( ) { this . extensionPath = vscode . extensions . getExtension ( "njpipeorgan.wolfram-language-notebook" ) ?. extensionPath || "" ; if ( ! this . extensionPath ) { throw Error ( ) ; } this . outputChannelAppendLine ( `WLNotebookController(), this.extensionPath = ${ this . extensionPath }` ) ; this . controller = vscode . notebooks . createNotebookController ( this . id , this . notebookType , this . label ) ; this . notebookRendererMessaging = vscode . notebooks . createRendererMessaging ( "wolfram-language-notebook-renderer" ) ; this . controller . supportedLanguages = this . supportedLanguages ; this . controller . supportsExecutionOrder = true ; this . controller . executeHandler = this . execute . bind ( this ) ; this . controller . onDidChangeSelectedNotebooks ( ( { notebook , selected } ) => { if ( selected ) { this . selectedNotebooks . add ( notebook ) ; this . outputChannelAppendLine ( `The controller is selected for a notebook ${ notebook . uri . fsPath }` ) ; if ( this . selectedNotebooks . size === 1 && ! this . kernelConnected ( ) ) { const defaultKernel = this . getConfig ( "kernel.connectOnOpeningNotebook" ) ; if ( defaultKernel ) { this . launchKernel ( this . getConfig ( "kernel.connectOnOpeningNotebook" ) as string ) ; } } } else { this . selectedNotebooks . delete ( notebook ) ; this . outputChannelAppendLine ( `The controller is unselected for a notebook ${ notebook . uri . fsPath }` ) ; } this . outputChannelAppendLine ( `There are ${ this . selectedNotebooks . size } notebook(s) for which the controller is selected.` ) ; if ( this . selectedNotebooks . size === 0 && this . getConfig ( "kernel.quitAutomatically" ) ) { this . quitKernel ( ) ; } } ) ; this . disposables . push ( vscode . window . onDidChangeActiveTextEditor ( event => { if ( event ?. document && this . supportedLanguages . includes ( event . document . languageId ) ) { this . statusBarKernelItem . show ( ) ; } else if ( ! this . kernelConnected ( ) ) { this . statusBarKernelItem . hide ( ) ; } } ) ) ; this . disposables . push ( vscode . workspace . onDidChangeConfiguration ( e => { if ( e . affectsConfiguration ( "wolframLanguageNotebook.rendering" ) || e . affectsConfiguration ( "wolframLanguageNotebook.frontEnd" ) ) { this . postConfigToKernel ( ) ; } } ) ) ; this . disposables . push ( this . statusBarKernelItem ) ; this . disposables . push ( this . statusBarExportItem ) ; this . disposables . push ( this . outputChannel ) ; this . controller . dispose = ( ) => { this . quitKernel ( ) ; this . outputChannelAppendLine ( `Notebook controller is disposed; there are ${ this . disposables . length } disposables.` ) ; this . disposables . forEach ( item => { item . dispose ( ) ; } ) ; } ; } getController ( ) { return this . controller ; } private outputChannelAppendLine ( str ) { this . outputChannel . appendLine ( "[" + new Date ( ) . toUTCString ( ) + "] " + str ) ; } private getConfig ( key ) { return vscode . workspace . getConfiguration ( "wolframLanguageNotebook" ) . get ( key ) ; } private getRandomPort ( portRanges ) { let ranges = [ ... portRanges . matchAll ( /\s*(\d+)\s*(?:[-‐‑‒–]\s*(\d+)\s*)?/g ) ] . map ( match => [ parseInt ( match [ 1 ] ) , parseInt ( match [ match [ 2 ] === undefined ? 1 : 2 ] ) ] ) . map ( pair => [ Math . max ( Math . min ( pair [ 0 ] , pair [ 1 ] ) , 1 ) , Math . min ( Math . max ( pair [ 0 ] , pair [ 1 ] ) + 1 , 65536 ) ] ) . filter ( pair => pair [ 0 ] < pair [ 1 ] ) ; if ( ranges . length === 0 ) { ranges = [ [ 49152 , 65536 ] ] ; } let cmf = [ ] ; ranges . reduce ( ( acc , pair , i ) => { cmf [ i ] = acc + ( pair [ 1 ] - pair [ 0 ] ) ; return cmf [ i ] ; } , 0 ) ; const rand = Math . random ( ) * cmf [ cmf . length - 1 ] ; for ( let i = 0 ; i < cmf . length ; ++ i ) { if ( rand <= cmf [ i ] ) { const lower = ranges [ i ] [ 0 ] ; const upper = ranges [ i ] [ 1 ] ; return Math . min ( Math . floor ( Math . random ( ) * ( upper - lower ) ) + lower , upper - 1 ) ; } } } private postMessageToKernel ( message ) { if ( this . socket !== undefined ) { this . socket . send ( typeof message === 'string' ? message : JSON . stringify ( message ) ) ; } else { this . outputChannelAppendLine ( "The socket is not available; cannot post the message." ) ; } } private writeFileChecked ( path , text ) { writeFile ( path , text , err => { if ( err ) { vscode . window . showErrorMessage ( `Unable to write file ${ path } \n${ err . message }` , "Retry" , "Save As..." , "Dismiss" ) . then ( value => { if ( value === "Retry" ) { this . writeFileChecked ( path , text ) ; } else if ( value === "Save As..." ) { vscode . window . showSaveDialog ( { defaultUri : vscode . Uri . file ( path ) , filters : { "All Files" : [ "*" ] } } ) . then ( value => { if ( value ) { this . writeFileChecked ( value . fsPath , text ) ; } } ) ; } } ) ; return ; } } ) ; } private async handleMessageFromKernel ( ) { while ( true ) { let [ message ] = await this . socket . receive ( ) . catch ( ( ) => { if ( this . kernelConnected ( ) ) { this . outputChannelAppendLine ( `Failed to receive messages from the kernel, but the kernel is connected.` ) ; } return [ new Error ( "receive-message" ) ] ; } ) ; if ( message instanceof Error ) { return ; } message = Buffer . from ( message ) . toString ( "utf-8" ) ; try { message = JSON . parse ( message ) ; } catch ( error ) { this . outputChannelAppendLine ( "Failed to parse the following message:" ) ; this . outputChannelAppendLine ( message ) ; continue ; } const id = message ?. uuid || "" ; const execution = this . executionQueue . find ( id ) ; switch ( message . type ) { case "show-input-name" : if ( execution ) { const match = message . name . match ( /In\[(\d+)\]/ ) ; if ( match ) { execution . execution . executionOrder = parseInt ( match [ 1 ] ) ; } } break ; case "show-output" : case "show-message" : case "show-text" : if ( execution ) { const cellLabel = String ( message . name || "" ) ; const renderMathJax = typeof message . text === "string" && Boolean ( cellLabel . match ( "^Out\\[.+\\]//TeXForm=.*" ) ) && this . getConfig ( "rendering.renderTexForm" ) === true ; const outputItems = [ ] ; if ( renderMathJax ) { outputItems . push ( vscode . NotebookCellOutputItem . text ( tex2svg ( JSON . parse ( message . text as string ) , { display : true } ) , "text/html" ) ) ; } if ( typeof message . html === "string" && ! renderMathJax ) { outputItems . push ( vscode . NotebookCellOutputItem . text ( message . html , "x-application/wolfram-language-html" ) ) ; } if ( typeof message . text === "string" && this . getConfig ( "frontEnd.storeOutputExpressions" ) ) { outputItems . push ( vscode . NotebookCellOutputItem . text ( message . text , "text/plain" ) ) ; } const output = new vscode . NotebookCellOutput ( outputItems ) ; output . metadata = { cellLabel , isBoxData : message . isBoxData || false } ; if ( execution ?. hasOutput ) { execution . execution . appendOutput ( output ) ; } else { execution . execution . replaceOutput ( output ) ; execution . hasOutput = true ; } } break ; case "evaluation-done" : if ( execution && ! execution . hasOutput ) { execution . execution . replaceOutput ( [ ] ) ; } this . executionQueue . end ( id , true ) ; this . checkoutExecutionQueue ( ) ; break ; case "request-input" : case "request-input-string" : let prompt = message . prompt as string ; let choices = null ; if ( prompt === "? " ) { prompt = "" ; } else if ( message . type === "request-input-string" ) { const match = prompt . match ( /^(.*) \(((?:.+, )*)(.+)((?:, .+)*)\) \[\3\]: $/ ) ; if ( match ) { prompt = match [ 1 ] ; choices = [ ... match [ 2 ] . split ( ", " ) . slice ( 0 , - 1 ) , match [ 3 ] , ... match [ 4 ] . split ( ", " ) . slice ( 1 ) ] . filter ( c => ( c . length > 0 ) ) ; } } if ( choices ) { const input = await vscode . window . showQuickPick ( choices , { title : "The kernel requested a choice" , placeHolder : prompt , ignoreFocusOut : true } ) ; this . postMessageToKernel ( { type : "reply-input-string" , text : input || "" } ) ; } else { const input = await vscode . window . showInputBox ( { title : "The kernel requested an input" , placeHolder : prompt , ignoreFocusOut : true } ) ; this . postMessageToKernel ( { type : ( message . type === "request-input" ? "reply-input" : "reply-input-string" ) , text : input || "" } ) ; } break ; case "reply-export-notebook" : this . statusBarExportItem . hide ( ) ; if ( ( message . text || "" ) === "" ) { vscode . window . showErrorMessage ( "Failed to export the notebook." ) ; break ; } const defaultFormat = message . format === "pdf" ? "pdf" : "nb" ; const defaultDescription = message . format === "pdf" ? "PDF" : "Wolfram Notebook" ; const exportData = message . format === "pdf" ? Buffer . from ( message . text , "base64" ) : message . text as string ; const path = await vscode . window . showSaveDialog ( { defaultUri : vscode . Uri . file ( ( message ?. path || "" ) . replace ( /\.[^/.]+$/ , "." + defaultFormat ) ) , filters : { [ defaultDescription ] : [ defaultFormat ] , "All Files" : [ "*" ] } } ) ; if ( path ) { this . writeFileChecked ( path . fsPath , exportData ) ; } break ; case "update-symbol-usages" : break ; case "update-symbol-usages-progress" : break ; default : this . outputChannelAppendLine ( "The following message has an unexpect type:" ) ; this . outputChannelAppendLine ( JSON . stringify ( message ) ) ; } } } private postConfigToKernel ( ) { const configNames = [ "frontEnd.storeOutputExpressions" , "rendering.outputSizeLimit" , "rendering.boxesTimeLimit" , "rendering.htmlTimeLimit" , "rendering.htmlMemoryLimit" , "rendering.imageWithTransparency" , "rendering.renderAsImages" ] ; const renderingConfig = vscode . workspace . getConfiguration ( "wolframLanguageNotebook" ) ; let config = { } ; configNames . forEach ( name => { config [ name . split ( '.' ) . pop ( ) as string ] = renderingConfig . get ( name ) ; } ) ; this . postMessageToKernel ( { type : "set-config" , config : config } ) ; } ; private quitKernel ( ) { if ( this . kernel ) { this . executionQueue . clear ( ) ; if ( this . kernel . pid ) { this . outputChannelAppendLine ( `Killing kernel process, pid = ${ this . kernel . pid }` ) ; } this . kernel . kill ( "SIGKILL" ) ; this . kernel = undefined ; this . connectingtoKernel = false ; } if ( this . socket !== undefined ) { this . outputChannelAppendLine ( "Closing socket" ) ; this . socket . close ( ) ; this . socket = undefined ; } this . statusBarKernelItem . setDisconnected ( ) ; } ; private showKernelLaunchFailed ( kernelName = "" ) { this . outputChannel . show ( ) ; vscode . window . showErrorMessage ( `Failed to connect to the kernel${ kernelName ? " \"" + kernelName + "\"" : "" }.` , "Try Again" , "Test in Terminal" , "Edit configurations" ) . then ( value => { if ( value === "Try Again" ) { this . launchKernel ( kernelName || undefined ) ; } else if ( value === "Test in Terminal" ) { this . launchKernel ( kernelName || undefined , true ) ; } else if ( value === "Edit configurations" ) { vscode . commands . executeCommand ( "workbench.action.openSettings" , "wolframLanguageNotebook.kernel.configurations" ) ; } } ) ; } private launchKernelWithName ( kernelName , kernel , testInTerminal = false ) { this . outputChannel . clear ( ) ; let connectionTimeout = this . getConfig ( "kernel.connectionTimeout" ) as number ; if ( ! ( 1000 < connectionTimeout ) ) { connectionTimeout = 1000 ; } const kernelIsRemote = ( kernel ?. type === "remote" ) ; const kernelCommand = String ( kernel ?. command || "" ) ; const sshCommand = String ( kernel ?. sshCommand || "ssh" ) ; const sshHost = String ( kernel ?. sshHost || "" ) ; const sshCredentialType = String ( kernel ?. sshCredentialType ) ; const sshCredential = String ( kernel ?. sshCredential || "none" ) ; const kernelPort = this . getRandomPort ( String ( kernel ?. ports ) ) ; this . outputChannelAppendLine ( `kernelIsRemote = ${ kernelIsRemote }, kernelPort = ${ kernelPort }` ) ; const kernelInitPath = path . join ( this . extensionPath , 'resources' , 'init-compressed.txt' ) ; const kernelRenderInitPath = path . join ( this . extensionPath , 'resources' , 'render-html.wl' ) ; this . outputChannelAppendLine ( `kernelInitPath = ${ kernelInitPath }` ) ; this . outputChannelAppendLine ( `kernelRenderInitPath = ${ kernelRenderInitPath }` ) ; let kernelInitString = "" ; let kernelRenderInitString = "" ; try { kernelInitString = readFileSync ( kernelInitPath ) . toString ( ) ; kernelRenderInitString = readFileSync ( kernelRenderInitPath ) . toString ( ) ; } catch ( error ) { vscode . window . showErrorMessage ( "Failed to read kernel initialization files." ) ; this . quitKernel ( ) ; return ; } const kernelInitCommands = kernelIsRemote ? `"zmqPort=${ kernelPort };ToExpression[Uncompress[\\"${ kernelInitString }\\"]]"` : `ToExpression["zmqPort=${ kernelPort };"<>Uncompress["${ kernelInitString }"]]` ; if ( this . kernel || this . socket ) { this . quitKernel ( ) ; } let launchCommand = "" ; let launchArguments = [ "" ] ; if ( kernelIsRemote ) { launchCommand = sshCommand || "ssh" ; launchArguments = [ "-tt" , ... ( sshCredentialType === "key" ? [ "-i" , sshCredential ] : [ ] ) , "-o" , "ExitOnForwardFailure=yes" , "-L" , `127.0.0.1:${ kernelPort }:127.0.0.1:${ kernelPort }` , sshHost , kernelCommand || "wolframscript" , ... ( testInTerminal ? [ ] : [ "-code" , kernelInitCommands ] ) ] ; } else { launchCommand = kernelCommand || "wolframscript" ; launchArguments = [ ... ( testInTerminal ? [ ] : [ "-code" , kernelInitCommands ] ) ] ; } this . outputChannelAppendLine ( `launchCommand = ${ String ( launchCommand ) . slice ( 0 , 200 ) }` ) ; this . outputChannelAppendLine ( `launchArguments = ${ String ( launchArguments ) . slice ( 0 , 200 ) }` ) ; if ( testInTerminal ) { const terminal = vscode . window . createTerminal ( "Wolfram Language" ) ; this . disposables . push ( terminal ) ; terminal . show ( ) ; terminal . sendText ( launchCommand + " " + launchArguments . join ( " " ) ) ; } else { this . connectingtoKernel = true ; this . statusBarKernelItem . setConnecting ( ) ; this . statusBarKernelItem . show ( ) ; this . kernel = child_process . spawn ( launchCommand , launchArguments , { stdio : "pipe" } ) ; let isFirstMessage = true ; this . kernel . stdout . on ( "data" , async ( data ) => { const message = data . toString ( ) ; if ( message . startsWith ( "<ERROR> " ) ) { vscode . window . showErrorMessage ( "The kernel has stopped due to the following error: " + message . slice ( 8 ) ) ; return ; } if ( this . connectingtoKernel ) { this . outputChannelAppendLine ( "Received the following data from kernel:" ) ; this . outputChannelAppendLine ( `${ data . toString ( ) }` ) ; } if ( isFirstMessage ) { if ( message . startsWith ( "<INITIALIZATION STARTS>" ) || ( "<INITIALIZATION STARTS>" ) . startsWith ( message ) ) { isFirstMessage = false ; } else { this . outputChannelAppendLine ( "The first message is expected to be <INITIALIZATION STARTS>, instead of the message above." ) ; if ( message . startsWith ( "Mathematica " ) || message . startsWith ( "Wolfram " ) ) { this . outputChannelAppendLine ( "  It seems that a WolframKernel is launched, but wolframscript is required" ) ; } this . quitKernel ( ) ; this . showKernelLaunchFailed ( kernelName ) ; return ; } } const match = message . match ( /\[address tcp:\/\/(127.0.0.1:[0-9]+)\]/ ) ; if ( match ) { this . socket = new zmq . Pair ( { linger : 0 } ) ; this . socket . connect ( "tcp://" + match [ 1 ] ) ; const rand = Math . floor ( Math . random ( ) * 1e9 ) . toString ( ) ; try { this . postMessageToKernel ( { type : "test" , text : rand } ) ; let timer ; const [ received ] = await Promise . race ( [ this . socket . receive ( ) , new Promise ( res => timer = setTimeout ( ( ) => res ( [ new Error ( "timeout" ) ] ) , connectionTimeout ) ) ] ) . finally ( ( ) => clearTimeout ( timer ) ) ; if ( received instanceof Error ) { throw received ; } this . outputChannelAppendLine ( "Received the following test message from kernel:" ) ; this . outputChannelAppendLine ( `${ received . toString ( ) }` ) ; const message = JSON . parse ( received . toString ( ) ) ; if ( message [ "type" ] !== "test" || message [ "text" ] !== rand ) { throw new Error ( "test" ) ; } this . evaluateFrontEnd ( kernelRenderInitString , false ) ; this . postConfigToKernel ( ) ; this . connectingtoKernel = false ; this . statusBarKernelItem . setConnected ( message [ "version" ] || "" , kernelIsRemote ) ; try { this . handleMessageFromKernel ( ) ; } catch { } this . checkoutExecutionQueue ( ) ; } catch ( error ) { if ( error instanceof Error ) { if ( error . message === "timeout" ) { this . outputChannelAppendLine ( "The kernel took too long to respond through the ZeroMQ link." ) ; } else if ( error . message === "test" ) { this . outputChannelAppendLine ( "The kernel responded with a wrong test message, as above" ) ; this . outputChannelAppendLine ( "  The expected message should contain: " + JSON . stringify ( { type : "test" , text : rand } ) ) ; } } this . quitKernel ( ) ; this . showKernelLaunchFailed ( kernelName ) ; } } } ) ; this . kernel . stderr . on ( "data" , ( data ) => { this . outputChannelAppendLine ( "Received the following data from kernel (stderr):" ) ; this . outputChannelAppendLine ( `${ data . toString ( ) }` ) ; } ) ; this . kernel . on ( "exit" , ( code , signal ) => { this . quitKernel ( ) ; this . outputChannelAppendLine ( `Process exited with code ${ code } and signal ${ signal }.` ) ; if ( this . restartAfterExitKernel ) { this . restartAfterExitKernel = false ; this . launchKernel ( ) ; } else { vscode . window . showWarningMessage ( `Kernel "${ kernelName }" has been disconnected.` ) ; } } ) ; this . kernel . on ( "error" , ( err ) => { this . outputChannelAppendLine ( `Error occured in spwaning the kernel process: \n${ err }` ) ; this . quitKernel ( ) ; this . showKernelLaunchFailed ( kernelName ) ; } ) ; } } ; private launchKernel ( kernelName = undefined , testInTerminal = false ) { if ( this . kernelConnected ( ) ) { return ; } this . quitKernel ( ) ; const kernels = this . getConfig ( "kernel.configurations" ) as any ; const numKernels = Object . keys ( kernels ) . length ; if ( kernelName === undefined ) { const withUseWolframscript = ( numKernels === 0 ) ; let leadingPicks = [ ] ; if ( withUseWolframscript ) { leadingPicks = [ { label : "$(debug-start) Use wolframscript" , detail : "Add wolframscript to kernel configurations and connect to it" } ] ; } else { leadingPicks = Object . keys ( kernels ) . map ( name => { const host = kernels [ name ] ?. type === "remote" ? String ( kernels [ name ] ?. sshHost || "remote" ) : "local" ; return { label : `$(debug-start) ${ name }` , detail : `${ host }> ${ String ( kernels [ name ] ?. command ) }` } ; } ) ; } vscode . window . showQuickPick ( [ ... leadingPicks , { label : "$(new-file) Add a new kernel" , detail : "" } , { label : "$(notebook-edit) Edit kernel configurations in settings" , detail : "" } ] ) . then ( value => { if ( value ) { if ( withUseWolframscript && value ?. label === "$(debug-start) Use wolframscript" ) { const config = vscode . workspace . getConfiguration ( "wolframLanguageNotebook" ) ; const newKernel = { wolframscript : { type : "local" , command : "wolframscript" , ports : "49152-65535" } } ; config . update ( "kernel.configurations" , { ... config . get ( "kernel.configurations" ) , ... newKernel } , vscode . ConfigurationTarget . Global ) . then ( ( ) => { setTimeout ( ( ) => { this . launchKernel ( "wolframscript" , testInTerminal ) ; } , 200 ) ; } ) ; } else if ( value . label . startsWith ( "$(debug-start)" ) ) { this . launchKernel ( value . label . substring ( 15 ) , testInTerminal ) ; } else if ( value . label === "$(new-file) Add a new kernel" ) { this . addNewKernel ( ) ; } else if ( value . label === "$(notebook-edit) Edit kernel configurations in settings" ) { vscode . commands . executeCommand ( "workbench.action.openSettings" , "wolframLanguageNotebook.kernel.configurations" ) ; } } } ) ; } else { const kernel = kernels [ kernelName ] ; if ( typeof kernel === "object" && "command" in kernel ) { this . launchKernelWithName ( kernelName , kernel , testInTerminal ) ; } else { vscode . window . showErrorMessage ( `Failed to find the kernel ${ kernelName } in configurations.\nA kernel must contain a \"command\" field.` , "Edit configurations" , "Dismiss" ) . then ( value => { if ( value === "Edit configurations" ) { vscode . commands . executeCommand ( "workbench.action.openSettings" , "wolframLanguageNotebook.kernel.configurations" ) ; } } ) ; } } } ; private kernelConnected ( ) { return Boolean ( this . kernel ) && Boolean ( this . socket ) ; } private async addNewKernel ( ) { const previousKernels = this . getConfig ( "kernel.configurations" ) as any ; const name = await vscode . window . showInputBox ( { prompt : "Enter the name of a new kernel, or an existing kernel to edit it" } ) ; if ( ! name ) { return ; } const exists = name in previousKernels && typeof previousKernels [ name ] === "object" ; const previously = exists ? previousKernels [ name ] : { } ; let type = await vscode . window . showQuickPick ( [ "On this machine" , "On a remote machine" ] , { placeHolder : `Where will this kernel be launched? ${ exists ? `(was ${ previously ?. type === "remote" ? "remote" : "local" })` : "" }` } ) ; if ( ! type ) { return ; } else { type = ( type === "On this machine" ) ? "local" : "remote" ; } let sshHost ; let sshCredentialType ; let sshCredential ; if ( type === "remote" ) { sshHost = await vscode . window . showInputBox ( { value : exists ? previously ?. sshHost : undefined , placeHolder : "user@hostname" , prompt : "Enter the username and the address of the remote machine." } ) ; if ( ! sshHost ) { return ; } sshCredentialType = await vscode . window . showQuickPick ( [ "Private key file" , "Skip" ] , { placeHolder : `Select SSH authentication method ${ exists ? `(was ${ previously ?. sshCredentialType === "key" ? "a private key file" : "skipped" })` : "" }` } ) ; if ( ! sshCredentialType ) { return ; } else { sshCredentialType = ( sshCredentialType === "Private key file" ) ? "key" : "none" ; } sshCredential = "" ; if ( sshCredentialType === "key" ) { const keyFile = await vscode . window . showOpenDialog ( { defaultUri : previously ?. sshCredential ? vscode . Uri . file ( previously . sshCredential ) : undefined } ) ; if ( keyFile === undefined || keyFile . length !== 1 ) { sshCredentialType = "none" ; } else { sshCredential = keyFile [ 0 ] . fsPath ; } } } let command = await vscode . window . showInputBox ( { value : exists ? previously ?. command : undefined , placeHolder : "Default: wolframscript" , prompt : "Enter the command to launch the kernel." } ) ; if ( command === undefined ) { return ; } else if ( command === "" ) { command = "wolframscript" ; } let ports = await vscode . window . showInputBox ( { value : exists ? previously ?. ports : undefined , placeHolder : "Default: 49152-65535" , prompt : type === "remote" ? "Enter port ranges for communication to the kernel on this machine and the remote machine" : "Enter port ranges for communication to the kernel on this machine" } ) ; if ( ports === undefined ) { return ; } else if ( ports === "" ) { ports = "49152-65535" ; } const newKernel = ( type === "remote" ) ? { [ name ] : { type , command , ports , sshCommand : "ssh" , sshHost , sshCredentialType , sshCredential } } : { [ name ] : { type , command , ports } } ; const config = vscode . workspace . getConfiguration ( "wolframLanguageNotebook" ) ; const update = await config . update ( "kernel.configurations" , { ... config . get ( "kernel.configurations" ) , ... newKernel } , vscode . ConfigurationTarget . Global ) ; vscode . window . showInformationMessage ( "A new kernel has been added." , "Start this kernel" , "Dismiss" ) . then ( value => { if ( value === "Start this kernel" ) { this . launchKernel ( name ) ; } } ) ; } ; manageKernel ( ) { if ( this . kernelConnected ( ) ) { vscode . window . showQuickPick ( [ { label : "$(debug-stop) Quit" , detail : "Quit the current kernel." } , { label : "$(debug-restart) Restart" , detail : "Quit the current kernel and start a new one." } , { label : "$(notebook-edit) Edit kernel configurations in settings" , detail : "" } ] ) . then ( value => { if ( value ?. label === "$(debug-stop) Quit" ) { this . quitKernel ( ) ; } else if ( value ?. label === "$(debug-restart) Restart" ) { this . quitKernel ( ) ; this . restartAfterExitKernel = true ; } else if ( value ?. label === "$(notebook-edit) Edit kernel configurations in settings" ) { vscode . commands . executeCommand ( "workbench.action.openSettings" , "wolframLanguageNotebook.kernel.configurations" ) ; } } ) ; } else { this . launchKernel ( ) ; } } ; private execute ( cells , _notebook , _controller ) { for ( let cell of cells ) { const execution = this . controller . createNotebookCellExecution ( cell ) ; const id = this . executionQueue . push ( execution ) ; const self = this ; execution . token . onCancellationRequested ( ( ) => { self . abortEvaluation ( id ) ; } ) ; } this . checkoutExecutionQueue ( ) ; } private abortEvaluation ( id ) { if ( ! this . executionQueue . empty ( ) ) { const execution = this . executionQueue . find ( id ) ; if ( execution ) { if ( execution ?. started ) { this . postMessageToKernel ( { type : "abort-evaluations" } ) ; } this . executionQueue . end ( id , false ) ; } } this . checkoutExecutionQueue ( ) ; } private checkoutExecutionQueue ( ) { const execution = this . executionQueue . pendingExecution ( ) ; if ( execution ) { if ( this . kernelConnected ( ) ) { const text = execution . execution . cell . document . getText ( ) . replace ( /\r\n/g , "\n" ) ; if ( text ) { this . postMessageToKernel ( { type : "evaluate-cell" , uuid : execution . id , text : text } ) ; this . executionQueue . start ( execution . id ) ; } else { this . executionQueue . start ( execution . id ) ; this . executionQueue . end ( execution . id , false ) ; } } else if ( this . connectingtoKernel ) { } else { this . launchKernel ( ) ; } } } async exportNotebook ( uri ) { let notebook ; this . selectedNotebooks . forEach ( current => { if ( current . uri . toString ( ) === uri . toString ( ) ) { notebook = current ; } } ) ; if ( ! notebook ) { return ; } const choice = await vscode . window . showQuickPick ( [ { label : "Wolfram Language Package/Script" , detail : "Export code cells only" } , { label : "Wolfram Notebook" , detail : "Export all cells" } , { label : "PDF" , detail : "Export all cells" } ] , { placeHolder : "Export As..." } ) ; if ( ! ( choice ?. label ) ) { return ; } if ( ( choice . label === "Wolfram Notebook" || choice . label === "PDF" ) && ! this . kernelConnected ( ) ) { const start = await vscode . window . showErrorMessage ( "Exporting as Wolfram Notebook/PDF requires a connected Wolfram Kernel" , "Connect" , "Dismiss" ) ; if ( start === "Connect" ) { this . launchKernel ( ) ; } return ; } const cellData = [ ] ; const decoder = new util . TextDecoder ( ) ; notebook . getCells ( ) . forEach ( cell => { if ( cell . kind === vscode . NotebookCellKind . Markup ) { cellData . push ( ... deserializeMarkup ( cell . document . getText ( ) . replace ( /\r\n/g , "\n" ) ) ) ; } else if ( cell . kind === vscode . NotebookCellKind . Code ) { const executionOrder = cell . executionSummary ?. executionOrder ; cellData . push ( { type : "Input" , label : executionOrder ? `In[${ executionOrder }]:=` : "" , text : cell . document . getText ( ) . replace ( /\r\n/g , "\n" ) } ) ; cell . outputs . forEach ( output => { const item = output . items . find ( item => item . mime === "text/plain" ) ; cellData . push ( { type : "Output" , label : ( output ?. metadata ?. cellLabel || "" ) . toString ( ) , text : decoder . decode ( item ?. data || new Uint8Array ( [ ] ) ) , isBoxData : output ?. metadata ?. isBoxData || false } ) ; } ) ; } } ) ; if ( choice . label === "Wolfram Language Package/Script" ) { const serializedCells = cellData . filter ( data => data . type === "Input" ) . map ( data => data . text + "\n" ) ; let documentText = "(* ::Package:: *)\n\n" + serializedCells . join ( "\n\n" ) ; const path = await vscode . window . showSaveDialog ( { defaultUri : vscode . Uri . file ( ( notebook . uri . fsPath || "" ) . replace ( /\.[^/.]+$/ , ".wl" ) ) , filters : { "Wolfram Language Package" : [ "wl" ] , "Wolfram Language Script" : [ "wls" ] , "All Files" : [ "*" ] } } ) ; if ( path ) { if ( path . fsPath . match ( /\.wls$/ ) ) { documentText = "#!/usr/bin/env wolframscript\n" + documentText ; } this . writeFileChecked ( path . fsPath , documentText ) ; } } else if ( choice . label === "Wolfram Notebook" || choice . label === "PDF" ) { this . statusBarExportItem . show ( ) ; this . postMessageToKernel ( { type : "request-export-notebook" , format : choice . label === "Wolfram Notebook" ? "nb" : "pdf" , path : notebook . uri . fsPath , cells : cellData } ) ; } } evaluateFrontEnd ( text , asynchronous = false ) { if ( this . kernelConnected ( ) ) { this . postMessageToKernel ( { type : "evaluate-front-end" , async : asynchronous , text : text } ) ; } } }