@@ -109,9 +109,18 @@ class PhpDebugSession extends vscode.DebugSession {
109109 /** A map from unique stackframe IDs (even across connections) to XDebug stackframes */
110110 private _stackFrames = new Map < number , xdebug . StackFrame > ( ) ;
111111
112+ /** A map from XDebug connections to their current status */
113+ private _statuses = new Map < xdebug . Connection , xdebug . StatusResponse > ( ) ;
114+
112115 /** A counter for unique context, property and eval result properties (as these are all requested by a VariableRequest from VS Code) */
113116 private _variableIdCounter = 1 ;
114117
118+ /** A map from unique VS Code variable IDs to XDebug statuses for virtual error stack frames */
119+ private _errorStackFrames = new Map < number , xdebug . StatusResponse > ( ) ;
120+
121+ /** A map from unique VS Code variable IDs to XDebug statuses for virtual error scopes */
122+ private _errorScopes = new Map < number , xdebug . StatusResponse > ( ) ;
123+
115124 /** A map from unique VS Code variable IDs to an XDebug contexts */
116125 private _contexts = new Map < number , xdebug . Context > ( ) ;
117126
@@ -203,6 +212,7 @@ class PhpDebugSession extends vscode.DebugSession {
203212 /** Checks the status of a StatusResponse and notifies VS Code accordingly */
204213 private _checkStatus ( response : xdebug . StatusResponse ) : void {
205214 const connection = response . connection ;
215+ this . _statuses . set ( connection , response ) ;
206216 if ( response . status === 'stopping' ) {
207217 connection . sendStopCommand ( ) . then ( response => this . _checkStatus ( response ) ) ;
208218 } else if ( response . status === 'stopped' ) {
@@ -427,31 +437,59 @@ class PhpDebugSession extends vscode.DebugSession {
427437 // this._stackFrames.clear();
428438 // this._properties.clear();
429439 // this._contexts.clear();
430- response . body = {
431- stackFrames : xdebugResponse . stack . map ( stackFrame => {
432- let source : vscode . Source ;
433- let line = stackFrame . line ;
434- const urlObject = url . parse ( stackFrame . fileUri ) ;
435- if ( urlObject . protocol === 'dbgp:' ) {
436- const sourceReference = this . _sourceIdCounter ++ ;
437- this . _sources . set ( sourceReference , { connection, url : stackFrame . fileUri } ) ;
438- // for eval code, we need to include .php extension to get syntax highlighting
439- source = new vscode . Source ( stackFrame . type === 'eval' ? 'eval.php' : stackFrame . name , null , sourceReference , stackFrame . type ) ;
440- // for eval code, we add a "<?php" line at the beginning to get syntax highlighting (see sourceRequest)
441- line ++ ;
442- } else {
443- // XDebug paths are URIs, VS Code file paths
444- const filePath = this . convertDebuggerPathToClient ( urlObject ) ;
445- // "Name" of the source and the actual file path
446- source = new vscode . Source ( path . basename ( filePath ) , filePath ) ;
447- }
448- // a new, unique ID for scopeRequests
449- const stackFrameId = this . _stackFrameIdCounter ++ ;
450- // save the connection this stackframe belongs to and the level of the stackframe under the stacktrace id
451- this . _stackFrames . set ( stackFrameId , stackFrame ) ;
452- // prepare response for VS Code (column is always 1 since XDebug doesn't tell us the column)
453- return new vscode . StackFrame ( stackFrameId , stackFrame . name , source , line , 1 ) ;
454- } )
440+ const status = this . _statuses . get ( connection ) ;
441+ if ( xdebugResponse . stack . length === 0 && status . exception ) {
442+ // special case: if a fatal error occurs (for example after an uncaught exception), the stack trace is EMPTY.
443+ // in that case, VS Code would normally not show any information to the user at all
444+ // to avoid this, we create a virtual stack frame with the info from the last status response we got
445+ const status = this . _statuses . get ( connection ) ;
446+ const id = this . _stackFrameIdCounter ++ ;
447+ const name = status . exception . name ;
448+ let line = status . line ;
449+ let source : vscode . Source ;
450+ const urlObject = url . parse ( status . fileUri ) ;
451+ if ( urlObject . protocol === 'dbgp:' ) {
452+ const sourceReference = this . _sourceIdCounter ++ ;
453+ this . _sources . set ( sourceReference , { connection, url : status . fileUri } ) ;
454+ // for eval code, we need to include .php extension to get syntax highlighting
455+ source = new vscode . Source ( status . exception . name + '.php' , null , sourceReference , status . exception . name ) ;
456+ // for eval code, we add a "<?php" line at the beginning to get syntax highlighting (see sourceRequest)
457+ line ++ ;
458+ } else {
459+ // XDebug paths are URIs, VS Code file paths
460+ const filePath = this . convertDebuggerPathToClient ( urlObject ) ;
461+ // "Name" of the source and the actual file path
462+ source = new vscode . Source ( path . basename ( filePath ) , filePath ) ;
463+ }
464+ this . _errorStackFrames . set ( id , status ) ;
465+ response . body = { stackFrames : [ new vscode . StackFrame ( id , name , source , status . line , 1 ) ] } ;
466+ } else {
467+ response . body = {
468+ stackFrames : xdebugResponse . stack . map ( stackFrame => {
469+ let source : vscode . Source ;
470+ let line = stackFrame . line ;
471+ const urlObject = url . parse ( stackFrame . fileUri ) ;
472+ if ( urlObject . protocol === 'dbgp:' ) {
473+ const sourceReference = this . _sourceIdCounter ++ ;
474+ this . _sources . set ( sourceReference , { connection, url : stackFrame . fileUri } ) ;
475+ // for eval code, we need to include .php extension to get syntax highlighting
476+ source = new vscode . Source ( stackFrame . type === 'eval' ? 'eval.php' : stackFrame . name , null , sourceReference , stackFrame . type ) ;
477+ // for eval code, we add a "<?php" line at the beginning to get syntax highlighting (see sourceRequest)
478+ line ++ ;
479+ } else {
480+ // XDebug paths are URIs, VS Code file paths
481+ const filePath = this . convertDebuggerPathToClient ( urlObject ) ;
482+ // "Name" of the source and the actual file path
483+ source = new vscode . Source ( path . basename ( filePath ) , filePath ) ;
484+ }
485+ // a new, unique ID for scopeRequests
486+ const stackFrameId = this . _stackFrameIdCounter ++ ;
487+ // save the connection this stackframe belongs to and the level of the stackframe under the stacktrace id
488+ this . _stackFrames . set ( stackFrameId , stackFrame ) ;
489+ // prepare response for VS Code (column is always 1 since XDebug doesn't tell us the column)
490+ return new vscode . StackFrame ( stackFrameId , stackFrame . name , source , line , 1 ) ;
491+ } )
492+ } ;
455493 }
456494 this . sendResponse ( response ) ;
457495 } )
@@ -474,73 +512,103 @@ class PhpDebugSession extends vscode.DebugSession {
474512 }
475513
476514 protected scopesRequest ( response : VSCodeDebugProtocol . ScopesResponse , args : VSCodeDebugProtocol . ScopesArguments ) : void {
477- const stackFrame = this . _stackFrames . get ( args . frameId ) ;
478- stackFrame . getContexts ( )
479- . then ( contexts => {
480- response . body = {
481- scopes : contexts . map ( context => {
515+ if ( this . _errorStackFrames . has ( args . frameId ) ) {
516+ // VS Code is requesting the scopes for a virtual error stack frame
517+ const status = this . _errorStackFrames . get ( args . frameId ) ;
518+ if ( status && status . exception ) {
519+ const variableId = this . _variableIdCounter ++ ;
520+ this . _errorScopes . set ( variableId , status ) ;
521+ response . body = { scopes : [ new vscode . Scope ( status . exception . name , variableId ) ] } ;
522+ }
523+ this . sendResponse ( response ) ;
524+ } else {
525+ const stackFrame = this . _stackFrames . get ( args . frameId ) ;
526+ stackFrame . getContexts ( )
527+ . then ( contexts => {
528+ response . body = {
529+ scopes : contexts . map ( context => {
530+ const variableId = this . _variableIdCounter ++ ;
531+ // remember that this new variable ID is assigned to a SCOPE (in XDebug "context"), not a variable (in XDebug "property"),
532+ // so when VS Code does a variablesRequest with that ID we do a context_get and not a property_get
533+ this . _contexts . set ( variableId , context ) ;
534+ // send VS Code the variable ID as identifier
535+ return new vscode . Scope ( context . name , variableId ) ;
536+ } )
537+ } ;
538+ const status = this . _statuses . get ( stackFrame . connection ) ;
539+ if ( status && status . exception ) {
482540 const variableId = this . _variableIdCounter ++ ;
483- // remember that this new variable ID is assigned to a SCOPE (in XDebug "context"), not a variable (in XDebug "property"),
484- // so when VS Code does a variablesRequest with that ID we do a context_get and not a property_get
485- this . _contexts . set ( variableId , context ) ;
486- // send VS Code the variable ID as identifier
487- return new vscode . Scope ( context . name , variableId ) ;
488- } )
489- } ;
490- this . sendResponse ( response ) ;
491- } )
492- . catch ( error => {
493- this . sendErrorResponse ( response , error . code , error . message ) ;
494- } ) ;
541+ this . _errorScopes . set ( variableId , status ) ;
542+ response . body . scopes . unshift ( new vscode . Scope ( status . exception . name , variableId ) ) ;
543+ }
544+ this . sendResponse ( response ) ;
545+ } )
546+ . catch ( error => {
547+ this . sendErrorResponse ( response , error . code , error . message ) ;
548+ } ) ;
549+ }
495550 }
496551
497552 protected variablesRequest ( response : VSCodeDebugProtocol . VariablesResponse , args : VSCodeDebugProtocol . VariablesArguments ) : void {
498553 const variablesReference = args . variablesReference ;
499- let propertiesPromise : Promise < xdebug . BaseProperty [ ] > ;
500- if ( this . _contexts . has ( variablesReference ) ) {
501- // VS Code is requesting the variables for a SCOPE, so we have to do a context_get
502- const context = this . _contexts . get ( variablesReference ) ;
503- propertiesPromise = context . getProperties ( ) ;
504- } else if ( this . _properties . has ( variablesReference ) ) {
505- // VS Code is requesting the subelements for a variable, so we have to do a property_get
506- const property = this . _properties . get ( variablesReference ) ;
507- propertiesPromise = property . hasChildren ? property . getChildren ( ) : Promise . resolve ( [ ] ) ;
508- } else if ( this . _evalResultProperties . has ( variablesReference ) ) {
509- // the children of properties returned from an eval command are always inlined, so we simply resolve them
510- const property = this . _evalResultProperties . get ( variablesReference ) ;
511- propertiesPromise = Promise . resolve ( property . hasChildren ? property . children : [ ] ) ;
554+ if ( this . _errorScopes . has ( variablesReference ) ) {
555+ // this is a virtual error scope
556+ const status = this . _errorScopes . get ( variablesReference ) ;
557+ response . body = {
558+ variables : [
559+ new vscode . Variable ( 'name' , '"' + status . exception . name + '"' ) ,
560+ new vscode . Variable ( 'message' , '"' + status . exception . message + '"' )
561+ ]
562+ } ;
563+ this . sendResponse ( response ) ;
512564 } else {
513- this . sendErrorResponse ( response , 0 , 'Unknown variable reference' ) ;
514- return ;
515- }
516- propertiesPromise
517- . then ( properties => {
518- response . body = {
519- variables : properties . map ( property => {
520- const displayValue = formatPropertyValue ( property ) ;
521- let variablesReference : number ;
522- if ( property . hasChildren || property . type === 'array' || property . type === 'object' ) {
523- // if the property has children, we have to send a variableReference back to VS Code
524- // so it can receive the child elements in another request.
525- // for arrays and objects we do it even when it does not have children so the user can still expand/collapse the entry
526- variablesReference = this . _variableIdCounter ++ ;
527- if ( property instanceof xdebug . Property ) {
528- this . _properties . set ( variablesReference , property ) ;
529- } else if ( property instanceof xdebug . EvalResultProperty ) {
530- this . _evalResultProperties . set ( variablesReference , property ) ;
565+ // it is a real scope
566+ let propertiesPromise : Promise < xdebug . BaseProperty [ ] > ;
567+ if ( this . _contexts . has ( variablesReference ) ) {
568+ // VS Code is requesting the variables for a SCOPE, so we have to do a context_get
569+ const context = this . _contexts . get ( variablesReference ) ;
570+ propertiesPromise = context . getProperties ( ) ;
571+ } else if ( this . _properties . has ( variablesReference ) ) {
572+ // VS Code is requesting the subelements for a variable, so we have to do a property_get
573+ const property = this . _properties . get ( variablesReference ) ;
574+ propertiesPromise = property . hasChildren ? property . getChildren ( ) : Promise . resolve ( [ ] ) ;
575+ } else if ( this . _evalResultProperties . has ( variablesReference ) ) {
576+ // the children of properties returned from an eval command are always inlined, so we simply resolve them
577+ const property = this . _evalResultProperties . get ( variablesReference ) ;
578+ propertiesPromise = Promise . resolve ( property . hasChildren ? property . children : [ ] ) ;
579+ } else {
580+ this . sendErrorResponse ( response , 0 , 'Unknown variable reference' ) ;
581+ return ;
582+ }
583+ propertiesPromise
584+ . then ( properties => {
585+ response . body = {
586+ variables : properties . map ( property => {
587+ const displayValue = formatPropertyValue ( property ) ;
588+ let variablesReference : number ;
589+ if ( property . hasChildren || property . type === 'array' || property . type === 'object' ) {
590+ // if the property has children, we have to send a variableReference back to VS Code
591+ // so it can receive the child elements in another request.
592+ // for arrays and objects we do it even when it does not have children so the user can still expand/collapse the entry
593+ variablesReference = this . _variableIdCounter ++ ;
594+ if ( property instanceof xdebug . Property ) {
595+ this . _properties . set ( variablesReference , property ) ;
596+ } else if ( property instanceof xdebug . EvalResultProperty ) {
597+ this . _evalResultProperties . set ( variablesReference , property ) ;
598+ }
599+ } else {
600+ variablesReference = 0 ;
531601 }
532- } else {
533- variablesReference = 0 ;
534- }
535- return new vscode . Variable ( property . name , displayValue , variablesReference ) ;
536- } )
537- }
538- this . sendResponse ( response ) ;
539- } )
540- . catch ( error => {
541- console . error ( util . inspect ( error ) ) ;
542- this . sendErrorResponse ( response , error . code , error . message ) ;
543- } )
602+ return new vscode . Variable ( property . name , displayValue , variablesReference ) ;
603+ } )
604+ }
605+ this . sendResponse ( response ) ;
606+ } )
607+ . catch ( error => {
608+ console . error ( util . inspect ( error ) ) ;
609+ this . sendErrorResponse ( response , error . code , error . message ) ;
610+ } ) ;
611+ }
544612 }
545613
546614 protected continueRequest ( response : VSCodeDebugProtocol . ContinueResponse , args : VSCodeDebugProtocol . ContinueArguments ) : void {
0 commit comments