diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs index f1bf3199e..9736b3e85 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugStateService.cs @@ -34,6 +34,8 @@ internal class DebugStateService internal bool IsUsingTempIntegratedConsole { get; set; } + internal string ExecuteMode { get; set; } + // This gets set at the end of the Launch/Attach handler which set debug state. internal TaskCompletionSource ServerStarted { get; set; } diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs index df8165319..038f61955 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs @@ -110,9 +110,11 @@ internal async Task LaunchScriptAsync(string scriptToLaunch) PSCommand command; if (System.IO.File.Exists(scriptToLaunch)) { - // For a saved file we just execute its path (after escaping it). + // For a saved file we just execute its path (after escaping it), with the configured operator + // (which can't be called that because it's a reserved keyword in C#). + string executeMode = _debugStateService?.ExecuteMode == "Call" ? "&" : "."; command = PSCommandHelpers.BuildDotSourceCommandWithArguments( - PSCommandHelpers.EscapeScriptFilePath(scriptToLaunch), _debugStateService?.Arguments); + PSCommandHelpers.EscapeScriptFilePath(scriptToLaunch), _debugStateService?.Arguments, executeMode); } else // It's a URI to an untitled script, or a raw script. { diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs index 1ec1557ed..97bd6cca7 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs @@ -65,6 +65,11 @@ internal record PsesLaunchRequestArguments : LaunchRequestArguments /// public string[] RuntimeArgs { get; set; } + /// + /// Gets or sets the script execution mode, either "DotSource" or "Call". + /// + public string ExecuteMode { get; set; } + /// /// Gets or sets optional environment variables to pass to the debuggee. The string valued /// properties of the 'environmentVariables' are used as key/value pairs. @@ -186,6 +191,7 @@ public async Task Handle(PsesLaunchRequestArguments request, Can _debugStateService.ScriptToLaunch = request.Script; _debugStateService.Arguments = request.Args; _debugStateService.IsUsingTempIntegratedConsole = request.CreateTemporaryIntegratedConsole; + _debugStateService.ExecuteMode = request.ExecuteMode; if (request.CreateTemporaryIntegratedConsole && !string.IsNullOrEmpty(request.Script) diff --git a/src/PowerShellEditorServices/Utility/PSCommandExtensions.cs b/src/PowerShellEditorServices/Utility/PSCommandExtensions.cs index b16129af9..4fe298424 100644 --- a/src/PowerShellEditorServices/Utility/PSCommandExtensions.cs +++ b/src/PowerShellEditorServices/Utility/PSCommandExtensions.cs @@ -94,10 +94,12 @@ private static StringBuilder AddCommandText(this StringBuilder sb, Command comma public static string EscapeScriptFilePath(string f) => string.Concat("'", f.Replace("'", "''"), "'"); - public static PSCommand BuildDotSourceCommandWithArguments(string command, IEnumerable arguments) + // Operator defaults to dot-source but could also be call (ampersand). + // It can't be called that because it's a reserved keyword in C#. + public static PSCommand BuildDotSourceCommandWithArguments(string command, IEnumerable arguments, string executeMode = ".") { string args = string.Join(" ", arguments ?? Array.Empty()); - string script = string.Concat(". ", command, string.IsNullOrEmpty(args) ? "" : " ", args); + string script = string.Concat(executeMode, " ", command, string.IsNullOrEmpty(args) ? "" : " ", args); // HACK: We use AddScript instead of AddArgument/AddParameter to reuse Powershell parameter binding logic. return new PSCommand().AddScript(script); } diff --git a/test/PowerShellEditorServices.Test.E2E/DebugAdapterClientExtensions.cs b/test/PowerShellEditorServices.Test.E2E/DebugAdapterClientExtensions.cs index bdf785ede..dfcb0bbc7 100644 --- a/test/PowerShellEditorServices.Test.E2E/DebugAdapterClientExtensions.cs +++ b/test/PowerShellEditorServices.Test.E2E/DebugAdapterClientExtensions.cs @@ -11,21 +11,17 @@ namespace PowerShellEditorServices.Test.E2E { public static class DebugAdapterClientExtensions { - public static async Task LaunchScript(this DebugAdapterClient debugAdapterClient, string script, TaskCompletionSource started) + public static async Task LaunchScript(this DebugAdapterClient debugAdapterClient, string script, TaskCompletionSource started, string executeMode = "DotSource") { - LaunchResponse launchResponse = await debugAdapterClient.Launch( + _ = await debugAdapterClient.Launch( new PsesLaunchRequestArguments { NoDebug = false, Script = script, Cwd = "", - CreateTemporaryIntegratedConsole = false - }); - - if (launchResponse is null) - { - throw new Exception("Launch response was null."); - } + CreateTemporaryIntegratedConsole = false, + ExecuteMode = executeMode, + }) ?? throw new Exception("Launch response was null."); // This will check to see if we received the Initialized event from the server. await started.Task; diff --git a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs index 05b10ca5b..654001da7 100644 --- a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs @@ -189,6 +189,17 @@ public async Task UsesDotSourceOperatorAndQuotesAsync() (i) => Assert.StartsWith(". '", i)); } + [Fact] + public async Task UsesCallOperatorWithSettingAsync() + { + string filePath = NewTestFile(GenerateScriptFromLoggingStatements("$($MyInvocation.Line)")); + await PsesDebugAdapterClient.LaunchScript(filePath, Started, executeMode: "Call"); + ConfigurationDoneResponse configDoneResponse = await PsesDebugAdapterClient.RequestConfigurationDone(new ConfigurationDoneArguments()); + Assert.NotNull(configDoneResponse); + Assert.Collection(await GetLog(), + (i) => Assert.StartsWith("& '", i)); + } + [Fact] public async Task CanLaunchScriptWithNoBreakpointsAsync() {