From 523eb9738dd272ab66f06632cb3afa901f42a99f Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Tue, 16 Aug 2022 16:32:36 -0400 Subject: [PATCH 1/2] Add artifical stack frame to represent context When stepping into certain contexts like a param block's default value expression, the engine does not provide a call stack frame to represent it. In this scenario we want to create an artifical call stack frame to represent the context we've stepped into. --- .../Services/DebugAdapter/DebugService.cs | 43 +++++++++++++++++-- .../Debugging/StackFrameDetails.cs | 6 +-- .../Utility/PathUtils.cs | 22 ++++++++++ 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index 906b9fe4d..0002245cb 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -994,10 +994,45 @@ await _remoteFileManager.FetchRemoteFileAsync( // Augment the top stack frame with details from the stop event if (invocationTypeScriptPositionProperty.GetValue(e.InvocationInfo) is IScriptExtent scriptExtent) { - stackFrameDetails[0].StartLineNumber = scriptExtent.StartLineNumber; - stackFrameDetails[0].EndLineNumber = scriptExtent.EndLineNumber; - stackFrameDetails[0].StartColumnNumber = scriptExtent.StartColumnNumber; - stackFrameDetails[0].EndColumnNumber = scriptExtent.EndColumnNumber; + StackFrameDetails targetFrame = stackFrameDetails[0]; + + // Certain context changes (like stepping into the default value expression + // of a parameter) do not create a call stack frame. In order to represent + // this context change we create a fake call stack frame. + if (!string.IsNullOrEmpty(scriptExtent.File) + && !PathUtils.IsPathEqual(scriptExtent.File, targetFrame.ScriptPath)) + { + await debugInfoHandle.WaitAsync().ConfigureAwait(false); + try + { + targetFrame = new StackFrameDetails + { + ScriptPath = scriptExtent.File, + // Just use the last frame's variables since we don't have a + // good way to get real values. + AutoVariables = targetFrame.AutoVariables, + CommandVariables = targetFrame.CommandVariables, + // Ideally we'd get a real value here but since there's no real + // call stack frame for this, we'd need to replicate a lot of + // engine code. + FunctionName = "", + }; + + StackFrameDetails[] newFrames = new StackFrameDetails[stackFrameDetails.Length + 1]; + newFrames[0] = targetFrame; + stackFrameDetails.CopyTo(newFrames, 1); + stackFrameDetails = newFrames; + } + finally + { + debugInfoHandle.Release(); + } + } + + targetFrame.StartLineNumber = scriptExtent.StartLineNumber; + targetFrame.EndLineNumber = scriptExtent.EndLineNumber; + targetFrame.StartColumnNumber = scriptExtent.StartColumnNumber; + targetFrame.EndColumnNumber = scriptExtent.EndColumnNumber; } } diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/StackFrameDetails.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/StackFrameDetails.cs index 6a8704df8..c188f33e4 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/StackFrameDetails.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/StackFrameDetails.cs @@ -31,7 +31,7 @@ internal class StackFrameDetails /// /// Gets the name of the function where the stack frame occurred. /// - public string FunctionName { get; private set; } + public string FunctionName { get; internal init; } /// /// Gets the start line number of the script where the stack frame occurred. @@ -62,12 +62,12 @@ internal class StackFrameDetails /// /// Gets or sets the VariableContainerDetails that contains the auto variables. /// - public VariableContainerDetails AutoVariables { get; private set; } + public VariableContainerDetails AutoVariables { get; internal init; } /// /// Gets or sets the VariableContainerDetails that contains the call stack frame variables. /// - public VariableContainerDetails CommandVariables { get; private set; } + public VariableContainerDetails CommandVariables { get; internal init; } #endregion diff --git a/src/PowerShellEditorServices/Utility/PathUtils.cs b/src/PowerShellEditorServices/Utility/PathUtils.cs index 568a92156..e5f7b85e4 100644 --- a/src/PowerShellEditorServices/Utility/PathUtils.cs +++ b/src/PowerShellEditorServices/Utility/PathUtils.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.IO; using System.Management.Automation; using System.Runtime.InteropServices; @@ -29,6 +30,10 @@ internal static class PathUtils /// internal static readonly char AlternatePathSeparator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? '/' : '\\'; + internal static readonly StringComparison PathComparison = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + ? StringComparison.Ordinal + : StringComparison.OrdinalIgnoreCase; + /// /// Converts all alternate path separators to the current platform's main path separators. /// @@ -36,6 +41,23 @@ internal static class PathUtils /// The normalized path. public static string NormalizePathSeparators(string path) => string.IsNullOrWhiteSpace(path) ? path : path.Replace(AlternatePathSeparator, DefaultPathSeparator); + internal static bool IsPathEqual(string left, string right) + { + if (string.IsNullOrEmpty(left)) + { + return string.IsNullOrEmpty(right); + } + + if (string.IsNullOrEmpty(right)) + { + return false; + } + + left = Path.GetFullPath(left).TrimEnd(DefaultPathSeparator); + right = Path.GetFullPath(right).TrimEnd(DefaultPathSeparator); + return left.Equals(right, PathComparison); + } + /// /// Return the given path with all PowerShell globbing characters escaped, /// plus optionally the whitespace. From ef28cd40b9acc8122ba0e0b2f34f9fcb4ff18725 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Tue, 16 Aug 2022 16:47:27 -0400 Subject: [PATCH 2/2] Add doc comments to new `PathUtils` members --- src/PowerShellEditorServices/Utility/PathUtils.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/PowerShellEditorServices/Utility/PathUtils.cs b/src/PowerShellEditorServices/Utility/PathUtils.cs index e5f7b85e4..066bf5917 100644 --- a/src/PowerShellEditorServices/Utility/PathUtils.cs +++ b/src/PowerShellEditorServices/Utility/PathUtils.cs @@ -30,6 +30,11 @@ internal static class PathUtils /// internal static readonly char AlternatePathSeparator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? '/' : '\\'; + /// + /// The value to be used when comparing paths. Will be + /// for case sensitive file systems and + /// in case insensitive file systems. + /// internal static readonly StringComparison PathComparison = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; @@ -41,6 +46,15 @@ internal static class PathUtils /// The normalized path. public static string NormalizePathSeparators(string path) => string.IsNullOrWhiteSpace(path) ? path : path.Replace(AlternatePathSeparator, DefaultPathSeparator); + /// + /// Determines whether two specified strings represent the same path. + /// + /// The first path to compare, or . + /// The second path to compare, or . + /// + /// if the value of represents the same + /// path as the value of ; otherwise, . + /// internal static bool IsPathEqual(string left, string right) { if (string.IsNullOrEmpty(left))