'use strict' ; import { window , workspace , Uri , ViewColumn , ExtensionContext , TextDocument , WebviewPanel , Disposable , TextDocumentChangeEvent , Webview , commands } from 'vscode' ; import { instrumentOperationAsVsCodeCommand } from "vscode-extension-telemetry-wrapper" ; import * as path from 'path' ; import { PlanEvaluator , PlanFunctionEvaluator } from 'ai-planning-val' ; import { PlanInfo , PLAN , VariableExpression , Plan , utils } from 'pddl-workspace' ; const makeSerializable = utils . serializationUtils . makeSerializable ; import { isPlan , getDomainAndProblemForPlan } from '../workspace/workspaceUtils' ; import { CodePddlWorkspace } from '../workspace/CodePddlWorkspace' ; import { CONF_PDDL , PLAN_REPORT_WIDTH , PDDL_CONFIGURE_COMMAND , VAL_STEP_PATH , VALUE_SEQ_PATH , VAL_VERBOSE , PLAN_REPORT_LINE_PLOT_GROUP_BY_LIFTED } from '../configuration/configuration' ; import { Menu } from '../Menu' ; import { ensureAbsoluteGlobalStoragePath , showError } from '../utils' ; import { LinePlotData , FinalStateData } from './model' ; import { handleValStepError } from './valStepErrorHandler' ; import { getDomainVisualizationConfigurationDataForPlans } from './DomainVisualization' ; import { getWebViewHtml } from '../webviewUtils' ; const VIEWS = "views" ; const COMMON_FOLDER = path . join ( VIEWS , "common" ) ; const STATIC_CONTENT_FOLDER = path . join ( VIEWS , 'planview' , 'static' ) ; export const PDDL_GENERATE_PLAN_REPORT = 'pddl.planReport' ; export const PDDL_EXPORT_PLAN = 'pddl.exportPlan' ; export const PDDL_SAVE_AS_EXPECTED_PLAN = 'pddl.saveAsExpectedPlan' ; export class PlanView extends Disposable { private webviewPanels = new Map < Uri , PlanPreviewPanel > ( ) ; private timeout : NodeJS . Timer | undefined ; public static readonly PLANNER_OUTPUT_URI = Uri . parse ( "pddl://planner/output" ) ; constructor ( private context , private codePddlWorkspace ) { super ( ( ) => this . dispose ( ) ) ; context . subscriptions . push ( instrumentOperationAsVsCodeCommand ( "pddl.plan.preview" , async ( planUri ) => { const planDocument = await getPlanDocument ( planUri ) ; if ( planDocument ) { return this . revealOrCreatePreview ( planDocument , ViewColumn . Beside ) ; } } ) ) ; context . subscriptions . push ( workspace . onDidChangeTextDocument ( ( e ) => { if ( e . document . languageId === PLAN ) { this . setNeedsRebuild ( e . document ) ; } } ) ) ; context . subscriptions . push ( workspace . onDidSaveTextDocument ( ( doc ) => { if ( doc . languageId === PLAN ) { this . setNeedsRebuild ( doc ) ; } } ) ) ; context . subscriptions . push ( workspace . onDidChangeConfiguration ( e => { if ( e . affectsConfiguration ( CONF_PDDL + '.' + PLAN_REPORT_WIDTH ) ) { this . webviewPanels . forEach ( panel => panel . setNeedsRebuild ( true ) ) ; this . rebuild ( ) ; } } ) ) ; } setPlannerOutput ( plans , reveal ) { const plannerOutputPanel = this . getPlannerOutputPanel ( ) ; plannerOutputPanel . setPlans ( plans ) ; this . resetTimeout ( ) ; if ( plans . length > 0 && reveal ) { plannerOutputPanel . reveal ( ) ; } } getPlannerOutputPanel ( ) { let plannerOutputPanel = this . webviewPanels . get ( PlanView . PLANNER_OUTPUT_URI ) ; if ( ! plannerOutputPanel ) { plannerOutputPanel = this . createPreviewPanel ( 'Planner output' , PlanView . PLANNER_OUTPUT_URI , ViewColumn . Three ) ; this . webviewPanels . set ( PlanView . PLANNER_OUTPUT_URI , plannerOutputPanel ) ; } return plannerOutputPanel ; } hasPlannerOutput ( ) { return this . webviewPanels . has ( PlanView . PLANNER_OUTPUT_URI ) ; } async setNeedsRebuild ( planDocument ) { const panel = this . webviewPanels . get ( planDocument . uri ) ; if ( panel ) { try { const plan = await this . parsePlanFile ( planDocument ) ; panel . setPlans ( [ plan ] ) ; } catch ( ex ) { panel . setError ( ex as Error ) ; } this . resetTimeout ( ) ; } } resetTimeout ( ) { if ( this . timeout ) { clearTimeout ( this . timeout ) ; } this . timeout = setTimeout ( ( ) => this . rebuild ( ) , 1000 ) ; } dispose ( ) { if ( this . timeout ) { clearTimeout ( this . timeout ) ; } } rebuild ( webviewPanel ? ) { this . webviewPanels . forEach ( async ( panel ) => { const updateThisPanel = webviewPanel === undefined || webviewPanel === panel . getPanel ( ) ; if ( updateThisPanel && panel . getNeedsRebuild ( ) && panel . getPanel ( ) . visible ) { this . updateContent ( panel ) ; } } ) ; } async updateContent ( previewPanel ) { previewPanel . setNeedsRebuild ( false ) ; const error = previewPanel . getError ( ) ; if ( error ) { previewPanel . getPanel ( ) . webview . postMessage ( { command : 'error' , message : error . message } ) ; } else { const plans = previewPanel . getPlans ( ) . map ( p => makeSerializable ( p ) ) ; const plansData = await getDomainVisualizationConfigurationDataForPlans ( plans ) ; previewPanel . getPanel ( ) . webview . postMessage ( { command : 'showPlans' , data : plansData } ) ; } } async revealOrCreatePreview ( doc , displayColumn ) { let previewPanel = this . webviewPanels . get ( doc . uri ) ; if ( previewPanel ) { previewPanel . reveal ( displayColumn ) ; } else { previewPanel = this . createPreviewPanelForDocument ( doc , displayColumn ) ; this . webviewPanels . set ( doc . uri , previewPanel ) ; } await this . setNeedsRebuild ( doc ) ; } createPreviewPanelForDocument ( doc , displayColumn ) { const previewTitle = `Preview '${ path . basename ( doc . uri . fsPath ) }'` ; return this . createPreviewPanel ( previewTitle , doc . uri , displayColumn ) ; } createPreviewPanel ( previewTitle , uri , displayColumn ) { const webViewPanel = window . createWebviewPanel ( 'planPreview' , previewTitle , displayColumn , { enableFindWidget : true , enableCommandUris : true , enableScripts : true , localResourceRoots : [ Uri . file ( this . context . asAbsolutePath ( VIEWS ) ) ] } ) ; webViewPanel . iconPath = Uri . file ( this . context . asAbsolutePath ( path . join ( "views" , "overview" , "file_type_pddl_plan.svg" ) ) ) ; this . getHtml ( webViewPanel . webview ) . then ( html => webViewPanel . webview . html = html ) ; const previewPanel = new PlanPreviewPanel ( uri , webViewPanel ) ; previewPanel . getPanel ( ) . onDidDispose ( ( ) => this . webviewPanels . delete ( uri ) , undefined , this . context . subscriptions ) ; previewPanel . getPanel ( ) . onDidChangeViewState ( e => this . rebuild ( e . webviewPanel ) ) ; previewPanel . getPanel ( ) . webview . onDidReceiveMessage ( e => this . handleMessage ( previewPanel , e ) , undefined , this . context . subscriptions ) ; return previewPanel ; } async getHtml ( webview ) { const googleCharts = Uri . parse ( "https://www.gstatic.com/charts/" ) ; return getWebViewHtml ( this . context , { relativePath : STATIC_CONTENT_FOLDER , htmlFileName : 'plans.html' , externalImages : [ Uri . parse ( 'data:' ) ] , allowUnsafeEval : true , externalScripts : [ googleCharts ] , externalStyles : [ googleCharts ] , fonts : [ Uri . file ( path . join ( ".." , ".." , ".." , COMMON_FOLDER , "codicon.ttf" ) ) ] } , webview ) ; } async parsePlanFile ( planDocument ) { const planFileInfo = await this . codePddlWorkspace . upsertAndParseFile ( planDocument ) as PlanInfo ; try { const domainAndProblem = getDomainAndProblemForPlan ( planFileInfo , this . codePddlWorkspace . pddlWorkspace ) ; return planFileInfo . getPlan ( domainAndProblem . domain , domainAndProblem . problem ) ; } catch ( ex ) { return new Plan ( planFileInfo . getSteps ( ) ) ; } } handleMessage ( previewPanel , message ) { console . log ( `Message received from the webview: ${ message . command }` ) ; switch ( message . command ) { case 'onload' : this . updateContent ( previewPanel ) ; break ; case 'linePlotDataRequest' : { const planIndex = message . planIndex ; this . getLinePlotData ( previewPanel , planIndex ) . catch ( showError ) ; break ; } case 'finalStateDataRequest' : { const planIndex = message . planIndex ; this . getFinalStateData ( previewPanel , planIndex ) . catch ( showError ) ; break ; } case 'selectPlan' : const planIndex = message . planIndex ; previewPanel . setSelectedPlanIndex ( planIndex ) ; break ; case 'showMenu' : this . showMenu ( previewPanel ) ; break ; case 'revealAction' : commands . executeCommand ( "pddl.revealAction" , Uri . file ( message . domainUri . fsPath ) , message . action ) ; break ; default : console . warn ( 'Unexpected command: ' + message . command ) ; } } async getLinePlotData ( previewPanel , planIndex ) { if ( planIndex >= previewPanel . getPlans ( ) . length ) { console . error ( `requesting data for plan index {} while there are only {} plans` , planIndex , previewPanel . getPlans ( ) . length ) ; return ; } const plan = previewPanel . getPlans ( ) [ planIndex ] ; const valStepPath = ensureAbsoluteGlobalStoragePath ( workspace . getConfiguration ( CONF_PDDL ) . get < string > ( VAL_STEP_PATH ) , this . context ) ; const valueSeqPath = ensureAbsoluteGlobalStoragePath ( workspace . getConfiguration ( CONF_PDDL ) . get < string > ( VALUE_SEQ_PATH ) , this . context ) ; const valVerbose = workspace . getConfiguration ( CONF_PDDL ) . get < boolean > ( VAL_VERBOSE , false ) ; try { if ( plan . domain && plan . problem ) { const groupByLifted = workspace . getConfiguration ( CONF_PDDL ) . get < boolean > ( PLAN_REPORT_LINE_PLOT_GROUP_BY_LIFTED , true ) ; const evaluator = new PlanFunctionEvaluator ( plan , { valStepPath , valueSeqPath , shouldGroupByLifted : groupByLifted , verbose : valVerbose } ) ; if ( evaluator . isAvailable ( ) ) { try { const functionValues = await evaluator . evaluate ( ) ; functionValues . forEach ( ( values , liftedVariable ) => { let chartTitleWithUnit = values . legend . length > 1 ? liftedVariable . name : liftedVariable . getFullName ( ) ; if ( liftedVariable . getUnit ( ) ) { chartTitleWithUnit += ` [${ liftedVariable . getUnit ( ) }]` ; } this . sendLinePlotData ( previewPanel , { planIndex : planIndex , name : chartTitleWithUnit , unit : liftedVariable . getUnit ( ) , legend : values . legend , data : values . values } ) ; } ) ; for ( let metricIndex = 0 ; metricIndex < plan . problem . getMetrics ( ) . length ; metricIndex ++ ) { const metric = plan . problem . getMetrics ( ) [ metricIndex ] ; const metricExpression = metric . getExpression ( ) ; if ( metricExpression instanceof VariableExpression ) { if ( metricExpression . name === "total-time" ) { continue ; } } const metricValues = await evaluator . evaluateExpression ( metric . getExpression ( ) ) ; const chartTitle = metric . getDocumentation ( ) [ metric . getDocumentation ( ) . length - 1 ] ?? "Metric" ; this . sendLinePlotData ( previewPanel , { planIndex : planIndex , name : chartTitle , unit : "" , legend : [ '' ] , data : metricValues . values } ) ; } } catch ( err ) { console . error ( err ) ; const valStepPath = evaluator . getValStepPath ( ) ; if ( valStepPath ) { handleValStepError ( err , valStepPath ) ; } } } else { previewPanel . getPanel ( ) . webview . postMessage ( { "command" : "setVisibility" , "elementId" : "downloadVal" , "visible" : true } ) ; } } else { previewPanel . getPanel ( ) . webview . postMessage ( { "command" : "setVisibility" , "elementId" : "domainProblemAssociation" , "visible" : true } ) ; } } finally { previewPanel . getPanel ( ) . webview . postMessage ( { "command" : "hideLinePlotLoadingProgress" , "planIndex" : planIndex } ) ; } } sendLinePlotData ( previewPanel , data ) { previewPanel . getPanel ( ) . webview . postMessage ( { "command" : "showLinePlot" , "data" : data } ) ; } async getFinalStateData ( previewPanel , planIndex ) { if ( planIndex >= previewPanel . getPlans ( ) . length ) { console . error ( `requesting data for plan index {} while there are only {} plans` , planIndex , previewPanel . getPlans ( ) . length ) ; return ; } const plan = previewPanel . getPlans ( ) [ planIndex ] ; const valStepPath = ensureAbsoluteGlobalStoragePath ( workspace . getConfiguration ( CONF_PDDL ) . get < string > ( VAL_STEP_PATH ) , this . context ) ; const valVerbose = workspace . getConfiguration ( CONF_PDDL ) . get < boolean > ( VAL_VERBOSE , false ) ; if ( plan . domain && plan . problem ) { try { const finalStateValues = await new PlanEvaluator ( ) . evaluatePlan ( plan , { valStepPath , verbose : valVerbose } ) ; if ( finalStateValues ) { const data = { finalState : finalStateValues . map ( tvv => tvv . getVariableValue ( ) ) , planIndex : planIndex } ; previewPanel . getPanel ( ) . webview . postMessage ( { "command" : "visualizeFinalState" , "data" : data } ) ; } } catch ( err ) { console . error ( err ) ; if ( valStepPath ) { handleValStepError ( err , valStepPath ) ; } } } } async showMenu ( previewPanel ) { await new Menu ( [ { label : "$(browser) Generate plan report" , detail : "Creates a self-contained HTML file and opens it in a default browser" , command : PDDL_GENERATE_PLAN_REPORT , args : [ previewPanel . getPlans ( ) , previewPanel . getSelectedPlanIndex ( ) ] } , { label : "$(file) Export as .plan file..." , detail : "Opens a file picker to confirm the name and location of the plan file." , command : PDDL_EXPORT_PLAN , args : [ previewPanel . getSelectedPlan ( ) ] } , { label : "$(beaker) Creates PDDL test with expected test assertion" , detail : "Saves this plan into PDDL Test as expected plan assertion." , command : PDDL_SAVE_AS_EXPECTED_PLAN , args : [ previewPanel . getSelectedPlan ( ) ] } , { label : "$(arrow-both) Change width..." , detail : "Select the width for the plan preview (in pixels)" , command : PDDL_CONFIGURE_COMMAND , args : [ CONF_PDDL + '.' + PLAN_REPORT_WIDTH ] } , { label : "$(arrow-both) $(browser) Change report width..." , detail : "Select the width of the next exported report (in pixels)" , command : PDDL_CONFIGURE_COMMAND , args : [ "pddl.planReport.exportWidth" ] } ] , { placeHolder : 'Select an action...' } ) . show ( ) ; } } async function getPlanDocument ( dotDocumentUri ) { if ( dotDocumentUri ) { return await workspace . openTextDocument ( dotDocumentUri ) ; } else { if ( window . activeTextEditor !== undefined && isPlan ( window . activeTextEditor . document ) ) { return window . activeTextEditor . document ; } else { return undefined ; } } } class PlanPreviewPanel { private needsRebuild = false ; private width = 200 ; selectedPlanIndex = 0 ; plans : Plan [ ] = [ ] ; error : Error | undefined ; constructor ( public uri , private panel ) { } setWidth ( width ) { this . width = width ; } getWidth ( ) { return this . width ; } setSelectedPlanIndex ( selectedPlan ) { this . selectedPlanIndex = selectedPlan ; } getSelectedPlanIndex ( ) { return this . selectedPlanIndex ; } getSelectedPlan ( ) { if ( this . plans . length > 0 ) { return this . plans [ this . selectedPlanIndex ] ; } else { return undefined ; } } setPlans ( plans ) { this . plans = plans ; this . selectedPlanIndex = plans ? plans . length - 1 : 0 ; this . error = undefined ; this . setNeedsRebuild ( true ) ; } setError ( ex ) { this . error = ex ; } getError ( ) { return this . error ; } getPlans ( ) { return this . plans ; } reveal ( displayColumn ? ) { this . panel . reveal ( displayColumn ) ; } setNeedsRebuild ( needsRebuild ) { this . needsRebuild = needsRebuild ; } getNeedsRebuild ( ) { return this . needsRebuild ; } getPanel ( ) { return this . panel ; } }