@@ -41,6 +41,7 @@ internal class DebugService
41
41
42
42
private readonly IPowerShellDebugContext _debugContext ;
43
43
44
+ // The LSP protocol refers to variables by individual IDs, this is an iterator for that purpose.
44
45
private int nextVariableId ;
45
46
private string temporaryScriptListingPath ;
46
47
private List < VariableDetailsBase > variables ;
@@ -654,7 +655,12 @@ private async Task FetchStackFramesAndVariablesAsync(string scriptNameOverride)
654
655
}
655
656
}
656
657
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 )
658
664
{
659
665
PSCommand psCommand = new PSCommand ( )
660
666
. AddCommand ( "Get-Variable" )
@@ -692,8 +698,17 @@ private async Task<VariableContainerDetails> FetchVariableContainerAsync(string
692
698
{
693
699
continue ;
694
700
}
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
+ }
695
710
696
- var variableDetails = new VariableDetails ( psVariableObject ) { Id = nextVariableId ++ } ;
711
+ var variableDetails = new VariableDetails ( variableInfo . Variable ) { Id = nextVariableId ++ } ;
697
712
variables . Add ( variableDetails ) ;
698
713
scopeVariableContainer . Children . Add ( variableDetails . Name , variableDetails ) ;
699
714
}
@@ -702,70 +717,95 @@ private async Task<VariableContainerDetails> FetchVariableContainerAsync(string
702
717
return scopeVariableContainer ;
703
718
}
704
719
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 )
707
725
{
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
+ }
710
741
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 ) )
714
756
{
715
757
return false ;
716
758
}
717
759
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 } )
721
761
{
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 ;
728
763
}
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 ) )
730
776
{
731
- variableScope = ( ScopedItemOptions ) optionsProperty . Value ;
777
+ return false ;
732
778
}
733
779
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" ) )
736
783
{
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
-
746
784
return false ;
747
785
}
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 ) )
749
791
{
750
792
return false ;
751
793
}
752
794
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" ) )
758
797
{
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
763
799
{
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
+ } ;
766
805
}
767
806
768
- return true ;
807
+ // Any other PSVariables that survive the above criteria should be included.
808
+ return variableInfo . Types [ 0 ] . EndsWith ( "PSVariable" ) ;
769
809
}
770
810
771
811
private async Task FetchStackFramesAsync ( string scriptNameOverride )
@@ -798,8 +838,10 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
798
838
: results ;
799
839
800
840
var stackFrameDetailList = new List < StackFrameDetails > ( ) ;
841
+ bool isTopStackFrame = true ;
801
842
foreach ( var callStackFrameItem in callStack )
802
843
{
844
+ // We have to use reflection to get the variable dictionary.
803
845
var callStackFrameComponents = ( callStackFrameItem as PSObject ) . BaseObject as IList ;
804
846
var callStackFrame = callStackFrameComponents [ 0 ] as PSObject ;
805
847
IDictionary callStackVariables = isOnRemoteMachine
@@ -820,27 +862,47 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
820
862
821
863
foreach ( DictionaryEntry entry in callStackVariables )
822
864
{
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 ++ } ;
832
873
variables . Add ( variableDetails ) ;
833
874
834
875
commandVariables . Children . Add ( variableDetails . Name , variableDetails ) ;
835
- if ( AddToAutoVariables ( new PSObject ( entry . Value ) ) )
876
+
877
+ if ( ShouldAddToAutoVariables ( psVarInfo ) )
836
878
{
837
879
autoVariables . Children . Add ( variableDetails . Name , variableDetails ) ;
838
880
}
839
881
}
840
882
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
+ }
842
902
903
+ var stackFrameDetailsEntry = StackFrameDetails . Create ( callStackFrame , autoVariables , commandVariables ) ;
843
904
string stackFrameScriptPath = stackFrameDetailsEntry . ScriptPath ;
905
+
844
906
if ( scriptNameOverride is not null
845
907
&& string . Equals ( stackFrameScriptPath , StackFrameDetails . NoFileScriptPath ) )
846
908
{
0 commit comments