Skip to content

Commit a914690

Browse files
rkeithhilldaviwil
authored andcommitted
Fix for vscode-powershell #837 callstack missing line info.
This PR addresses two bugs. First the missing line info was due to use passing back null when we didn't have end line/col info. The protocol claims that should work. But when I changed the end line/col to reflect the same values as the start line/col (assuming the end values were null - not set) then the callstack started to display the line info. It is a bit weird but the very first frame in some cases contains a col other than 0. The second bug was that the call stack was getting duplicated. I implemented the new stackTraceRequest for paging stack frames back to VSCode and now that bug is fixed. I tried to implement StackFrame.PresentationHint == "subtle" for script outside the initial working directory. Do you have a better way to get the workspaceRoot path in the debugger? I had to cache the working dir when SetWorkingDirectory() gets called. And when you attach to a process, there isn't a way to set the "cwd". Finally, I ran into one bug I'll file later that has to do with the base of the call stack sometimes showing "<ScriptBlock> <No File> 1". If you click on that you get an error telling you <No File> can't be opened.
1 parent 5a336b0 commit a914690

File tree

6 files changed

+141
-26
lines changed

6 files changed

+141
-26
lines changed

src/PowerShellEditorServices.Protocol/DebugAdapter/StackFrame.cs

+40-14
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,57 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter
77
{
88
public class StackFrame
99
{
10+
/// <summary>
11+
/// Gets or sets an identifier for the stack frame. It must be unique across all threads.
12+
/// This id can be used to retrieve the scopes of the frame with the 'scopesRequest' or
13+
/// to restart the execution of a stackframe. */
14+
/// </summary>
1015
public int Id { get; set; }
1116

17+
/// <summary>
18+
/// Gets or sets the name of the stack frame, typically a method name
19+
/// </summary>
1220
public string Name { get; set; }
1321

22+
/// <summary>
23+
/// Gets or sets the optional source of the frame.
24+
/// </summary>
1425
public Source Source { get; set; }
1526

27+
/// <summary>
28+
/// Gets or sets line within the file of the frame. If source is null or doesn't exist,
29+
/// line is 0 and must be ignored.
30+
/// </summary>
1631
public int Line { get; set; }
1732

33+
/// <summary>
34+
/// Gets or sets an optional end line of the range covered by the stack frame.
35+
/// </summary>
1836
public int? EndLine { get; set; }
1937

38+
/// <summary>
39+
/// Gets or sets the column within the line. If source is null or doesn't exist,
40+
/// column is 0 and must be ignored.
41+
/// </summary>
2042
public int Column { get; set; }
2143

44+
/// <summary>
45+
/// Gets or sets an optional end column of the range covered by the stack frame.
46+
/// </summary>
2247
public int? EndColumn { get; set; }
2348

24-
// /** An identifier for the stack frame. */
25-
//id: number;
26-
///** The name of the stack frame, typically a method name */
27-
//name: string;
28-
///** The source of the frame. */
29-
//source: Source;
30-
///** The line within the file of the frame. */
31-
//line: number;
32-
///** The column within the line. */
33-
//column: number;
34-
///** All arguments and variables declared in this stackframe. */
35-
//scopes: Scope[];
49+
/// <summary>
50+
/// Gets the module associated with this frame, if any.
51+
/// </summary>
52+
public object ModuleId { get; set; }
53+
54+
/// <summary>
55+
/// Gets an optional hint for how to present this frame in the UI. A value of 'label'
56+
/// can be used to indicate that the frame is an artificial frame that is used as a
57+
/// visual label or separator. A value of 'subtle' can be used to change the appearance
58+
/// of a frame in a 'subtle' way.
59+
/// </summary>
60+
public string PresentationHint { get; private set; }
3661

3762
public static StackFrame Create(
3863
StackFrameDetails stackFrame,
@@ -43,9 +68,10 @@ public static StackFrame Create(
4368
Id = id,
4469
Name = stackFrame.FunctionName,
4570
Line = stackFrame.StartLineNumber,
46-
EndLine = stackFrame.EndLineNumber > 0 ? (int?)stackFrame.EndLineNumber : null,
71+
EndLine = stackFrame.EndLineNumber,
4772
Column = stackFrame.StartColumnNumber,
48-
EndColumn = stackFrame.EndColumnNumber > 0 ? (int?)stackFrame.EndColumnNumber : null,
73+
EndColumn = stackFrame.EndColumnNumber,
74+
PresentationHint = stackFrame.PresentationHint.ToString().ToLower(),
4975
Source = new Source
5076
{
5177
Path = stackFrame.ScriptPath

src/PowerShellEditorServices.Protocol/DebugAdapter/StackTraceRequest.cs

+25-3
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,38 @@ public static readonly
1818
[DebuggerDisplay("ThreadId = {ThreadId}, Levels = {Levels}")]
1919
public class StackTraceRequestArguments
2020
{
21-
public int ThreadId { get; private set; }
21+
/// <summary>
22+
/// Gets or sets the ThreadId of this stacktrace.
23+
/// </summary>
24+
public int ThreadId { get; set; }
2225

2326
/// <summary>
24-
/// Gets the maximum number of frames to return. If levels is not specified or 0, all frames are returned.
27+
/// Gets or sets the index of the first frame to return. If omitted frames start at 0.
2528
/// </summary>
26-
public int Levels { get; private set; }
29+
public int? StartFrame { get; set; }
30+
31+
/// <summary>
32+
/// Gets or sets the maximum number of frames to return. If levels is not specified or 0, all frames are returned.
33+
/// </summary>
34+
public int? Levels { get; set; }
35+
36+
/// <summary>
37+
/// Gets or sets the format string that specifies details on how to format the stack frames.
38+
/// </summary>
39+
public string Format { get; set; }
2740
}
2841

2942
public class StackTraceResponseBody
3043
{
44+
/// <summary>
45+
/// Gets the frames of the stackframe. If the array has length zero, there are no stackframes available.
46+
/// This means that there is no location information available.
47+
/// </summary>
3148
public StackFrame[] StackFrames { get; set; }
49+
50+
/// <summary>
51+
/// Gets the total number of frames available.
52+
/// </summary>
53+
public int? TotalFrames { get; set; }
3254
}
3355
}

src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs

+14-2
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,18 @@ protected async Task HandleStackTraceRequest(
643643

644644
List<StackFrame> newStackFrames = new List<StackFrame>();
645645

646-
for (int i = 0; i < stackFrames.Length; i++)
646+
int startFrameIndex = stackTraceParams.StartFrame ?? 0;
647+
int maxFrameCount = stackFrames.Length;
648+
649+
// If the number of requested levels == 0 (or null), that means get all stack frames
650+
// after the specified startFrame index. Otherwise get all the stack frames.
651+
int requestedFrameCount = (stackTraceParams.Levels ?? 0);
652+
if (requestedFrameCount > 0)
653+
{
654+
maxFrameCount = Math.Min(maxFrameCount, startFrameIndex + requestedFrameCount);
655+
}
656+
657+
for (int i = startFrameIndex; i < maxFrameCount; i++)
647658
{
648659
// Create the new StackFrame object with an ID that can
649660
// be referenced back to the current list of stack frames
@@ -656,7 +667,8 @@ protected async Task HandleStackTraceRequest(
656667
await requestContext.SendResult(
657668
new StackTraceResponseBody
658669
{
659-
StackFrames = newStackFrames.ToArray()
670+
StackFrames = newStackFrames.ToArray(),
671+
TotalFrames = newStackFrames.Count
660672
});
661673
}
662674

src/PowerShellEditorServices/Debugging/DebugService.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -842,8 +842,12 @@ private async Task FetchStackFrames(string scriptNameOverride)
842842
VariableContainerDetails localVariables =
843843
await FetchVariableContainer(i.ToString(), autoVariables);
844844

845+
// When debugging, this is the best way I can find to get what is likely the workspace root.
846+
// This is controlled by the "cwd:" setting in the launch config.
847+
string workspaceRootPath = this.powerShellContext.InitialWorkingDirectory;
848+
845849
this.stackFrameDetails[i] =
846-
StackFrameDetails.Create(callStackFrames[i], autoVariables, localVariables);
850+
StackFrameDetails.Create(callStackFrames[i], autoVariables, localVariables, workspaceRootPath);
847851

848852
string stackFrameScriptPath = this.stackFrameDetails[i].ScriptPath;
849853
if (scriptNameOverride != null &&

src/PowerShellEditorServices/Debugging/StackFrameDetails.cs

+49-6
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,30 @@
33
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
44
//
55

6+
using System;
67
using System.Management.Automation;
78

89
namespace Microsoft.PowerShell.EditorServices
910
{
11+
public enum StackFramePresentationHint
12+
{
13+
/// <summary>
14+
/// Dispays the stack frame as a normal stack frame.
15+
/// </summary>
16+
Normal,
17+
18+
/// <summary>
19+
/// Used to label an entry in the call stack that doesn't actually correspond to a stack frame.
20+
/// This is typically used to label transitions to/from "external" code.
21+
/// </summary>
22+
Label,
23+
24+
/// <summary>
25+
/// Displays the stack frame in a subtle way, typically used from loctaions outside of the current project or workspace.
26+
/// </summary>
27+
Subtle
28+
}
29+
1030
/// <summary>
1131
/// Contains details pertaining to a single stack frame in
1232
/// the current debugging session.
@@ -43,7 +63,7 @@ public class StackFrameDetails
4363
/// <summary>
4464
/// Gets the line number of the script where the stack frame occurred.
4565
/// </summary>
46-
public int EndLineNumber { get; internal set; }
66+
public int? EndLineNumber { get; internal set; }
4767

4868
/// <summary>
4969
/// Gets the start column number of the line where the stack frame occurred.
@@ -53,7 +73,12 @@ public class StackFrameDetails
5373
/// <summary>
5474
/// Gets the end column number of the line where the stack frame occurred.
5575
/// </summary>
56-
public int EndColumnNumber { get; internal set; }
76+
public int? EndColumnNumber { get; internal set; }
77+
78+
/// <summary>
79+
/// Gets a hint value that determines how the stack frame should be displayed.
80+
/// </summary>
81+
public StackFramePresentationHint PresentationHint { get; internal set; }
5782

5883
/// <summary>
5984
/// Gets or sets the VariableContainerDetails that contains the auto variables.
@@ -86,16 +111,34 @@ public class StackFrameDetails
86111
static internal StackFrameDetails Create(
87112
PSObject callStackFrameObject,
88113
VariableContainerDetails autoVariables,
89-
VariableContainerDetails localVariables)
114+
VariableContainerDetails localVariables,
115+
string workspaceRootPath = null)
90116
{
117+
string moduleId = string.Empty;
118+
var presentationHint = StackFramePresentationHint.Normal;
119+
120+
var invocationInfo = callStackFrameObject.Properties["InvocationInfo"].Value as InvocationInfo;
121+
string scriptPath = (callStackFrameObject.Properties["ScriptName"].Value as string) ?? NoFileScriptPath;
122+
int startLineNumber = (int)(callStackFrameObject.Properties["ScriptLineNumber"].Value ?? 0);
123+
124+
if (workspaceRootPath != null &&
125+
invocationInfo != null &&
126+
!scriptPath.StartsWith(workspaceRootPath, StringComparison.OrdinalIgnoreCase))
127+
{
128+
presentationHint = StackFramePresentationHint.Subtle;
129+
}
130+
91131
return new StackFrameDetails
92132
{
93-
ScriptPath = (callStackFrameObject.Properties["ScriptName"].Value as string) ?? NoFileScriptPath,
133+
ScriptPath = scriptPath,
94134
FunctionName = callStackFrameObject.Properties["FunctionName"].Value as string,
95-
StartLineNumber = (int)(callStackFrameObject.Properties["ScriptLineNumber"].Value ?? 0),
135+
StartLineNumber = startLineNumber,
136+
EndLineNumber = startLineNumber, // End line number isn't given in PowerShell stack frames
96137
StartColumnNumber = 0, // Column number isn't given in PowerShell stack frames
138+
EndColumnNumber = 0,
97139
AutoVariables = autoVariables,
98-
LocalVariables = localVariables
140+
LocalVariables = localVariables,
141+
PresentationHint = presentationHint
99142
};
100143
}
101144

src/PowerShellEditorServices/Session/PowerShellContext.cs

+8
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ public RunspaceDetails CurrentRunspace
103103
private set;
104104
}
105105

106+
/// <summary>
107+
/// Gets the working directory path the PowerShell context was inititially set when the debugger launches.
108+
/// This path is used to determine whether a script in the call stack is an "external" script.
109+
/// </summary>
110+
public string InitialWorkingDirectory { get; private set; }
111+
106112
#endregion
107113

108114
#region Constructors
@@ -1073,6 +1079,8 @@ internal void ReleaseRunspaceHandle(RunspaceHandle runspaceHandle)
10731079
/// <param name="path"></param>
10741080
public async Task SetWorkingDirectory(string path)
10751081
{
1082+
this.InitialWorkingDirectory = path;
1083+
10761084
using (RunspaceHandle runspaceHandle = await this.GetRunspaceHandle())
10771085
{
10781086
runspaceHandle.Runspace.SessionStateProxy.Path.SetLocation(path);

0 commit comments

Comments
 (0)