1515using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution;
1616using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
1717using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility;
18- using Microsoft.PowerShell.EditorServices.Services.TextDocument;
1918using Microsoft.PowerShell.EditorServices.Utility;
2019
2120namespace Microsoft.PowerShell.EditorServices.Services
@@ -49,6 +48,7 @@ internal class DebugService
4948 private VariableContainerDetails scriptScopeVariables;
5049 private VariableContainerDetails localScopeVariables;
5150 private StackFrameDetails[] stackFrameDetails;
51+ private PathMapping[] _pathMappings;
5252
5353 private readonly SemaphoreSlim debugInfoHandle = AsyncUtils.CreateSimpleLockingSemaphore();
5454 #endregion
@@ -123,22 +123,22 @@ public DebugService(
123123 /// <summary>
124124 /// Sets the list of line breakpoints for the current debugging session.
125125 /// </summary>
126- /// <param name="scriptFile ">The ScriptFile in which breakpoints will be set.</param>
126+ /// <param name="scriptPath ">The path in which breakpoints will be set.</param>
127127 /// <param name="breakpoints">BreakpointDetails for each breakpoint that will be set.</param>
128128 /// <param name="clearExisting">If true, causes all existing breakpoints to be cleared before setting new ones.</param>
129+ /// <param name="skipRemoteMapping">If true, skips the remote file manager mapping of the script path.</param>
129130 /// <returns>An awaitable Task that will provide details about the breakpoints that were set.</returns>
130131 public async Task<IReadOnlyList<BreakpointDetails>> SetLineBreakpointsAsync(
131- ScriptFile scriptFile ,
132+ string scriptPath ,
132133 IReadOnlyList<BreakpointDetails> breakpoints,
133- bool clearExisting = true)
134+ bool clearExisting = true,
135+ bool skipRemoteMapping = false)
134136 {
135137 DscBreakpointCapability dscBreakpoints = await _debugContext.GetDscBreakpointCapabilityAsync().ConfigureAwait(false);
136138
137- string scriptPath = scriptFile.FilePath;
138-
139139 _psesHost.Runspace.ThrowCancelledIfUnusable();
140140 // Make sure we're using the remote script path
141- if (_psesHost.CurrentRunspace.IsOnRemoteMachine && _remoteFileManager is not null)
141+ if (!skipRemoteMapping && _psesHost.CurrentRunspace.IsOnRemoteMachine && _remoteFileManager is not null)
142142 {
143143 if (!_remoteFileManager.IsUnderRemoteTempPath(scriptPath))
144144 {
@@ -162,7 +162,7 @@ public async Task<IReadOnlyList<BreakpointDetails>> SetLineBreakpointsAsync(
162162 {
163163 if (clearExisting)
164164 {
165- await _breakpointService.RemoveAllBreakpointsAsync(scriptFile.FilePath ).ConfigureAwait(false);
165+ await _breakpointService.RemoveAllBreakpointsAsync(scriptPath ).ConfigureAwait(false);
166166 }
167167
168168 return await _breakpointService.SetBreakpointsAsync(breakpoints).ConfigureAwait(false);
@@ -603,6 +603,59 @@ public VariableScope[] GetVariableScopes(int stackFrameId)
603603 };
604604 }
605605
606+ internal void SetPathMappings(PathMapping[] pathMappings) => _pathMappings = pathMappings;
607+
608+ internal void UnsetPathMappings() => _pathMappings = null;
609+
610+ internal bool TryGetMappedLocalPath(string remotePath, out string localPath)
611+ {
612+ if (_pathMappings is not null)
613+ {
614+ foreach (PathMapping mapping in _pathMappings)
615+ {
616+ if (string.IsNullOrWhiteSpace(mapping.LocalRoot) || string.IsNullOrWhiteSpace(mapping.RemoteRoot))
617+ {
618+ // If either path mapping is null, we can't map the path.
619+ continue;
620+ }
621+
622+ if (remotePath.StartsWith(mapping.RemoteRoot, StringComparison.OrdinalIgnoreCase))
623+ {
624+ localPath = mapping.LocalRoot + remotePath.Substring(mapping.RemoteRoot.Length);
625+ return true;
626+ }
627+ }
628+ }
629+
630+ localPath = null;
631+ return false;
632+ }
633+
634+ internal bool TryGetMappedRemotePath(string localPath, out string remotePath)
635+ {
636+ if (_pathMappings is not null)
637+ {
638+ foreach (PathMapping mapping in _pathMappings)
639+ {
640+ if (string.IsNullOrWhiteSpace(mapping.LocalRoot) || string.IsNullOrWhiteSpace(mapping.RemoteRoot))
641+ {
642+ // If either path mapping is null, we can't map the path.
643+ continue;
644+ }
645+
646+ if (localPath.StartsWith(mapping.LocalRoot, StringComparison.OrdinalIgnoreCase))
647+ {
648+ // If the local path starts with the local path mapping, we can replace it with the remote path.
649+ remotePath = mapping.RemoteRoot + localPath.Substring(mapping.LocalRoot.Length);
650+ return true;
651+ }
652+ }
653+ }
654+
655+ remotePath = null;
656+ return false;
657+ }
658+
606659 #endregion
607660
608661 #region Private Methods
@@ -873,14 +926,19 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
873926 StackFrameDetails stackFrameDetailsEntry = StackFrameDetails.Create(callStackFrame, autoVariables, commandVariables);
874927 string stackFrameScriptPath = stackFrameDetailsEntry.ScriptPath;
875928
876- if (scriptNameOverride is not null
877- && string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath) )
929+ bool isNoScriptPath = string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath);
930+ if (scriptNameOverride is not null && isNoScriptPath )
878931 {
879932 stackFrameDetailsEntry.ScriptPath = scriptNameOverride;
880933 }
934+ else if (TryGetMappedLocalPath(stackFrameScriptPath, out string localMappedPath)
935+ && !isNoScriptPath)
936+ {
937+ stackFrameDetailsEntry.ScriptPath = localMappedPath;
938+ }
881939 else if (_psesHost.CurrentRunspace.IsOnRemoteMachine
882940 && _remoteFileManager is not null
883- && !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath) )
941+ && !isNoScriptPath )
884942 {
885943 stackFrameDetailsEntry.ScriptPath =
886944 _remoteFileManager.GetMappedPath(stackFrameScriptPath, _psesHost.CurrentRunspace);
@@ -981,9 +1039,13 @@ await _executionService.ExecutePSCommandAsync<PSObject>(
9811039 // Begin call stack and variables fetch. We don't need to block here.
9821040 StackFramesAndVariablesFetched = FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null);
9831041
1042+ if (!noScriptName && TryGetMappedLocalPath(e.InvocationInfo.ScriptName, out string mappedLocalPath))
1043+ {
1044+ localScriptPath = mappedLocalPath;
1045+ }
9841046 // If this is a remote connection and the debugger stopped at a line
9851047 // in a script file, get the file contents
986- if (_psesHost.CurrentRunspace.IsOnRemoteMachine
1048+ else if (_psesHost.CurrentRunspace.IsOnRemoteMachine
9871049 && _remoteFileManager is not null
9881050 && !noScriptName)
9891051 {
@@ -1034,8 +1096,12 @@ private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e)
10341096 {
10351097 // TODO: This could be either a path or a script block!
10361098 string scriptPath = lineBreakpoint.Script;
1037- if (_psesHost.CurrentRunspace.IsOnRemoteMachine
1038- && _remoteFileManager is not null)
1099+ if (TryGetMappedLocalPath(scriptPath, out string mappedLocalPath))
1100+ {
1101+ scriptPath = mappedLocalPath;
1102+ }
1103+ else if (_psesHost.CurrentRunspace.IsOnRemoteMachine
1104+ && _remoteFileManager is not null)
10391105 {
10401106 string mappedPath = _remoteFileManager.GetMappedPath(scriptPath, _psesHost.CurrentRunspace);
10411107
0 commit comments