@@ -41,6 +41,7 @@ internal class DebugService
4141
4242 private readonly IPowerShellDebugContext _debugContext ;
4343
44+ // The LSP protocol refers to variables by individual IDs, this is an iterator for that purpose.
4445 private int nextVariableId ;
4546 private string temporaryScriptListingPath ;
4647 private List < VariableDetailsBase > variables ;
@@ -654,7 +655,12 @@ private async Task FetchStackFramesAndVariablesAsync(string scriptNameOverride)
654655 }
655656 }
656657
657- private async Task < VariableContainerDetails > FetchVariableContainerAsync ( string scope )
658+ private Task < VariableContainerDetails > FetchVariableContainerAsync ( string scope )
659+ {
660+ return FetchVariableContainerAsync ( scope , autoVarsOnly : false ) ;
661+ }
662+
663+ private async Task < VariableContainerDetails > FetchVariableContainerAsync ( string scope , bool autoVarsOnly )
658664 {
659665 PSCommand psCommand = new PSCommand ( )
660666 . AddCommand ( "Get-Variable" )
@@ -692,8 +698,17 @@ private async Task<VariableContainerDetails> FetchVariableContainerAsync(string
692698 {
693699 continue ;
694700 }
701+ var variableInfo = TryVariableInfo ( psVariableObject ) ;
702+ if ( variableInfo is null || ! ShouldAddAsVariable ( variableInfo ) )
703+ {
704+ continue ;
705+ }
706+ if ( autoVarsOnly && ! ShouldAddToAutoVariables ( variableInfo ) )
707+ {
708+ continue ;
709+ }
695710
696- var variableDetails = new VariableDetails ( psVariableObject ) { Id = nextVariableId ++ } ;
711+ var variableDetails = new VariableDetails ( variableInfo . Variable ) { Id = nextVariableId ++ } ;
697712 variables . Add ( variableDetails ) ;
698713 scopeVariableContainer . Children . Add ( variableDetails . Name , variableDetails ) ;
699714 }
@@ -702,70 +717,95 @@ private async Task<VariableContainerDetails> FetchVariableContainerAsync(string
702717 return scopeVariableContainer ;
703718 }
704719
705- // TODO: This function needs explanation, thought, and improvement.
706- private bool AddToAutoVariables ( PSObject psvariable )
720+ // This is a helper type for FetchStackFramesAsync to preserve the variable Type after deserialization.
721+ private record VariableInfo ( string [ ] Types , PSVariable Variable ) ;
722+
723+ // Create a VariableInfo for both serialized and deserialized variables.
724+ private static VariableInfo TryVariableInfo ( PSObject psObject )
707725 {
708- string variableName = psvariable . Properties [ "Name" ] . Value as string ;
709- object variableValue = psvariable . Properties [ "Value" ] . Value ;
726+ if ( psObject . TypeNames . Contains ( "System.Management.Automation.PSVariable" ) )
727+ {
728+ return new VariableInfo ( psObject . TypeNames . ToArray ( ) , psObject . BaseObject as PSVariable ) ;
729+ }
730+ if ( psObject . TypeNames . Contains ( "Deserialized.System.Management.Automation.PSVariable" ) )
731+ {
732+ // Rehydrate the relevant variable properties and recreate it.
733+ ScopedItemOptions options = ( ScopedItemOptions ) Enum . Parse ( typeof ( ScopedItemOptions ) , psObject . Properties [ "Options" ] . Value . ToString ( ) ) ;
734+ PSVariable reconstructedVar = new (
735+ psObject . Properties [ "Name" ] . Value . ToString ( ) ,
736+ psObject . Properties [ "Value" ] . Value ,
737+ options
738+ ) ;
739+ return new VariableInfo ( psObject . TypeNames . ToArray ( ) , reconstructedVar ) ;
740+ }
710741
711- // Don't put any variables created by PSES in the Auto variable container.
712- if ( variableName . StartsWith ( PsesGlobalVariableNamePrefix )
713- || variableName . Equals ( "PSDebugContext" ) )
742+ return null ;
743+ }
744+
745+ /// <summary>
746+ /// Filters out variables we don't care about such as built-ins
747+ /// </summary>
748+ private static bool ShouldAddAsVariable ( VariableInfo variableInfo )
749+ {
750+ // Filter built-in constant or readonly variables like $true, $false, $null, etc.
751+ ScopedItemOptions variableScope = variableInfo . Variable . Options ;
752+ var constantAllScope = ScopedItemOptions . AllScope | ScopedItemOptions . Constant ;
753+ var readonlyAllScope = ScopedItemOptions . AllScope | ScopedItemOptions . ReadOnly ;
754+ if ( ( ( variableScope & constantAllScope ) == constantAllScope )
755+ || ( ( variableScope & readonlyAllScope ) == readonlyAllScope ) )
714756 {
715757 return false ;
716758 }
717759
718- ScopedItemOptions variableScope = ScopedItemOptions . None ;
719- PSPropertyInfo optionsProperty = psvariable . Properties [ "Options" ] ;
720- if ( string . Equals ( optionsProperty . TypeNameOfValue , "System.String" ) )
760+ if ( variableInfo . Variable . Name switch { "null" => true , _ => false } )
721761 {
722- if ( ! Enum . TryParse < ScopedItemOptions > (
723- optionsProperty . Value as string ,
724- out variableScope ) )
725- {
726- _logger . LogWarning ( $ "Could not parse a variable's ScopedItemOptions value of '{ optionsProperty . Value } '") ;
727- }
762+ return false ;
728763 }
729- else if ( optionsProperty . Value is ScopedItemOptions )
764+
765+ return true ;
766+ }
767+
768+ // This method curates variables that should be added to the "auto" view, which we define as variables that are
769+ // very likely to be contextually relevant to the user, in an attempt to reduce noise when debugging.
770+ // Variables not listed here can still be found in the other containers like local and script, this is
771+ // provided as a convenience.
772+ private bool ShouldAddToAutoVariables ( VariableInfo variableInfo )
773+ {
774+ var variableToAdd = variableInfo . Variable ;
775+ if ( ! ShouldAddAsVariable ( variableInfo ) )
730776 {
731- variableScope = ( ScopedItemOptions ) optionsProperty . Value ;
777+ return false ;
732778 }
733779
734- // Some local variables, if they exist, should be displayed by default
735- if ( psvariable . TypeNames [ 0 ] . EndsWith ( "LocalVariable" ) )
780+ // Filter internal variables created by Powershell Editor Services.
781+ if ( variableToAdd . Name . StartsWith ( PsesGlobalVariableNamePrefix )
782+ || variableToAdd . Name . Equals ( "PSDebugContext" ) )
736783 {
737- if ( variableName . Equals ( "PSItem" ) || variableName . Equals ( "_" ) )
738- {
739- return true ;
740- }
741- else if ( variableName . Equals ( "args" , StringComparison . OrdinalIgnoreCase ) )
742- {
743- return variableValue is Array array && array . Length > 0 ;
744- }
745-
746784 return false ;
747785 }
748- else if ( ! psvariable . TypeNames [ 0 ] . EndsWith ( nameof ( PSVariable ) ) )
786+
787+ // Filter Global-Scoped variables. We first cast to VariableDetails to ensure the prefix
788+ // is added for purposes of comparison.
789+ VariableDetails variableToAddDetails = new ( variableToAdd ) ;
790+ if ( globalScopeVariables . Children . ContainsKey ( variableToAddDetails . Name ) )
749791 {
750792 return false ;
751793 }
752794
753- var constantAllScope = ScopedItemOptions . AllScope | ScopedItemOptions . Constant ;
754- var readonlyAllScope = ScopedItemOptions . AllScope | ScopedItemOptions . ReadOnly ;
755-
756- if ( ( ( variableScope & constantAllScope ) == constantAllScope )
757- || ( ( variableScope & readonlyAllScope ) == readonlyAllScope ) )
795+ // We curate a list of LocalVariables that, if they exist, should be displayed by default.
796+ if ( variableInfo . Types [ 0 ] . EndsWith ( "LocalVariable" ) )
758797 {
759- // The constructor we are using here does not automatically add the dollar prefix,
760- // so we do it manually.
761- string prefixedVariableName = VariableDetails . DollarPrefix + variableName ;
762- if ( globalScopeVariables . Children . ContainsKey ( prefixedVariableName ) )
798+ return variableToAdd . Name switch
763799 {
764- return false ;
765- }
800+ "PSItem" or "_" or "" => true ,
801+ "args" or "input" => variableToAdd . Value is Array array && array . Length > 0 ,
802+ "PSBoundParameters" => variableToAdd . Value is IDictionary dict && dict . Count > 0 ,
803+ _ => false
804+ } ;
766805 }
767806
768- return true ;
807+ // Any other PSVariables that survive the above criteria should be included.
808+ return variableInfo . Types [ 0 ] . EndsWith ( "PSVariable" ) ;
769809 }
770810
771811 private async Task FetchStackFramesAsync ( string scriptNameOverride )
@@ -798,8 +838,10 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
798838 : results ;
799839
800840 var stackFrameDetailList = new List < StackFrameDetails > ( ) ;
841+ bool isTopStackFrame = true ;
801842 foreach ( var callStackFrameItem in callStack )
802843 {
844+ // We have to use reflection to get the variable dictionary.
803845 var callStackFrameComponents = ( callStackFrameItem as PSObject ) . BaseObject as IList ;
804846 var callStackFrame = callStackFrameComponents [ 0 ] as PSObject ;
805847 IDictionary callStackVariables = isOnRemoteMachine
@@ -820,27 +862,47 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
820862
821863 foreach ( DictionaryEntry entry in callStackVariables )
822864 {
823- // TODO: This should be deduplicated into a new function.
824- object psVarValue = isOnRemoteMachine
825- ? ( entry . Value as PSObject ) . Properties [ "Value" ] . Value
826- : ( entry . Value as PSVariable ) . Value ;
827-
828- // The constructor we are using here does not automatically add the dollar
829- // prefix, so we do it manually.
830- string psVarName = VariableDetails . DollarPrefix + entry . Key . ToString ( ) ;
831- var variableDetails = new VariableDetails ( psVarName , psVarValue ) { Id = nextVariableId ++ } ;
865+ VariableInfo psVarInfo = TryVariableInfo ( new PSObject ( entry . Value ) ) ;
866+ if ( psVarInfo is null )
867+ {
868+ _logger . LogError ( $ "A object was received that is not a PSVariable object") ;
869+ continue ;
870+ }
871+
872+ var variableDetails = new VariableDetails ( psVarInfo . Variable ) { Id = nextVariableId ++ } ;
832873 variables . Add ( variableDetails ) ;
833874
834875 commandVariables . Children . Add ( variableDetails . Name , variableDetails ) ;
835- if ( AddToAutoVariables ( new PSObject ( entry . Value ) ) )
876+
877+ if ( ShouldAddToAutoVariables ( psVarInfo ) )
836878 {
837879 autoVariables . Children . Add ( variableDetails . Name , variableDetails ) ;
838880 }
839881 }
840882
841- var stackFrameDetailsEntry = StackFrameDetails . Create ( callStackFrame , autoVariables , commandVariables ) ;
883+ // If this is the top stack frame, we also want to add relevant local variables to
884+ // the "Auto" container (not to be confused with Automatic PowerShell variables).
885+ //
886+ // TODO: We can potentially use `Get-Variable -Scope x` to add relevant local
887+ // variables to other frames but frames and scopes are not perfectly analagous and
888+ // we'd need a way to detect things such as module borders and dot-sourced files.
889+ if ( isTopStackFrame )
890+ {
891+ var localScopeAutoVariables = await FetchVariableContainerAsync ( VariableContainerDetails . LocalScopeName , autoVarsOnly : true ) . ConfigureAwait ( false ) ;
892+ foreach ( KeyValuePair < string , VariableDetailsBase > entry in localScopeAutoVariables . Children )
893+ {
894+ // NOTE: `TryAdd` doesn't work on `IDictionary`.
895+ if ( ! autoVariables . Children . ContainsKey ( entry . Key ) )
896+ {
897+ autoVariables . Children . Add ( entry . Key , entry . Value ) ;
898+ }
899+ }
900+ isTopStackFrame = false ;
901+ }
842902
903+ var stackFrameDetailsEntry = StackFrameDetails . Create ( callStackFrame , autoVariables , commandVariables ) ;
843904 string stackFrameScriptPath = stackFrameDetailsEntry . ScriptPath ;
905+
844906 if ( scriptNameOverride is not null
845907 && string . Equals ( stackFrameScriptPath , StackFrameDetails . NoFileScriptPath ) )
846908 {
0 commit comments