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..066bf5917 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,15 @@ 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; + /// /// Converts all alternate path separators to the current platform's main path separators. /// @@ -36,6 +46,32 @@ 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)) + { + 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.