diff --git a/.gitignore b/.gitignore
index f73bd5823..d74b9f73b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,3 +77,4 @@ module/PowerShellEditorServices/Third\ Party\ Notices.txt
# JetBrains generated file (Rider, intelliJ)
.idea/
+nupkgs/
diff --git a/PowerShellEditorServices.Common.props b/PowerShellEditorServices.Common.props
index 80d45b7ba..563a917ba 100644
--- a/PowerShellEditorServices.Common.props
+++ b/PowerShellEditorServices.Common.props
@@ -14,8 +14,9 @@
true
- true
- true
+ false
+ false
+ false
true
diff --git a/global.json b/global.json
index 4c4c3ae5e..6a288505a 100644
--- a/global.json
+++ b/global.json
@@ -1,7 +1,7 @@
{
"sdk": {
- "version": "8.0.100",
- "rollForward": "latestFeature",
+ "version": "10.0.100",
+ "rollForward": "latestMajor",
"allowPrerelease": false
}
}
diff --git a/src/PowerShellEditorServices/Server/PsesDebugServer.cs b/src/PowerShellEditorServices/Server/PsesDebugServer.cs
index 6aca82e04..428ca0119 100644
--- a/src/PowerShellEditorServices/Server/PsesDebugServer.cs
+++ b/src/PowerShellEditorServices/Server/PsesDebugServer.cs
@@ -51,8 +51,10 @@ public PsesDebugServer(
/// A task that completes when the server is ready.
public async Task StartAsync()
{
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] PsesDebugServer.StartAsync entered\n");
_debugAdapterServer = await DebugAdapterServer.From(options =>
{
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] DebugAdapterServer.From callback\n");
// We need to let the PowerShell Context Service know that we are in a debug session
// so that it doesn't send the powerShell/startDebugger message.
_psesHost = ServiceProvider.GetService();
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugEventHandlerService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugEventHandlerService.cs
index e3237a3b0..4ad656311 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugEventHandlerService.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugEventHandlerService.cs
@@ -66,6 +66,7 @@ internal void UnregisterEventHandlers()
private void OnDebuggerStopped(object sender, DebuggerStoppedEventArgs e)
{
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] OnDebuggerStopped: reason={e.OriginalEvent.Breakpoints.Count} breakpoints\n");
// Provide the reason for why the debugger has stopped script execution.
// See https://github.com/Microsoft/vscode/issues/3648
// The reason is displayed in the breakpoints viewlet. Some recommended reasons are:
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs
index 90faceda4..5edbab79e 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs
@@ -49,6 +49,7 @@ internal class DebugService
private VariableContainerDetails globalScopeVariables;
private VariableContainerDetails scriptScopeVariables;
private VariableContainerDetails localScopeVariables;
+ private List immyScopeVariables = new();
private StackFrameDetails[] stackFrameDetails;
private readonly PropertyInfo invocationTypeScriptPositionProperty;
@@ -596,7 +597,7 @@ public VariableScope[] GetVariableScopes(int stackFrameId)
int autoVariablesId = stackFrames[stackFrameId].AutoVariables.Id;
int commandVariablesId = stackFrames[stackFrameId].CommandVariables.Id;
- return new VariableScope[]
+ var scopes = new List
{
new VariableScope(autoVariablesId, VariableContainerDetails.AutoVariablesName),
new VariableScope(commandVariablesId, VariableContainerDetails.CommandVariablesName),
@@ -604,6 +605,14 @@ public VariableScope[] GetVariableScopes(int stackFrameId)
new VariableScope(scriptScopeVariables.Id, VariableContainerDetails.ScriptScopeName),
new VariableScope(globalScopeVariables.Id, VariableContainerDetails.GlobalScopeName),
};
+
+ // Add Immy-specific variable scopes (e.g. "Script", "RunContext", "Action", "Session")
+ foreach (var immyScope in immyScopeVariables)
+ {
+ scopes.Add(new VariableScope(immyScope.Id, immyScope.Name));
+ }
+
+ return scopes.ToArray();
}
#endregion
@@ -629,6 +638,9 @@ private async Task FetchStackFramesAndVariablesAsync(string scriptNameOverride)
localScopeVariables = await FetchVariableContainerAsync(VariableContainerDetails.LocalScopeName).ConfigureAwait(false);
await FetchStackFramesAsync(scriptNameOverride).ConfigureAwait(false);
+
+ // Build Immy-specific variable scopes from VariableLayerAttribute on each PSVariable.
+ FetchImmyVariableScopesAsync();
}
finally
{
@@ -689,6 +701,78 @@ private async Task FetchVariableContainerAsync(string
return scopeVariableContainer;
}
+ ///
+ /// Reads VariableLayerAttribute from each variable's Attributes collection (set by
+ /// ImmyBot's MetascriptRunspaceExtensions) and organizes variables into Immy-specific
+ /// scope containers like "Script", "RunContext", "Action", "Session", etc.
+ /// These are not real PowerShell scopes but logical groupings that show
+ /// which ImmyBot layer injected each variable.
+ ///
+ private void FetchImmyVariableScopesAsync()
+ {
+ immyScopeVariables.Clear();
+
+ // Scan all fetched scope variables for VariableLayerAttribute.
+ // We check by type name since PSES can't reference ImmyBot's attribute class.
+ const string layerAttrTypeName = "VariableLayerAttribute";
+
+ var layerGroups = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+
+ void ScanScope(VariableContainerDetails scope)
+ {
+ foreach (var child in scope.Children.Values)
+ {
+ // VariableDetails wraps a PSVariable; check its Attributes.
+ if (child is VariableDetails varDetails && varDetails.PSVariable is PSVariable psVar)
+ {
+ foreach (var attr in psVar.Attributes)
+ {
+ if (attr.GetType().Name.Equals(layerAttrTypeName, StringComparison.Ordinal))
+ {
+ string layer = attr.GetType().GetProperty("Layer")?.GetValue(attr)?.ToString() ?? "Unknown";
+ if (!layerGroups.TryGetValue(layer, out var list))
+ {
+ list = new List();
+ layerGroups[layer] = list;
+ }
+ list.Add(child);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ ScanScope(globalScopeVariables);
+ ScanScope(scriptScopeVariables);
+ ScanScope(localScopeVariables);
+
+ foreach (var layerGroup in layerGroups)
+ {
+ string layerName = layerGroup.Key;
+
+ VariableContainerDetails layerContainer = new(nextVariableId++, layerName);
+ variables.Add(layerContainer);
+
+ foreach (var varDetails in layerGroup.Value)
+ {
+ if (!layerContainer.Children.ContainsKey(varDetails.Name))
+ {
+ layerContainer.Children.Add(varDetails.Name, varDetails);
+ }
+ }
+
+ if (layerContainer.Children.Count > 0)
+ {
+ immyScopeVariables.Add(layerContainer);
+ }
+ else
+ {
+ variables.Remove(layerContainer);
+ }
+ }
+ }
+
// This is a helper type for FetchStackFramesAsync to preserve the variable Type after deserialization.
private record VariableInfo(string[] Types, PSVariable Variable);
@@ -756,10 +840,11 @@ private bool ShouldAddToAutoVariables(VariableInfo variableInfo)
return false;
}
- // Filter Global-Scoped variables. We first cast to VariableDetails to ensure the prefix
- // is added for purposes of comparison.
- VariableDetails variableToAddDetails = new(variableToAdd);
- if (globalScopeVariables.Children.ContainsKey(variableToAddDetails.Name))
+ // Filter well-known PowerShell built-in/preference variables to reduce noise.
+ // We used to filter everything in the global scope, but that also filtered
+ // user-defined variables that ended up in the global scope (e.g. from
+ // dot-sourced scripts). Instead, we now only filter known noise variables.
+ if (s_builtInVariableNames.Contains(variableToAdd.Name))
{
return false;
}
@@ -769,10 +854,13 @@ private bool ShouldAddToAutoVariables(VariableInfo variableInfo)
{
return variableToAdd.Name switch
{
- "PSItem" or "_" or "" => true,
+ // Skip empty/nothing variables
+ null or "" or "_" => false,
+ // Only show args/input if they have content
"args" or "input" => variableToAdd.Value is Array array && array.Length > 0,
"PSBoundParameters" => variableToAdd.Value is IDictionary dict && dict.Count > 0,
- _ => false
+ // Show all other local variables (e.g. $a, $result, $computer)
+ _ => true
};
}
@@ -780,6 +868,23 @@ private bool ShouldAddToAutoVariables(VariableInfo variableInfo)
return variableInfo.Types[0].EndsWith("PSVariable");
}
+ private static readonly HashSet s_builtInVariableNames = new(StringComparer.OrdinalIgnoreCase)
+ {
+ "ConfirmPreference", "DebugPreference", "Error", "ErrorActionPreference", "ErrorView",
+ "ExecutionContext", "FormatEnumerationLimit", "HOME", "Host", "InformationPreference",
+ "input", "MaximumHistoryCount", "MyInvocation", "NestedPromptLevel", "OutputEncoding",
+ "PID", "PROFILE", "ProgressPreference", "PSBoundParameters", "PSCommandPath",
+ "PSCulture", "PSDebugContext", "PSDefaultParameterValues", "PSEmailServer",
+ "PSHome", "PSItem", "PSLogUserProfile", "PSModuleAutoLoadingPreference",
+ "PSModulePath", "PSNativeCommandArgumentPassing", "PSNativeCommandUseErrorActionPreference",
+ "PSScriptRoot", "PSSessionConfigurationName", "PSSessionOption", "PSStyle",
+ "PSUICulture", "PSVersionTable", "PWD", "ShellId", "StackTrace",
+ "VerbosePreference", "WarningPreference", "WhatIfPreference", "^", "$",
+ "?", "true", "false", "null", "args", "PSCommand", "PSPath",
+ "ForEach", "Where", "psEditor", "ImmyBotVersion", "ImmyScriptPath",
+ "CanAccessParentTenant", "__psEditorServices_CallStack",
+ };
+
private async Task FetchStackFramesAsync(string scriptNameOverride)
{
// This glorious hack ensures that Get-PSCallStack returns a list of CallStackFrame
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs
index f67230af5..29bbe681f 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs
@@ -29,6 +29,11 @@ internal class VariableDetails : VariableDetailsBase
protected object ValueObject { get; }
private VariableDetails[] cachedChildren;
+ ///
+ /// The original PSVariable if this was created from one, used for attribute inspection.
+ ///
+ public PSVariable PSVariable { get; }
+
#endregion
#region Constructors
@@ -43,6 +48,7 @@ internal class VariableDetails : VariableDetailsBase
public VariableDetails(PSVariable psVariable)
: this(DollarPrefix + psVariable.Name, psVariable.Value)
{
+ PSVariable = psVariable;
}
///
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs
index 4c99ff747..e29bcc0ae 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs
@@ -49,6 +49,7 @@ public BreakpointHandlers(
public async Task Handle(SetBreakpointsArguments request, CancellationToken cancellationToken)
{
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] BreakpointHandlers.Handle called, path='{request.Source.Path}'\n");
if (!_workspaceService.TryGetFile(request.Source.Path, out ScriptFile scriptFile))
{
string message = _debugStateService.NoDebug ? string.Empty : "Source file could not be accessed, breakpoint not set.";
@@ -82,31 +83,60 @@ public async Task Handle(SetBreakpointsArguments request
};
}
- // At this point, the source file has been verified as a PowerShell script.
+ // Use the DocumentUri directly — the frontend now uses pspath:// URIs everywhere,
+ // so no normalization is needed.
+ string breakpointScriptPath = scriptFile.DocumentUri.ToString();
IReadOnlyList breakpointDetails = request.Breakpoints
.Select((srcBreakpoint) => BreakpointDetails.Create(
- scriptFile.FilePath,
+ breakpointScriptPath,
srcBreakpoint.Line,
srcBreakpoint.Column,
srcBreakpoint.Condition,
srcBreakpoint.HitCondition,
srcBreakpoint.LogMessage)).ToList();
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] BreakpointHandlers: FilePath='{scriptFile.FilePath}', DocumentUri='{scriptFile.DocumentUri}', breakpointScriptPath='{breakpointScriptPath}', #breakpoints={breakpointDetails.Count}, line={breakpointDetails[0].LineNumber}\n");
+
// If this is a "run without debugging (Ctrl+F5)" session ignore requests to set breakpoints.
IReadOnlyList updatedBreakpointDetails = breakpointDetails;
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] BreakpointHandlers: NoDebug={_debugStateService.NoDebug}\n");
if (!_debugStateService.NoDebug)
{
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] BreakpointHandlers: calling WaitForSetBreakpointHandleAsync\n");
await _debugStateService.WaitForSetBreakpointHandleAsync().ConfigureAwait(false);
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] BreakpointHandlers: calling SetLineBreakpointsAsync\n");
try
{
+ // The debugger's DebugMode may be Default or RemoteScript, neither of
+ // which support SetLineBreakpoint. Use reflection to set it to Local so
+ // breakpoints can be registered before the script launches.
+ var debugger = _runspaceContext.CurrentRunspace.Runspace.Debugger;
+ var debugModeProp = typeof(System.Management.Automation.Debugger).GetProperty("DebugMode");
+ var currentMode = (System.Management.Automation.DebugModes)debugModeProp!.GetValue(debugger)!;
+ if ((currentMode & System.Management.Automation.DebugModes.LocalScript) == 0)
+ {
+ debugModeProp.SetValue(debugger, currentMode | System.Management.Automation.DebugModes.LocalScript);
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] BreakpointHandlers: added LocalScript to DebugMode (was {currentMode})\n");
+ }
+
updatedBreakpointDetails =
await _debugService.SetLineBreakpointsAsync(
scriptFile,
breakpointDetails).ConfigureAwait(false);
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] BreakpointHandlers: SetLineBreakpointsAsync returned {updatedBreakpointDetails.Count} breakpoints, verified={updatedBreakpointDetails.FirstOrDefault()?.Verified}\n");
+
+ // Re-call SetDebugMode after breakpoints are registered. The debugger's
+ // SetDebugMode internally checks if _idToBreakpoint is non-empty and sets
+ // _context._debuggingMode to Enabled. If SetDebugMode was called before
+ // breakpoints were added, _debuggingMode stays 0 and breakpoints are never
+ // checked during execution.
+ debugger.SetDebugMode(System.Management.Automation.DebugModes.LocalScript | System.Management.Automation.DebugModes.RemoteScript);
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] BreakpointHandlers: re-called SetDebugMode after breakpoint registration\n");
}
catch (Exception e)
{
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] BreakpointHandlers: EXCEPTION: {e}\n");
// Log whatever the error is
_logger.LogException($"Caught error while setting breakpoints in SetBreakpoints handler for file {scriptFile?.FilePath}", e);
}
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs
index df8165319..59e21fe3c 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs
@@ -119,24 +119,15 @@ internal async Task LaunchScriptAsync(string scriptToLaunch)
bool isScriptFile = _workspaceService.TryGetFile(scriptToLaunch, out ScriptFile untitledScript);
if (isScriptFile && BreakpointApiUtils.SupportsBreakpointApis(_runspaceContext.CurrentRunspace))
{
- // Parse untitled files with their `Untitled:` URI as the filename which will
- // cache the URI and contents within the PowerShell parser. By doing this, we
- // light up the ability to debug untitled files with line breakpoints. This is
- // only possible with PowerShell 7's new breakpoint APIs since the old API,
- // Set-PSBreakpoint, validates that the given path points to a real file.
+ // Use the DocumentUri directly — the frontend now uses pspath:// URIs everywhere.
+ string scriptUri = untitledScript.DocumentUri.ToString();
+
ScriptBlockAst ast = Parser.ParseInput(
untitledScript.Contents,
- untitledScript.DocumentUri.ToString(),
+ scriptUri,
out Token[] _,
out ParseError[] _);
- // In order to use utilize the parser's cache (and therefore hit line
- // breakpoints) we need to use the AST's `ScriptBlock` object. Due to
- // limitations in PowerShell's public API, this means we must use the
- // `PSCommand.AddArgument(object)` method, hence this hack where we dot-source
- // `$args[0]. Fortunately the dot-source operator maintains a stack of arguments
- // on each invocation, so passing the user's arguments directly in the initial
- // `AddScript` surprisingly works.
command = PSCommandHelpers
.BuildDotSourceCommandWithArguments("$args[0]", _debugStateService?.Arguments)
.AddArgument(ast.GetScriptBlock());
@@ -155,10 +146,72 @@ internal async Task LaunchScriptAsync(string scriptToLaunch)
}
}
- await _executionService.ExecutePSCommandAsync(
- command,
- CancellationToken.None,
- s_debuggerExecutionOptions).ConfigureAwait(false);
+ // Fix: Set _debuggingMode on the TLS (Thread Local Storage) execution context.
+ // The debugger checks _debuggingMode from the TLS context during script execution,
+ // not from the runspace's debugger context. If TLS _debuggingMode is 0, the debugger
+ // skips breakpoint checks entirely, even though breakpoints are registered.
+ try
+ {
+ var localPipelineType = typeof(System.Management.Automation.Runspaces.Runspace).Assembly
+ .GetType("System.Management.Automation.Runspaces.LocalPipeline");
+ var getCtxMethod = localPipelineType?.GetMethod(
+ "GetExecutionContextFromTLS",
+ System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
+ var tlsContext = getCtxMethod?.Invoke(null, null);
+ if (tlsContext is not null)
+ {
+ var execContextType = tlsContext.GetType();
+ var debuggingModeField = execContextType.GetField(
+ "_debuggingMode",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ if (debuggingModeField is not null)
+ {
+ // DebugModes.LocalScript = 1
+ debuggingModeField.SetValue(tlsContext, (System.Management.Automation.DebugModes)1);
+ }
+ }
+ }
+ catch (System.Exception ex)
+ {
+ _logger.LogError(ex, "Failed to set TLS _debuggingMode");
+ }
+
+ // Fix: Ensure _context.CurrentRunspace is set on the debugger's ExecutionContext.
+ // In PSES with UseCurrentThread, the debugger's _context.CurrentRunspace can be null
+ // which causes OnDebuggerStop to crash with NullReferenceException.
+ try
+ {
+ var dbg = _runspaceContext.CurrentRunspace.Runspace.Debugger;
+ var dbgType = dbg.GetType();
+ var contextField = dbgType.GetField(
+ "_context", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ if (contextField is not null)
+ {
+ var context = contextField.GetValue(dbg);
+ var currentRunspaceProp = context?.GetType().GetProperty(
+ "CurrentRunspace", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ if (currentRunspaceProp is not null && currentRunspaceProp.GetValue(context) is null)
+ {
+ currentRunspaceProp.SetValue(context, _runspaceContext.CurrentRunspace.Runspace);
+ }
+ }
+ }
+ catch (System.Exception ex)
+ {
+ _logger.LogError(ex, "Failed to fix null CurrentRunspace on debugger._context");
+ }
+
+ try
+ {
+ await _executionService.ExecutePSCommandAsync(
+ command,
+ CancellationToken.None,
+ s_debuggerExecutionOptions).ConfigureAwait(false);
+ }
+ catch (System.Exception ex)
+ {
+ _logger.LogError(ex, "LaunchScriptAsync: ExecutePSCommandAsync threw");
+ }
_debugAdapterServer?.SendNotification(EventNames.Terminated);
}
diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs
index 700c154b9..ea057da7f 100644
--- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs
+++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs
@@ -124,6 +124,7 @@ public LaunchAndAttachHandler(
public async Task Handle(PsesLaunchRequestArguments request, CancellationToken cancellationToken)
{
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] LaunchAndAttachHandler.Handle called, Script='{request.Script}'\n");
// The debugger has officially started. We use this to later check if we should stop it.
((PsesInternalHost)_executionService).DebugContext.IsActive = true;
diff --git a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs
index 7c05b3da2..3936afa9f 100644
--- a/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs
+++ b/src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs
@@ -57,6 +57,8 @@ public SynchronousPowerShellTask(
public override IReadOnlyList Run(CancellationToken cancellationToken)
{
+ var cmdText = _psCommand.GetInvocationText();
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] SynchronousPowerShellTask.Run on thread {System.Environment.CurrentManagedThreadId}, command={cmdText.Substring(0, System.Math.Min(50, cmdText.Length))}\n");
_psesHost.Runspace.ThrowCancelledIfUnusable();
PowerShellContextFrame frame = _psesHost.PushPowerShellForExecution();
try
@@ -133,7 +135,103 @@ private IReadOnlyList ExecuteNormally(CancellationToken cancellationTok
invocationSettings.ErrorActionPreference = ActionPreference.Stop;
}
- result = _pwsh.InvokeCommand(_psCommand, invocationSettings);
+ // Check _debuggingMode right before Invoke
+ try {
+ var debugger = _pwsh.Runspace.Debugger;
+ var debuggerType = debugger.GetType();
+ var contextField = debuggerType.GetField("_context", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ if (contextField != null) {
+ var context = contextField.GetValue(debugger);
+ var execContextType = context.GetType();
+ var debuggingModeField = execContextType.GetField("_debuggingMode", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ if (debuggingModeField != null) {
+ var debuggingMode = debuggingModeField.GetValue(context);
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] ExecuteNormally: _context._debuggingMode={debuggingMode} BEFORE Invoke, InBreakpoint={debugger.InBreakpoint}\n");
+ }
+ // Check if Runspace.DefaultRunspace matches our runspace
+ var defaultRunspace = System.Management.Automation.Runspaces.Runspace.DefaultRunspace;
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] ExecuteNormally: Runspace.DefaultRunspace.Id={defaultRunspace?.Id}, _pwsh.Runspace.Id={_pwsh.Runspace.Id}, same={defaultRunspace == _pwsh.Runspace}\n");
+ if (defaultRunspace != null && defaultRunspace != _pwsh.Runspace) {
+ var ecProp = typeof(System.Management.Automation.Runspaces.Runspace).GetProperty("ExecutionContext", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ var tlsExecContext = ecProp?.GetValue(defaultRunspace);
+ var tlsDebugMode = execContextType.GetField("_debuggingMode", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(tlsExecContext);
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] ExecuteNormally: TLS _debuggingMode={tlsDebugMode}\n");
+ }
+ // Check if the ExecutionContext from the runspace matches the debugger's context
+ var ecProp2 = typeof(System.Management.Automation.Runspaces.Runspace).GetProperty("ExecutionContext", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ var runspaceEc = ecProp2?.GetValue(_pwsh.Runspace);
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] ExecuteNormally: runspace.ExecutionContext == debugger._context: {runspaceEc == context}\n");
+ // Check _idToBreakpoint
+ var idToBreakpointField = debuggerType.GetField("_idToBreakpoint", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ if (idToBreakpointField != null) {
+ var idToBreakpoint = idToBreakpointField.GetValue(debugger) as System.Collections.IDictionary;
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] ExecuteNormally: _idToBreakpoint count={idToBreakpoint?.Count}\n");
+ if (idToBreakpoint != null) {
+ foreach (System.Collections.DictionaryEntry entry in idToBreakpoint) {
+ var bp = entry.Value;
+ var bpType = bp.GetType();
+ var scriptProp = bpType.GetProperty("Script");
+ var lineProp = bpType.GetProperty("Line");
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] breakpoint: id={entry.Key}, script='{scriptProp?.GetValue(bp)}', line={lineProp?.GetValue(bp)}\n");
+ // Check SequencePointIndex and BreakpointBitArray
+ var spIndexProp = bpType.GetProperty("SequencePointIndex", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ if (spIndexProp != null) {
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] SequencePointIndex={spIndexProp.GetValue(bp)}\n");
+ }
+ var bpBitArrayProp = bpType.GetProperty("BreakpointBitArray", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ if (bpBitArrayProp != null) {
+ var bitArray = bpBitArrayProp.GetValue(bp) as System.Collections.BitArray;
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] BreakpointBitArray={(bitArray == null ? "null" : bitArray.Count + " bits")}\n");
+ }
+ }
+ }
+ }
+ // Check _pendingBreakpoints
+ var pendingBpField = debuggerType.GetField("_pendingBreakpoints", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ if (pendingBpField != null) {
+ var pendingBp = pendingBpField.GetValue(debugger) as System.Collections.IDictionary;
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] ExecuteNormally: _pendingBreakpoints count={pendingBp?.Count}\n");
+ if (pendingBp != null) {
+ foreach (System.Collections.DictionaryEntry entry in pendingBp) {
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] pending key='{entry.Key}'\n");
+ }
+ }
+ }
+ }
+ // Also log the command text
+ var cmdText = _psCommand.GetInvocationText();
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] ExecuteNormally: about to invoke '{cmdText.Substring(0, System.Math.Min(100, cmdText.Length))}'\n");
+ } catch (System.Exception ex) {
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] ExecuteNormally: reflection failed: {ex.Message}\n");
+ }
+
+ // Check CurrentRunspace before Invoke
+ try {
+ var dbg = _pwsh.Runspace.Debugger;
+ var dbgType = dbg.GetType();
+ var contextField = dbgType.GetField("_context", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ if (contextField != null) {
+ var context = contextField.GetValue(dbg);
+ var crProp = context?.GetType().GetProperty("CurrentRunspace", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ var cr = crProp?.GetValue(context);
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] ExecuteNormally: BEFORE Invoke CurrentRunspace={cr?.GetHashCode()}, isNull={cr == null}, ctxHash={context?.GetHashCode()}, thread={System.Environment.CurrentManagedThreadId}\n");
+ }
+ } catch { }
+
+ result = _pwsh.InvokeCommand(_psCommand, invocationSettings);
+
+ // Check CurrentRunspace after Invoke
+ try {
+ var dbg = _pwsh.Runspace.Debugger;
+ var dbgType = dbg.GetType();
+ var contextField = dbgType.GetField("_context", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ if (contextField != null) {
+ var context = contextField.GetValue(dbg);
+ var crProp = context?.GetType().GetProperty("CurrentRunspace", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ var cr = crProp?.GetValue(context);
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] ExecuteNormally: AFTER Invoke CurrentRunspace={cr?.GetHashCode()}, isNull={cr == null}, ctxHash={context?.GetHashCode()}, thread={System.Environment.CurrentManagedThreadId}\n");
+ }
+ } catch { }
cancellationToken.ThrowIfCancellationRequested();
}
// Allow terminate exceptions to propagate for flow control.
diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs
index 01d33bd42..0d1786386 100644
--- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs
+++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs
@@ -890,13 +890,13 @@ private void PushPowerShellAndMaybeRunLoop(PowerShellContextFrame frame, bool sk
try
{
- if (_psFrameStack.Count == 1)
+ if (frame.IsDebug)
{
- RunTopLevelExecutionLoop();
+ RunDebugExecutionLoop();
}
- else if (frame.IsDebug)
+ else if (_psFrameStack.Count == 1)
{
- RunDebugExecutionLoop();
+ RunTopLevelExecutionLoop();
}
else
{
@@ -1290,9 +1290,11 @@ private void InvokeInput(string input, CancellationToken cancellationToken)
private void AddRunspaceEventHandlers(Runspace runspace)
{
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] AddRunspaceEventHandlers: runspace Id={runspace.Id}, Debugger type={runspace.Debugger.GetType().Name}\n");
runspace.Debugger.DebuggerStop += OnDebuggerStopped;
runspace.Debugger.BreakpointUpdated += OnBreakpointUpdated;
runspace.StateChanged += OnRunspaceStateChanged;
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] AddRunspaceEventHandlers: DebuggerStop subscribed, DebugMode={runspace.Debugger.DebugMode}\n");
}
private void RemoveRunspaceEventHandlers(Runspace runspace)
@@ -1545,6 +1547,7 @@ internal void WaitForExternalDebuggerStops()
private void OnDebuggerStopped(object sender, DebuggerStopEventArgs debuggerStopEventArgs)
{
+ try { System.Console.Error.WriteLine("PSES_ON_DEBUGGER_STOPPED_CALLED"); System.IO.File.AppendAllText("/tmp/pses-debug.log", "[PSES] PSES_ON_DEBUGGER_STOPPED_CALLED\n"); } catch (System.Exception ex) { System.Console.Error.WriteLine($"PSES_ON_DEBUGGER_STOPPED_FAILED: {ex.Message}"); }
// If ErrorActionPreference is set to Break, any engine exception is going to trigger a
// pipeline stop. Technically this is the same behavior as a standalone PowerShell
// process, but we use pipeline stops with greater frequency due to features like run
@@ -1569,10 +1572,13 @@ private void OnDebuggerStopped(object sender, DebuggerStopEventArgs debuggerStop
if (triggerObject is PipelineStoppedException pse)
{
+ System.IO.File.AppendAllText("/tmp/pses-debug2.log", "[PSES] OnDebuggerStopped: throwing PipelineStoppedException\n");
throw pse;
}
}
+ System.IO.File.AppendAllText("/tmp/pses-debug.log", $"[PSES] OnDebuggerStopped: past trigger check, isExternal={Environment.CurrentManagedThreadId != _pipelineThread.ManagedThreadId}, thread={Environment.CurrentManagedThreadId}, pipelineThread={_pipelineThread.ManagedThreadId}\n");
+
// The debugger has officially started. We use this to later check if we should stop it.
DebugContext.IsActive = true;
diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DidChangeWatchedFilesHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DidChangeWatchedFilesHandler.cs
index edbe9b0ad..70cdc067b 100644
--- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DidChangeWatchedFilesHandler.cs
+++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DidChangeWatchedFilesHandler.cs
@@ -66,7 +66,11 @@ public Task Handle(DidChangeWatchedFilesParams request, CancellationToken
matcher.AddExcludePatterns(_workspaceService.ExcludeFilesGlob);
foreach (FileEvent change in request.Changes)
{
- if (matcher.Match(change.Uri.GetFileSystemPath()).HasMatches)
+ string changePath = change.Uri.ToUri().IsFile
+ ? change.Uri.GetFileSystemPath()
+ : change.Uri.ToUri().AbsolutePath;
+
+ if (matcher.Match(changePath).HasMatches)
{
continue;
}
@@ -100,7 +104,7 @@ public Task Handle(DidChangeWatchedFilesParams request, CancellationToken
string fileContents;
try
{
- fileContents = WorkspaceService.ReadFileContents(change.Uri);
+ fileContents = _workspaceService.ReadFileContents(change.Uri);
}
catch
{
diff --git a/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs b/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs
index fb6772d2b..a1eb7a4fb 100644
--- a/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs
+++ b/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs
@@ -183,12 +183,31 @@ internal static List GetLines(string text)
/// True if the path is an untitled file, false otherwise.
internal static bool IsUntitledPath(string path)
{
- Validate.IsNotNull(nameof(path), path);
- // This may not have been given a URI, so return false instead of throwing.
- return Uri.IsWellFormedUriString(path, UriKind.RelativeOrAbsolute) &&
- !string.Equals(DocumentUri.From(path).Scheme, Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase);
+ if (!Uri.IsWellFormedUriString(path, UriKind.RelativeOrAbsolute))
+ {
+ return false;
+ }
+
+ DocumentUri documentUri = DocumentUri.From(path);
+ string scheme = documentUri.Scheme?.ToLowerInvariant();
+ if (!IsSupportedScheme(scheme))
+ {
+ return false;
+ }
+
+ return scheme switch
+ {
+ "inmemory" or "untitled" or "vscode-notebook-cell" => true,
+ _ => false,
+ };
}
+ internal static bool IsSupportedScheme(string scheme) => scheme?.ToLowerInvariant() switch
+ {
+ "file" or "inmemory" or "untitled" or "vscode-notebook-cell" or "pspath" => true,
+ _ => false,
+ };
+
///
/// Gets a line from the file's contents.
///
diff --git a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs
index ad9b04552..7e1ee82e8 100644
--- a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs
+++ b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs
@@ -6,10 +6,16 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Management.Automation;
using System.Security;
using System.Text;
+using System.Threading;
+using Microsoft.Extensions.FileSystemGlobbing;
using Microsoft.Extensions.Logging;
+using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution;
+using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
+using Microsoft.PowerShell.EditorServices.Services.Workspace;
using Microsoft.PowerShell.EditorServices.Utility;
using OmniSharp.Extensions.LanguageServer.Protocol;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
@@ -24,15 +30,42 @@ internal class WorkspaceService
{
#region Private Fields
+ // List of all file extensions considered PowerShell files in the .Net Core Framework.
+ private static readonly string[] s_psFileExtensionsCoreFramework =
+ {
+ ".ps1",
+ ".psm1",
+ ".psd1"
+ };
+
+ // .Net Core doesn't appear to use the same three letter pattern matching rule although the docs
+ // suggest it should be find the '.ps1xml' files because we search for the pattern '*.ps1'.
+ // ref https://docs.microsoft.com/en-us/dotnet/api/system.io.directory.getfiles?view=netcore-2.1#System_IO_Directory_GetFiles_System_String_System_String_System_IO_EnumerationOptions_
+ private static readonly string[] s_psFileExtensionsFullFramework =
+ {
+ ".ps1",
+ ".psm1",
+ ".psd1",
+ ".ps1xml"
+ };
+
// An array of globs which includes everything.
private static readonly string[] s_psIncludeAllGlob = new[]
{
"**/*"
};
+ private const string s_psPathScheme = "pspath";
+
+ // The host component of pspath:// URIs for items from the ScriptPSProvider drive.
+ // PowerShell's PSPath uses the drive name (pspath:) but we need the provider name
+ // as the URI host to round-trip correctly through GetPowerShellPath.
+ private const string s_psPathProviderHost = "ScriptPSProvider";
+
private readonly ILogger logger;
private readonly Version powerShellVersion;
private readonly ConcurrentDictionary workspaceFiles = new();
+ private readonly PsesInternalHost psesInternalHost;
#endregion
@@ -79,11 +112,18 @@ public WorkspaceService(ILoggerFactory factory)
FollowSymlinks = true;
}
+ ///
+ /// Creates a new instance of the Workspace class backed by a PowerShell host.
+ ///
+ public WorkspaceService(ILoggerFactory factory, PsesInternalHost psesInternalHost)
+ : this(factory) => this.psesInternalHost = psesInternalHost;
+
#endregion
#region Public Methods
- public IEnumerable WorkspacePaths => WorkspaceFolders.Select(i => i.Uri.GetFileSystemPath());
+ public IEnumerable WorkspacePaths => WorkspaceFolders.Select(
+ folder => folder.Uri.ToUri().IsFile ? folder.Uri.GetFileSystemPath() : GetPowerShellPath(folder.Uri));
///
/// Gets an open file in the workspace. If the file isn't open but exists on the filesystem, load and return it.
@@ -118,18 +158,8 @@ public ScriptFile GetFile(DocumentUri documentUri)
// Make sure the file isn't already loaded into the workspace
if (!workspaceFiles.TryGetValue(keyName, out ScriptFile scriptFile))
{
- // This method allows FileNotFoundException to bubble up
- // if the file isn't found.
- using (StreamReader streamReader = OpenStreamReader(documentUri))
- {
- scriptFile =
- new ScriptFile(
- documentUri,
- streamReader,
- powerShellVersion);
-
- workspaceFiles[keyName] = scriptFile;
- }
+ scriptFile = ScriptFile.Create(documentUri, ReadFileContents(documentUri), powerShellVersion);
+ workspaceFiles[keyName] = scriptFile;
logger.LogDebug("Opened file on disk: " + documentUri.ToString());
}
@@ -171,18 +201,10 @@ public bool TryGetFile(Uri fileUri, out ScriptFile scriptFile) =>
/// The out parameter that will contain the ScriptFile object.
public bool TryGetFile(DocumentUri documentUri, out ScriptFile scriptFile)
{
- switch (documentUri.Scheme)
+ if (!ScriptFile.IsSupportedScheme(documentUri.Scheme))
{
- // List supported schemes here
- case "file":
- case "inmemory":
- case "untitled":
- case "vscode-notebook-cell":
- break;
-
- default:
- scriptFile = null;
- return false;
+ scriptFile = null;
+ return false;
}
try
@@ -281,7 +303,14 @@ public void CloseFile(ScriptFile scriptFile)
Validate.IsNotNull(nameof(scriptFile), scriptFile);
string keyName = GetFileKey(scriptFile.DocumentUri);
- workspaceFiles.TryRemove(keyName, out ScriptFile _);
+ if (workspaceFiles.TryRemove(keyName, out ScriptFile _))
+ {
+ logger.LogDebug("Closed file: " + scriptFile.DocumentUri);
+ }
+ else
+ {
+ logger.LogWarning("Tried to close file that was not open: " + scriptFile.DocumentUri);
+ }
}
///
@@ -314,6 +343,35 @@ public string GetRelativePath(ScriptFile scriptFile)
return fileUri.ToString();
}
+ ///
+ /// Finds a file in the first workspace folder where it exists, if possible.
+ /// Used as a backwards-compatible way to find files in the workspace.
+ ///
+ ///
+ /// Best possible path.
+ public string FindFileInWorkspace(string filePath)
+ {
+ // If the file path is already an absolute path, just return it.
+ if (Path.IsPathRooted(filePath))
+ {
+ return filePath;
+ }
+
+ // If the file path is relative, try to find it in the workspace folders.
+ foreach (WorkspaceFolder workspaceFolder in WorkspaceFolders)
+ {
+ string folderPath = workspaceFolder.Uri.GetFileSystemPath();
+ string combinedPath = Path.Combine(folderPath, filePath);
+ if (File.Exists(combinedPath))
+ {
+ return combinedPath;
+ }
+ }
+
+ // If the file path is not found in the workspace folders, return the original path.
+ return filePath;
+ }
+
///
/// Enumerate all the PowerShell (ps1, psm1, psd1) files in the workspace in a recursive manner, using default values.
///
@@ -337,7 +395,95 @@ public IEnumerable EnumeratePSFiles(
string[] excludeGlobs,
string[] includeGlobs,
int maxDepth,
- bool ignoreReparsePoints) => [];
+ bool ignoreReparsePoints)
+ {
+ string[] powerShellWorkspacePaths = GetPowerShellWorkspacePaths()
+ .Where(path => !string.IsNullOrEmpty(path))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
+ string[] fileSystemWorkspacePaths = GetFileSystemWorkspacePaths()
+ .Where(path => !string.IsNullOrEmpty(path))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
+ if (powerShellWorkspacePaths.Length == 0 && fileSystemWorkspacePaths.Length == 0)
+ {
+ yield break;
+ }
+
+ if (psesInternalHost is not null && powerShellWorkspacePaths.Length > 0)
+ {
+ // Determine if all workspace paths are FileSystem provider paths.
+ // Non-FileSystem providers (e.g. pspath:) don't support Include/Exclude/Depth/FollowSymlink
+ // dynamic parameters, so we only add them for FileSystem paths.
+ bool allFileSystem = powerShellWorkspacePaths.All(
+ p => p.StartsWith("Microsoft.PowerShell.FileSystem", StringComparison.OrdinalIgnoreCase)
+ || !p.Contains("::"));
+
+ PSCommand psCommand = new PSCommand()
+ .AddCommand(@"Microsoft.PowerShell.Management\Get-ChildItem")
+ .AddParameter("LiteralPath", powerShellWorkspacePaths)
+ .AddParameter("Recurse")
+ .AddParameter("ErrorAction", ActionPreference.SilentlyContinue)
+ .AddParameter("Force");
+
+ if (allFileSystem)
+ {
+ psCommand
+ .AddParameter("Include", includeGlobs.Concat(VersionUtils.IsNetCore ? s_psFileExtensionsCoreFramework : s_psFileExtensionsFullFramework).ToArray())
+ .AddParameter("Exclude", excludeGlobs)
+ .AddParameter("Depth", maxDepth);
+
+ if (VersionUtils.IsNetCore)
+ {
+ psCommand.AddParameter("FollowSymlink", !ignoreReparsePoints);
+ }
+ }
+
+ psCommand
+ .AddCommand("Where-Object")
+ .AddParameter("Property", "PSIsContainer")
+ .AddParameter("EQ")
+ .AddParameter("Value", false);
+
+ IReadOnlyList results = psesInternalHost.InvokePSCommand(psCommand, null, CancellationToken.None);
+ foreach (string path in results.Select(ConvertWorkspaceItemPath).Where(path => !string.IsNullOrEmpty(path)))
+ {
+ yield return path;
+ }
+
+ yield break;
+ }
+
+ Matcher matcher = new();
+ foreach (string pattern in includeGlobs) { matcher.AddInclude(pattern); }
+ foreach (string pattern in excludeGlobs) { matcher.AddExclude(pattern); }
+
+ foreach (string rootPath in fileSystemWorkspacePaths)
+ {
+ if (!Directory.Exists(rootPath))
+ {
+ continue;
+ }
+
+ WorkspaceFileSystemWrapperFactory fsFactory = new(
+ rootPath,
+ maxDepth,
+ VersionUtils.IsNetCore ? s_psFileExtensionsCoreFramework : s_psFileExtensionsFullFramework,
+ ignoreReparsePoints,
+ logger);
+
+ PatternMatchingResult fileMatchResult = matcher.Execute(fsFactory.RootDirectory);
+ foreach (FilePatternMatch item in fileMatchResult.Files)
+ {
+ // item.Path always contains forward slashes in paths when it should be backslashes on Windows.
+ // Since we're returning strings here, it's important to use the correct directory separator.
+ string path = VersionUtils.IsWindows ? item.Path.Replace('/', Path.DirectorySeparatorChar) : item.Path;
+ yield return Path.Combine(rootPath, path);
+ }
+ }
+ }
#endregion
@@ -353,10 +499,161 @@ internal static StreamReader OpenStreamReader(DocumentUri uri)
return new StreamReader(fileStream, new UTF8Encoding(), detectEncodingFromByteOrderMarks: true);
}
- internal static string ReadFileContents(DocumentUri uri)
+ internal string ReadFileContents(DocumentUri uri)
{
- using StreamReader reader = OpenStreamReader(uri);
- return reader.ReadToEnd();
+ if (uri.ToUri().IsFile || psesInternalHost is null)
+ {
+ using StreamReader reader = OpenStreamReader(uri);
+ return reader.ReadToEnd();
+ }
+
+ string psPath = GetPowerShellPath(uri);
+ try
+ {
+ IReadOnlyList result = psesInternalHost.InvokePSCommand(
+ new PSCommand()
+ .AddCommand(@"Microsoft.PowerShell.Management\Get-Content")
+ .AddParameter("LiteralPath", psPath)
+ .AddParameter("ErrorAction", ActionPreference.Stop),
+ new PowerShellExecutionOptions { ThrowOnError = true },
+ CancellationToken.None);
+
+ return string.Join(Environment.NewLine, result);
+ }
+ catch (ActionPreferenceStopException ex)
+ when (ex.ErrorRecord.CategoryInfo.Category == ErrorCategory.ObjectNotFound
+ && ex.ErrorRecord.TargetObject is string[] missingFiles
+ && missingFiles.Length == 1)
+ {
+ throw new FileNotFoundException(ex.ErrorRecord.ToString(), missingFiles[0], ex.ErrorRecord.Exception);
+ }
+ }
+
+ // Return only file-backed workspace roots as filesystem paths.
+ // Example:
+ // file:///repo -> /repo
+ // pspath://FileSystem/C%3A/repo -> excluded
+ private IEnumerable GetFileSystemWorkspacePaths()
+ {
+ if (WorkspaceFolders.Count > 0)
+ {
+ return WorkspaceFolders
+ .Select(folder => folder.Uri)
+ .Where(uri => uri.ToUri().IsFile)
+ .Select(uri => uri.GetFileSystemPath());
+ }
+
+ return string.IsNullOrEmpty(InitialWorkingDirectory)
+ ? Array.Empty()
+ : new[] { InitialWorkingDirectory };
+ }
+
+ // Return only provider-backed workspace roots as PowerShell literal paths.
+ // Example:
+ // pspath://FileSystem/C%3A/repo -> FileSystem::C:/repo
+ // file:///repo -> excluded
+ private IEnumerable GetPowerShellWorkspacePaths()
+ {
+ if (WorkspaceFolders.Count > 0)
+ {
+ return WorkspaceFolders
+ .Select(folder => folder.Uri)
+ .Where(uri => !uri.ToUri().IsFile)
+ .Select(GetPowerShellPath);
+ }
+
+ return Array.Empty();
+ }
+
+ // Normalize Get-ChildItem output to a workspace path string.
+ // Example:
+ // FullName=/repo/a.ps1 -> /repo/a.ps1
+ // PSPath=Registry::HKEY_CURRENT_USER\\Software\\Foo -> pspath://Registry/HKEY_CURRENT_USER/Software/Foo
+ private static string ConvertWorkspaceItemPath(PSObject item)
+ {
+ if (item.Properties["FullName"]?.Value is string fullName && !string.IsNullOrEmpty(fullName))
+ {
+ return fullName;
+ }
+
+ return item.Properties["PSPath"]?.Value is string psPath && !string.IsNullOrEmpty(psPath)
+ ? CreatePowerShellPathUri(psPath)
+ : null;
+ }
+
+ // Convert a document URI to the literal path PowerShell commands should use.
+ // Example:
+ // file:///repo/a.ps1 -> /repo/a.ps1
+ // pspath://FileSystem/C%3A/repo/a.ps1 -> FileSystem::C:/repo/a.ps1
+ private static string GetPowerShellPath(DocumentUri uri)
+ {
+ Uri parsedUri = uri.ToUri();
+ if (parsedUri.IsFile)
+ {
+ return parsedUri.LocalPath;
+ }
+
+ if (string.Equals(uri.Scheme, s_psPathScheme, StringComparison.OrdinalIgnoreCase))
+ {
+ string provider = parsedUri.GetComponents(UriComponents.Host, UriFormat.Unescaped);
+ string path = Uri.UnescapeDataString(parsedUri.AbsolutePath);
+ if (path.Length >= 3 && path[0] == '/' && char.IsLetter(path[1]) && path[2] == ':')
+ {
+ path = path.TrimStart('/');
+ }
+
+ return string.IsNullOrEmpty(provider)
+ ? path.TrimStart('/')
+ : $"{provider}::{path}";
+ }
+
+ throw new NotSupportedException($"Unsupported URI scheme '{uri.Scheme}'.");
+ }
+
+ // Convert a PowerShell provider path to the pspath:// document form used by the workspace.
+ // Example:
+ // FileSystem::C:\\repo\\a.ps1 -> pspath://FileSystem/C%3A/repo/a.ps1
+ // Registry::HKEY_CURRENT_USER\\Software\\Foo -> pspath://Registry/HKEY_CURRENT_USER/Software/Foo
+ // pspath:\local\Function\MyScript.ps1 -> pspath://ScriptPSProvider/local/Function/MyScript.ps1
+ // ScriptPSProvider::pspath:\local\Function\MyScript.ps1 -> pspath://ScriptPSProvider/local/Function/MyScript.ps1
+ private static string CreatePowerShellPathUri(string psPath)
+ {
+ // Strip the provider qualifier prefix if present (e.g. "ScriptPSProvider::pspath:\..." -> "pspath:\...")
+ string pathPart = psPath;
+ if (pathPart.Contains("::"))
+ {
+ string[] qualifiedParts = pathPart.Split(new[] { "::" }, 2, StringSplitOptions.None);
+ if (qualifiedParts.Length == 2)
+ {
+ pathPart = qualifiedParts[1];
+ }
+ }
+
+ // Handle drive-qualified paths from the pspath: provider drive, e.g.
+ // pspath:\local\Function\MyScript.ps1
+ // PowerShell sets PSPath to the drive-qualified path (potentially provider-qualified
+ // as "ScriptPSProvider::pspath:\..."), so we strip the provider prefix above and
+ // then reconstruct the provider-qualified URI form.
+ if (pathPart.StartsWith($"{s_psPathScheme}:", StringComparison.OrdinalIgnoreCase))
+ {
+ int colonIndex = pathPart.IndexOf(':');
+ string drivePath = pathPart.Substring(colonIndex + 1).Replace('\\', '/').TrimStart('/');
+ string driveEncodedPath = string.Join("/", drivePath.Split('/').Select(Uri.EscapeDataString));
+ return $"{s_psPathScheme}://{s_psPathProviderHost}/{driveEncodedPath}";
+ }
+
+ // For other provider-qualified paths (e.g. FileSystem::C:\repo\a.ps1),
+ // use the original psPath which still has the "::" separator.
+ string[] parts = psPath.Split(new[] { "::" }, 2, StringSplitOptions.None);
+ if (parts.Length != 2)
+ {
+ return $"{s_psPathScheme}:///{Uri.EscapeDataString(psPath)}";
+ }
+
+ string provider = parts[0].Split('\\').Last();
+ string normalizedPath = parts[1].Replace('\\', '/');
+ string encodedPath = string.Join("/", normalizedPath.Split('/').Select(Uri.EscapeDataString));
+ return $"{s_psPathScheme}://{Uri.EscapeDataString(provider)}/{encodedPath}";
}
internal string ResolveWorkspacePath(string path) => ResolveRelativeScriptPath(InitialWorkingDirectory, path);
diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs
index 30b020c30..5142bdbde 100644
--- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs
+++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs
@@ -33,21 +33,22 @@ internal class TestReadLine : IReadLine
}
[Trait("Category", "DebugService")]
- public class DebugServiceTests : IDisposable
+ public class DebugServiceTests : IAsyncLifetime
{
- private readonly PsesInternalHost psesHost;
- private readonly BreakpointService breakpointService;
- private readonly DebugService debugService;
+ private PsesInternalHost psesHost;
+ private BreakpointService breakpointService;
+ private DebugService debugService;
private readonly BlockingCollection debuggerStoppedQueue = new();
- private readonly WorkspaceService workspace;
- private readonly ScriptFile debugScriptFile;
- private readonly ScriptFile oddPathScriptFile;
- private readonly ScriptFile variableScriptFile;
+ private WorkspaceService workspace;
+ private ScriptFile debugScriptFile;
+ private ScriptFile oddPathScriptFile;
+ private ScriptFile psProviderPathScriptFile;
+ private ScriptFile variableScriptFile;
private readonly TestReadLine testReadLine = new();
- public DebugServiceTests()
+ public async Task InitializeAsync()
{
- psesHost = PsesHostFactory.Create(NullLoggerFactory.Instance);
+ psesHost = await PsesHostFactory.Create(NullLoggerFactory.Instance);
// This is required for remote debugging, but we call it here to end up in the same
// state as the usual startup path.
psesHost.DebugContext.EnableDebugMode();
@@ -70,32 +71,51 @@ public DebugServiceTests()
debugService.DebuggerStopped += OnDebuggerStopped;
// Load the test debug files.
- workspace = new WorkspaceService(NullLoggerFactory.Instance);
+ workspace = new WorkspaceService(NullLoggerFactory.Instance, psesHost);
debugScriptFile = GetDebugScript("DebugTest.ps1");
oddPathScriptFile = GetDebugScript("Debug' W&ith $Params [Test].ps1");
variableScriptFile = GetDebugScript("VariableTest.ps1");
+
+ string variableScriptFilePath = TestUtilities.GetSharedPath(Path.Combine("Debugging", "VariableTest.ps1"));
+ dynamic psItem = (await psesHost.ExecutePSCommandAsync(
+ new PSCommand()
+ .AddCommand("Get-Item")
+ .AddParameter("LiteralPath", variableScriptFilePath),
+ CancellationToken.None)).First();
+
+ psProviderPathScriptFile = workspace.GetFile(ConvertPSPathToUri((string)psItem.PSPath.ToString()));
}
- public void Dispose()
+ public async Task DisposeAsync()
{
debugService.Abort();
+ await Task.Run(psesHost.StopAsync);
debuggerStoppedQueue.Dispose();
-#pragma warning disable VSTHRD002
- psesHost.StopAsync().Wait();
-#pragma warning restore VSTHRD002
- GC.SuppressFinalize(this);
}
///
- /// This event handler lets us test that the debugger stopped or paused as expected. It will
- /// deadlock if called in the PSES Pipeline Thread, which can easily happen in this test
- /// code when methods on are called. Hence we treat this test
- /// code like UI code and use 'ConfigureAwait(true)' or 'Task.Run(...)' to ensure we stay
- /// OFF the pipeline thread.
+ /// This event handler lets us test that the debugger stopped or paused
+ /// as expected. It will deadlock if called in the PSES Pipeline Thread.
+ /// Hence we use 'Task.Run(...)' when accessing the queue to ensure we
+ /// stay OFF the pipeline thread.
///
///
///
- private void OnDebuggerStopped(object sender, DebuggerStoppedEventArgs e) => debuggerStoppedQueue.Add(e);
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD110:Observe result of async calls", Justification = "This intentionally fires and forgets on another thread.")]
+ private void OnDebuggerStopped(object sender, DebuggerStoppedEventArgs e) => Task.Run(() => debuggerStoppedQueue.Add(e));
+
+ // Convert a PowerShell provider path into the pspath:// URI form used by workspace tests.
+ // Example:
+ // FileSystem::C:\\repo\\a.ps1 -> pspath://FileSystem/C%3A/repo/a.ps1
+ // Registry::HKEY_CURRENT_USER\\Software\\Foo -> pspath://Registry/HKEY_CURRENT_USER/Software/Foo
+ private static string ConvertPSPathToUri(string psPath)
+ {
+ string[] parts = psPath.Split(new[] { "::" }, 2, StringSplitOptions.None);
+ string provider = parts[0].Split('\\').Last();
+ string normalizedPath = parts[1].Replace('\\', '/');
+ string encodedPath = string.Join("/", normalizedPath.Split('/').Select(Uri.EscapeDataString));
+ return $"pspath://{Uri.EscapeDataString(provider)}/{encodedPath}";
+ }
private ScriptFile GetDebugScript(string fileName) => workspace.GetFile(TestUtilities.GetSharedPath(Path.Combine("Debugging", fileName)));
@@ -118,20 +138,20 @@ private Task ExecuteScriptFileAsync(string scriptFilePath, params string[] args)
private Task ExecuteVariableScriptFileAsync() => ExecuteScriptFileAsync(variableScriptFile.FilePath);
- private void AssertDebuggerPaused()
+ private async Task AssertDebuggerPaused()
{
using CancellationTokenSource cts = new(60000);
- DebuggerStoppedEventArgs eventArgs = debuggerStoppedQueue.Take(cts.Token);
+ DebuggerStoppedEventArgs eventArgs = await Task.Run(() => debuggerStoppedQueue.Take(cts.Token));
Assert.Empty(eventArgs.OriginalEvent.Breakpoints);
}
- private void AssertDebuggerStopped(
+ private async Task AssertDebuggerStopped(
string scriptPath = "",
int lineNumber = -1,
CommandBreakpointDetails commandBreakpointDetails = default)
{
- using CancellationTokenSource cts = new(60000);
- DebuggerStoppedEventArgs eventArgs = debuggerStoppedQueue.Take(cts.Token);
+ using CancellationTokenSource cts = new(30000);
+ DebuggerStoppedEventArgs eventArgs = await Task.Run(() => debuggerStoppedQueue.Take(cts.Token));
Assert.True(psesHost.DebugContext.IsStopped);
@@ -176,8 +196,8 @@ await debugService.SetCommandBreakpointsAsync(
Task> executeTask = psesHost.ExecutePSCommandAsync(
new PSCommand().AddScript("Get-Random -SetSeed 42 -Maximum 100"), CancellationToken.None);
- AssertDebuggerStopped("", 1);
- await Task.Run(debugService.Continue);
+ await AssertDebuggerStopped("", 1);
+ debugService.Continue();
Assert.Equal(17, (await executeTask)[0]);
StackFrameDetails[] stackFrames = await debugService.GetStackFramesAsync();
@@ -204,7 +224,7 @@ await debugService.SetCommandBreakpointsAsync(
public async Task DebuggerAcceptsScriptArgs(string[] args)
{
IReadOnlyList breakpoints = await debugService.SetLineBreakpointsAsync(
- oddPathScriptFile,
+ oddPathScriptFile.FilePath,
new[] { BreakpointDetails.Create(oddPathScriptFile.FilePath, 3) });
Assert.Single(breakpoints);
@@ -218,7 +238,7 @@ public async Task DebuggerAcceptsScriptArgs(string[] args)
Task _ = ExecuteScriptFileAsync(oddPathScriptFile.FilePath, args);
- AssertDebuggerStopped(oddPathScriptFile.FilePath, 3);
+ await AssertDebuggerStopped(oddPathScriptFile.FilePath, 3);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.LocalScopeName);
@@ -285,7 +305,7 @@ public async Task DebuggerStopsOnFunctionBreakpoints()
new[] { CommandBreakpointDetails.Create("Write-Host") });
Task _ = ExecuteDebugFileAsync();
- AssertDebuggerStopped(debugScriptFile.FilePath, 6);
+ await AssertDebuggerStopped(debugScriptFile.FilePath, 6);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.LocalScopeName);
@@ -296,8 +316,8 @@ public async Task DebuggerStopsOnFunctionBreakpoints()
Assert.Equal("1", i.ValueString);
// The function breakpoint should fire the next time through the loop.
- await Task.Run(debugService.Continue);
- AssertDebuggerStopped(debugScriptFile.FilePath, 6);
+ debugService.Continue();
+ await AssertDebuggerStopped(debugScriptFile.FilePath, 6);
variables = await GetVariables(VariableContainerDetails.LocalScopeName);
@@ -313,7 +333,7 @@ public async Task DebuggerSetsAndClearsLineBreakpoints()
{
IReadOnlyList breakpoints =
await debugService.SetLineBreakpointsAsync(
- debugScriptFile,
+ debugScriptFile.FilePath,
new[] {
BreakpointDetails.Create(debugScriptFile.FilePath, 5),
BreakpointDetails.Create(debugScriptFile.FilePath, 10)
@@ -326,7 +346,7 @@ await debugService.SetLineBreakpointsAsync(
Assert.Equal(10, breakpoints[1].LineNumber);
breakpoints = await debugService.SetLineBreakpointsAsync(
- debugScriptFile,
+ debugScriptFile.FilePath,
new[] { BreakpointDetails.Create(debugScriptFile.FilePath, 2) });
confirmedBreakpoints = await GetConfirmedBreakpoints(debugScriptFile);
@@ -334,7 +354,7 @@ await debugService.SetLineBreakpointsAsync(
Assert.Equal(2, breakpoints[0].LineNumber);
await debugService.SetLineBreakpointsAsync(
- debugScriptFile,
+ debugScriptFile.FilePath,
Array.Empty());
IReadOnlyList remainingBreakpoints = await GetConfirmedBreakpoints(debugScriptFile);
@@ -345,16 +365,16 @@ await debugService.SetLineBreakpointsAsync(
public async Task DebuggerStopsOnLineBreakpoints()
{
await debugService.SetLineBreakpointsAsync(
- debugScriptFile,
+ debugScriptFile.FilePath,
new[] {
BreakpointDetails.Create(debugScriptFile.FilePath, 5),
BreakpointDetails.Create(debugScriptFile.FilePath, 7)
});
Task _ = ExecuteDebugFileAsync();
- AssertDebuggerStopped(debugScriptFile.FilePath, 5);
- await Task.Run(debugService.Continue);
- AssertDebuggerStopped(debugScriptFile.FilePath, 7);
+ await AssertDebuggerStopped(debugScriptFile.FilePath, 5);
+ debugService.Continue();
+ await AssertDebuggerStopped(debugScriptFile.FilePath, 7);
}
[Fact]
@@ -364,13 +384,13 @@ public async Task DebuggerStopsOnConditionalBreakpoints()
const int breakpointValue2 = 20;
await debugService.SetLineBreakpointsAsync(
- debugScriptFile,
+ debugScriptFile.FilePath,
new[] {
BreakpointDetails.Create(debugScriptFile.FilePath, 7, null, $"$i -eq {breakpointValue1} -or $i -eq {breakpointValue2}"),
});
Task _ = ExecuteDebugFileAsync();
- AssertDebuggerStopped(debugScriptFile.FilePath, 7);
+ await AssertDebuggerStopped(debugScriptFile.FilePath, 7);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.LocalScopeName);
@@ -382,8 +402,8 @@ await debugService.SetLineBreakpointsAsync(
// The conditional breakpoint should not fire again, until the value of
// i reaches breakpointValue2.
- await Task.Run(debugService.Continue);
- AssertDebuggerStopped(debugScriptFile.FilePath, 7);
+ debugService.Continue();
+ await AssertDebuggerStopped(debugScriptFile.FilePath, 7);
variables = await GetVariables(VariableContainerDetails.LocalScopeName);
@@ -400,13 +420,13 @@ public async Task DebuggerStopsOnHitConditionBreakpoint()
const int hitCount = 5;
await debugService.SetLineBreakpointsAsync(
- debugScriptFile,
+ debugScriptFile.FilePath,
new[] {
BreakpointDetails.Create(debugScriptFile.FilePath, 6, null, null, $"{hitCount}"),
});
Task _ = ExecuteDebugFileAsync();
- AssertDebuggerStopped(debugScriptFile.FilePath, 6);
+ await AssertDebuggerStopped(debugScriptFile.FilePath, 6);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.LocalScopeName);
@@ -423,11 +443,11 @@ public async Task DebuggerStopsOnConditionalAndHitConditionBreakpoint()
const int hitCount = 5;
await debugService.SetLineBreakpointsAsync(
- debugScriptFile,
+ debugScriptFile.FilePath,
new[] { BreakpointDetails.Create(debugScriptFile.FilePath, 6, null, "$i % 2 -eq 0", $"{hitCount}") });
Task _ = ExecuteDebugFileAsync();
- AssertDebuggerStopped(debugScriptFile.FilePath, 6);
+ await AssertDebuggerStopped(debugScriptFile.FilePath, 6);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.LocalScopeName);
@@ -444,7 +464,7 @@ public async Task DebuggerProvidesMessageForInvalidConditionalBreakpoint()
{
IReadOnlyList breakpoints =
await debugService.SetLineBreakpointsAsync(
- debugScriptFile,
+ debugScriptFile.FilePath,
new[] {
// TODO: Add this breakpoint back when it stops moving around?! The ordering
// of these two breakpoints seems to do with which framework executes the
@@ -472,7 +492,7 @@ public async Task DebuggerFindsParsableButInvalidSimpleBreakpointConditions()
{
IReadOnlyList breakpoints =
await debugService.SetLineBreakpointsAsync(
- debugScriptFile,
+ debugScriptFile.FilePath,
new[] {
BreakpointDetails.Create(debugScriptFile.FilePath, 5, column: null, condition: "$i == 100"),
BreakpointDetails.Create(debugScriptFile.FilePath, 7, column: null, condition: "$i > 100")
@@ -495,18 +515,16 @@ public async Task DebuggerBreaksWhenRequested()
IReadOnlyList confirmedBreakpoints = await GetConfirmedBreakpoints(debugScriptFile);
Assert.Empty(confirmedBreakpoints);
Task _ = ExecuteDebugFileAsync();
- // NOTE: This must be run on a separate thread so the async event handlers can fire.
- await Task.Run(debugService.Break);
- AssertDebuggerPaused();
+ debugService.Break();
+ await AssertDebuggerPaused();
}
[Fact]
public async Task DebuggerRunsCommandsWhileStopped()
{
Task _ = ExecuteDebugFileAsync();
- // NOTE: This must be run on a separate thread so the async event handlers can fire.
- await Task.Run(debugService.Break);
- AssertDebuggerPaused();
+ debugService.Break();
+ await AssertDebuggerPaused();
// Try running a command from outside the pipeline thread
Task> executeTask = psesHost.ExecutePSCommandAsync(
@@ -526,16 +544,17 @@ await debugService.SetCommandBreakpointsAsync(
ScriptFile testScript = GetDebugScript("PSDebugContextTest.ps1");
Task _ = ExecuteScriptFileAsync(testScript.FilePath);
- AssertDebuggerStopped(testScript.FilePath, 11);
+ await AssertDebuggerStopped(testScript.FilePath, 11);
VariableDetails prompt = await debugService.EvaluateExpressionAsync("prompt", false, CancellationToken.None);
Assert.Equal("True > ", prompt.ValueString);
}
- [SkippableFact]
- public async Task DebuggerBreaksInUntitledScript()
+ [Theory]
+ [InlineData("Command")]
+ [InlineData("Line")]
+ public async Task DebuggerBreaksInUntitledScript(string breakpointType)
{
- Skip.IfNot(VersionUtils.PSEdition == "Core", "Untitled script breakpoints only supported in PowerShell Core");
const string contents = "Write-Output $($MyInvocation.Line)";
const string scriptPath = "untitled:Untitled-1";
Assert.True(ScriptFile.IsUntitledPath(scriptPath));
@@ -544,14 +563,23 @@ public async Task DebuggerBreaksInUntitledScript()
Assert.Equal(contents, scriptFile.Contents);
Assert.True(workspace.TryGetFile(scriptPath, out ScriptFile _));
- await debugService.SetCommandBreakpointsAsync(
- new[] { CommandBreakpointDetails.Create("Write-Output") });
+ if (breakpointType == "Command")
+ {
+ await debugService.SetCommandBreakpointsAsync(
+ new[] { CommandBreakpointDetails.Create("Write-Output") });
+ }
+ else
+ {
+ await debugService.SetLineBreakpointsAsync(
+ scriptFile.FilePath,
+ new[] { BreakpointDetails.Create(scriptPath, 1) });
+ }
ConfigurationDoneHandler configurationDoneHandler = new(
- NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null, psesHost);
+ NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null);
Task _ = configurationDoneHandler.LaunchScriptAsync(scriptPath);
- AssertDebuggerStopped(scriptPath, 1);
+ await AssertDebuggerStopped(scriptPath, 1);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.CommandVariablesName);
VariableDetailsBase myInvocation = Array.Find(variables, v => v.Name == "$MyInvocation");
@@ -570,7 +598,7 @@ await debugService.SetCommandBreakpointsAsync(
public async Task RecordsF5CommandInPowerShellHistory()
{
ConfigurationDoneHandler configurationDoneHandler = new(
- NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null, psesHost);
+ NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null);
await configurationDoneHandler.LaunchScriptAsync(debugScriptFile.FilePath);
IReadOnlyList historyResult = await psesHost.ExecutePSCommandAsync(
@@ -610,7 +638,7 @@ public async Task RecordsF8CommandInHistory()
public async Task OddFilePathsLaunchCorrectly()
{
ConfigurationDoneHandler configurationDoneHandler = new(
- NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null, psesHost);
+ NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null);
await configurationDoneHandler.LaunchScriptAsync(oddPathScriptFile.FilePath);
IReadOnlyList historyResult = await psesHost.ExecutePSCommandAsync(
@@ -621,15 +649,29 @@ public async Task OddFilePathsLaunchCorrectly()
Assert.Equal(". " + PSCommandHelpers.EscapeScriptFilePath(oddPathScriptFile.FilePath), Assert.Single(historyResult));
}
+ [Fact]
+ public async Task PSProviderPathsLaunchCorrectly()
+ {
+ ConfigurationDoneHandler configurationDoneHandler = new(
+ NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null);
+ await configurationDoneHandler.LaunchScriptAsync(psProviderPathScriptFile.FilePath);
+
+ IReadOnlyList historyResult = await psesHost.ExecutePSCommandAsync(
+ new PSCommand().AddScript("(Get-History).CommandLine"),
+ CancellationToken.None);
+
+ Assert.Equal(". $args[0]", Assert.Single(historyResult));
+ }
+
[Fact]
public async Task DebuggerVariableStringDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
- variableScriptFile,
+ variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 8) });
Task _ = ExecuteVariableScriptFileAsync();
- AssertDebuggerStopped(variableScriptFile.FilePath);
+ await AssertDebuggerStopped(variableScriptFile.FilePath);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.LocalScopeName);
@@ -643,11 +685,11 @@ await debugService.SetLineBreakpointsAsync(
public async Task DebuggerGetsVariables()
{
await debugService.SetLineBreakpointsAsync(
- variableScriptFile,
+ variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 21) });
Task _ = ExecuteVariableScriptFileAsync();
- AssertDebuggerStopped(variableScriptFile.FilePath);
+ await AssertDebuggerStopped(variableScriptFile.FilePath);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.LocalScopeName);
@@ -693,11 +735,11 @@ await debugService.SetLineBreakpointsAsync(
public async Task DebuggerSetsVariablesNoConversion()
{
await debugService.SetLineBreakpointsAsync(
- variableScriptFile,
+ variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 14) });
Task _ = ExecuteVariableScriptFileAsync();
- AssertDebuggerStopped(variableScriptFile.FilePath);
+ await AssertDebuggerStopped(variableScriptFile.FilePath);
VariableScope[] scopes = debugService.GetVariableScopes(0);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.LocalScopeName);
@@ -723,8 +765,8 @@ await debugService.SetLineBreakpointsAsync(
// The above just tests that the debug service returns the correct new value string.
// Let's step the debugger and make sure the values got set to the new values.
- await Task.Run(debugService.StepOver);
- AssertDebuggerStopped(variableScriptFile.FilePath);
+ debugService.StepOver();
+ await AssertDebuggerStopped(variableScriptFile.FilePath);
// Test set of a local string variable (not strongly typed)
variables = await GetVariables(VariableContainerDetails.LocalScopeName);
@@ -746,12 +788,12 @@ await debugService.SetLineBreakpointsAsync(
public async Task DebuggerSetsVariablesWithConversion()
{
await debugService.SetLineBreakpointsAsync(
- variableScriptFile,
+ variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 14) });
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
- AssertDebuggerStopped(variableScriptFile.FilePath);
+ await AssertDebuggerStopped(variableScriptFile.FilePath);
VariableScope[] scopes = debugService.GetVariableScopes(0);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.LocalScopeName);
@@ -779,8 +821,8 @@ await debugService.SetLineBreakpointsAsync(
// The above just tests that the debug service returns the correct new value string.
// Let's step the debugger and make sure the values got set to the new values.
- await Task.Run(debugService.StepOver);
- AssertDebuggerStopped(variableScriptFile.FilePath);
+ debugService.StepOver();
+ await AssertDebuggerStopped(variableScriptFile.FilePath);
// Test set of a local string variable (not strongly typed but force conversion)
variables = await GetVariables(VariableContainerDetails.LocalScopeName);
@@ -802,12 +844,12 @@ await debugService.SetLineBreakpointsAsync(
public async Task DebuggerVariableEnumDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
- variableScriptFile,
+ variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 15) });
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
- AssertDebuggerStopped(variableScriptFile.FilePath);
+ await AssertDebuggerStopped(variableScriptFile.FilePath);
StackFrameDetails[] stackFrames = await debugService.GetStackFramesAsync();
VariableDetailsBase[] variables = await debugService.GetVariables(stackFrames[0].AutoVariables.Id, CancellationToken.None);
@@ -822,12 +864,12 @@ await debugService.SetLineBreakpointsAsync(
public async Task DebuggerVariableHashtableDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
- variableScriptFile,
+ variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 11) });
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
- AssertDebuggerStopped(variableScriptFile.FilePath);
+ await AssertDebuggerStopped(variableScriptFile.FilePath);
StackFrameDetails[] stackFrames = await debugService.GetStackFramesAsync();
VariableDetailsBase[] variables = await debugService.GetVariables(stackFrames[0].AutoVariables.Id, CancellationToken.None);
@@ -855,12 +897,12 @@ await debugService.SetLineBreakpointsAsync(
public async Task DebuggerVariableNullStringDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
- variableScriptFile,
+ variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 16) });
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
- AssertDebuggerStopped(variableScriptFile.FilePath);
+ await AssertDebuggerStopped(variableScriptFile.FilePath);
StackFrameDetails[] stackFrames = await debugService.GetStackFramesAsync();
VariableDetailsBase[] variables = await debugService.GetVariables(stackFrames[0].AutoVariables.Id, CancellationToken.None);
@@ -875,12 +917,12 @@ await debugService.SetLineBreakpointsAsync(
public async Task DebuggerVariablePSObjectDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
- variableScriptFile,
+ variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 17) });
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
- AssertDebuggerStopped(variableScriptFile.FilePath);
+ await AssertDebuggerStopped(variableScriptFile.FilePath);
StackFrameDetails[] stackFrames = await debugService.GetStackFramesAsync();
VariableDetailsBase[] variables = await debugService.GetVariables(stackFrames[0].AutoVariables.Id, CancellationToken.None);
@@ -907,7 +949,7 @@ public async Task DebuggerEnumerableShowsRawView()
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
- AssertDebuggerStopped(commandBreakpointDetails: breakpoint);
+ await AssertDebuggerStopped(commandBreakpointDetails: breakpoint);
VariableDetailsBase simpleArrayVar = Array.Find(
await GetVariables(VariableContainerDetails.ScriptScopeName),
@@ -964,7 +1006,7 @@ public async Task DebuggerDictionaryShowsRawView()
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
- AssertDebuggerStopped(commandBreakpointDetails: breakpoint);
+ await AssertDebuggerStopped(commandBreakpointDetails: breakpoint);
VariableDetailsBase simpleDictionaryVar = Array.Find(
await GetVariables(VariableContainerDetails.ScriptScopeName),
@@ -1027,7 +1069,7 @@ public async Task DebuggerDerivedDictionaryPropertyInRawView()
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
- AssertDebuggerStopped(commandBreakpointDetails: breakpoint);
+ await AssertDebuggerStopped(commandBreakpointDetails: breakpoint);
VariableDetailsBase sortedDictionaryVar = Array.Find(
await GetVariables(VariableContainerDetails.ScriptScopeName),
@@ -1071,12 +1113,12 @@ await GetVariables(VariableContainerDetails.ScriptScopeName),
public async Task DebuggerVariablePSCustomObjectDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
- variableScriptFile,
+ variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 18) });
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
- AssertDebuggerStopped(variableScriptFile.FilePath);
+ await AssertDebuggerStopped(variableScriptFile.FilePath);
StackFrameDetails[] stackFrames = await debugService.GetStackFramesAsync();
VariableDetailsBase[] variables = await debugService.GetVariables(stackFrames[0].AutoVariables.Id, CancellationToken.None);
@@ -1100,12 +1142,12 @@ await debugService.SetLineBreakpointsAsync(
public async Task DebuggerVariableProcessObjectDisplaysCorrectly()
{
await debugService.SetLineBreakpointsAsync(
- variableScriptFile,
+ variableScriptFile.FilePath,
new[] { BreakpointDetails.Create(variableScriptFile.FilePath, 19) });
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
- AssertDebuggerStopped(variableScriptFile.FilePath);
+ await AssertDebuggerStopped(variableScriptFile.FilePath);
StackFrameDetails[] stackFrames = await debugService.GetStackFramesAsync();
VariableDetailsBase[] variables = await debugService.GetVariables(stackFrames[0].AutoVariables.Id, CancellationToken.None);
@@ -1134,7 +1176,7 @@ await debugService.SetCommandBreakpointsAsync(
ScriptFile testScript = GetDebugScript("GetChildItemTest.ps1");
Task _ = ExecuteScriptFileAsync(testScript.FilePath);
- AssertDebuggerStopped(testScript.FilePath, 2);
+ await AssertDebuggerStopped(testScript.FilePath, 2);
VariableDetailsBase[] variables = await GetVariables(VariableContainerDetails.LocalScopeName);
VariableDetailsBase var = Array.Find(variables, v => v.Name == "$file");
@@ -1154,7 +1196,7 @@ public async Task DebuggerToStringShouldMarshallToPipeline()
// Execute the script and wait for the breakpoint to be hit
Task _ = ExecuteVariableScriptFileAsync();
- AssertDebuggerStopped(commandBreakpointDetails: breakpoint);
+ await AssertDebuggerStopped(commandBreakpointDetails: breakpoint);
VariableDetailsBase[] vars = await GetVariables(VariableContainerDetails.ScriptScopeName);
VariableDetailsBase customToStrings = Array.Find(vars, i => i.Name is "$CustomToStrings");
diff --git a/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs b/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs
index 11c27c4cf..7a1239ea8 100644
--- a/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs
+++ b/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs
@@ -665,8 +665,9 @@ public void DocumentUriReturnsCorrectStringForAbsolutePath()
[InlineData(@"C:\Users\me\Documents\test.ps1", false)]
[InlineData("/Users/me/Documents/test.ps1", false)]
[InlineData("vscode-notebook-cell:/Users/me/Documents/test.ps1#0001", true)]
- [InlineData("https://microsoft.com", true)]
+ [InlineData("https://microsoft.com", false)]
[InlineData("Untitled:Untitled-1", true)]
+ [InlineData("pspath://filesystem/C%3A/Users/me/Documents/test.ps1", false)]
[InlineData(@"'a log statement' > 'c:\Users\me\Documents\test.txt'
", false)]
public void IsUntitledFileIsCorrect(string path, bool expected) => Assert.Equal(expected, ScriptFile.IsUntitledPath(path));
diff --git a/test/PowerShellEditorServices.Test/packages.lock.json b/test/PowerShellEditorServices.Test/packages.lock.json
index 23fd75c30..5136423ff 100644
--- a/test/PowerShellEditorServices.Test/packages.lock.json
+++ b/test/PowerShellEditorServices.Test/packages.lock.json
@@ -11,6 +11,15 @@
"Microsoft.CodeCoverage": "17.9.0"
}
},
+ "Microsoft.NETFramework.ReferenceAssemblies": {
+ "type": "Direct",
+ "requested": "[1.0.3, )",
+ "resolved": "1.0.3",
+ "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
+ "dependencies": {
+ "Microsoft.NETFramework.ReferenceAssemblies.net462": "1.0.3"
+ }
+ },
"Microsoft.PowerShell.5.ReferenceAssemblies": {
"type": "Direct",
"requested": "[1.1.0, )",
@@ -174,6 +183,11 @@
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
+ "Microsoft.NETFramework.ReferenceAssemblies.net462": {
+ "type": "Transitive",
+ "resolved": "1.0.3",
+ "contentHash": "IzAV30z22ESCeQfxP29oVf4qEo8fBGXLXSU6oacv/9Iqe6PzgHDKCaWfwMBak7bSJQM0F5boXWoZS+kChztRIQ=="
+ },
"Microsoft.TestPlatform.ObjectModel": {
"type": "Transitive",
"resolved": "17.8.0",
@@ -536,7 +550,7 @@
"Microsoft.PowerShell.EditorServices.Test.Shared": {
"type": "Project",
"dependencies": {
- "Microsoft.PowerShell.EditorServices": "[0.0.1, )"
+ "Microsoft.PowerShell.EditorServices": "[3.18.1, )"
}
}
},
@@ -1671,7 +1685,7 @@
"Microsoft.PowerShell.EditorServices.Test.Shared": {
"type": "Project",
"dependencies": {
- "Microsoft.PowerShell.EditorServices": "[0.0.1, )"
+ "Microsoft.PowerShell.EditorServices": "[3.18.1, )"
}
}
},
@@ -2794,7 +2808,7 @@
"Microsoft.PowerShell.EditorServices.Test.Shared": {
"type": "Project",
"dependencies": {
- "Microsoft.PowerShell.EditorServices": "[0.0.1, )"
+ "Microsoft.PowerShell.EditorServices": "[3.18.1, )"
}
}
},
@@ -3916,7 +3930,7 @@
"Microsoft.PowerShell.EditorServices.Test.Shared": {
"type": "Project",
"dependencies": {
- "Microsoft.PowerShell.EditorServices": "[0.0.1, )"
+ "Microsoft.PowerShell.EditorServices": "[3.18.1, )"
}
}
}