@@ -187,6 +187,11 @@ class HitCountEntry(NamedTuple):
187187 type : str
188188
189189
190+ class PathMapping (NamedTuple ):
191+ local_root : Optional [str ]
192+ remote_root : Optional [str ]
193+
194+
190195class Debugger :
191196 __instance = None
192197 __lock = threading .RLock ()
@@ -217,7 +222,7 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Any:
217222 raise RuntimeError (f"Attempt to create a '{ cls .__qualname__ } ' instance outside of instance()" )
218223
219224 def __init__ (self ) -> None :
220- self .breakpoints : Dict [str , BreakpointsEntry ] = {}
225+ self .breakpoints : Dict [pathlib . PurePath , BreakpointsEntry ] = {}
221226
222227 self .exception_breakpoints : Set [ExceptionBreakpointsEntry ] = set ()
223228 self .exception_breakpoints .add (
@@ -243,6 +248,7 @@ def __init__(self) -> None:
243248 self .no_debug = False
244249 self .terminated = False
245250 self .attached = False
251+ self .path_mappings : List [PathMapping ] = []
246252
247253 @property
248254 def debug (self ) -> bool :
@@ -400,7 +406,11 @@ def set_breakpoints(
400406 lines : Optional [List [int ]] = None ,
401407 source_modified : Optional [bool ] = None ,
402408 ) -> List [Breakpoint ]:
403- path = str (Path (source .path ).resolve ()) if source .path else ""
409+
410+ if self .is_windows_path (source .path or "" ):
411+ path = pathlib .PureWindowsPath (source .path or "" )
412+ else :
413+ path = pathlib .PurePath (source .path or "" )
404414
405415 if path in self .breakpoints and not breakpoints and not lines :
406416 self .breakpoints .pop (path )
@@ -409,7 +419,7 @@ def set_breakpoints(
409419 tuple (breakpoints ) if breakpoints else (), tuple (lines ) if lines else ()
410420 )
411421 return [
412- Breakpoint (id = id (v ), source = Source (path = path ), verified = True , line = v .line ) for v in result .breakpoints
422+ Breakpoint (id = id (v ), source = Source (path = str ( path ) ), verified = True , line = v .line ) for v in result .breakpoints
413423 ]
414424 else :
415425 self ._logger .error ("not supported breakpoint" )
@@ -475,7 +485,7 @@ def process_start_state(self, source: str, line_no: int, type: str, status: str)
475485 self .requested_state = RequestedState .Nothing
476486
477487 if source is not None :
478- source = str (Path (source ).resolve ( ))
488+ source = self . map_path_to_client ( str (Path (source ).absolute () ))
479489 if source in self .breakpoints :
480490 breakpoints = [v for v in self .breakpoints [source ].breakpoints if v .line == line_no ]
481491 if len (breakpoints ) > 0 :
@@ -520,7 +530,7 @@ def process_start_state(self, source: str, line_no: int, type: str, status: str)
520530 body = OutputEventBody (
521531 output = message + os .linesep ,
522532 category = OutputCategory .CONSOLE ,
523- source = Source (path = source ) if source else None ,
533+ source = Source (path = str ( source ) ) if source else None ,
524534 line = line_no ,
525535 )
526536 ),
@@ -583,7 +593,7 @@ def start_output_group(self, name: str, attributes: Dict[str, Any], type: Option
583593 output = f"\u001b [38;5;14m{ (type + ' ' ) if type else '' } \u001b [0m{ name } \n " ,
584594 category = OutputCategory .CONSOLE ,
585595 group = OutputGroup .START ,
586- source = Source (path = source ) if source else None ,
596+ source = Source (path = str ( self . map_path_to_client ( source )) ) if source else None ,
587597 line = line_no if source is not None else None ,
588598 column = 0 if source is not None else None ,
589599 )
@@ -602,7 +612,7 @@ def end_output_group(self, name: str, attributes: Dict[str, Any]) -> None:
602612 output = "" ,
603613 category = OutputCategory .CONSOLE ,
604614 group = OutputGroup .END ,
605- source = Source (path = source ) if source else None ,
615+ source = Source (path = str ( self . map_path_to_client ( source )) ) if source else None ,
606616 line = line_no ,
607617 )
608618 ),
@@ -864,6 +874,30 @@ def get_threads(self) -> List[Thread]:
864874
865875 return [Thread (id = main_thread .ident if main_thread .ident else 0 , name = main_thread .name or "" )]
866876
877+ WINDOW_PATH_REGEX = re .compile (r"^(([a-z]:[\\/])|(\\\\)).*$" , re .RegexFlag .IGNORECASE )
878+
879+ @classmethod
880+ def is_windows_path (cls , path : os .PathLike [str ]) -> bool :
881+ return bool (cls .WINDOW_PATH_REGEX .fullmatch (str (path )))
882+
883+ def map_path_to_client (self , path : os .PathLike [str ]) -> pathlib .PurePath :
884+ if not self .path_mappings :
885+ return pathlib .PurePath (path )
886+
887+ for mapping in self .path_mappings :
888+
889+ remote_root_path = Path (mapping .remote_root or "." ).absolute ()
890+
891+ if Path (path ).is_relative_to (remote_root_path ):
892+ if self .is_windows_path (mapping .local_root ):
893+ local_root_path = str (pathlib .PureWindowsPath (mapping .local_root ))
894+ return pathlib .PureWindowsPath (path .replace (str (remote_root_path ), local_root_path or "" ))
895+ else :
896+ local_root_path = str (pathlib .PurePath (mapping .local_root ))
897+ return pathlib .PurePath (path .replace (str (remote_root_path ), local_root_path or "" ))
898+
899+ return path
900+
867901 def get_stack_trace (
868902 self ,
869903 thread_id : int ,
@@ -879,7 +913,7 @@ def get_stack_trace(
879913
880914 def source_from_entry (entry : StackFrameEntry ) -> Optional [Source ]:
881915 if entry .source is not None and entry .is_file :
882- return Source (path = entry .source )
916+ return Source (path = str ( self . map_path_to_client ( entry .source )) )
883917 else :
884918 return None
885919
@@ -927,7 +961,7 @@ def log_message(self, message: Dict[str, Any]) -> None:
927961 self .last_fail_message = msg
928962
929963 current_frame = self .full_stack_frames [0 ] if self .full_stack_frames else None
930- source = Source (path = current_frame .source ) if current_frame else None
964+ source = Source (path = str ( self . map_path_to_client ( current_frame .source )) ) if current_frame else None
931965 line = current_frame .line if current_frame else None
932966
933967 if self .output_log :
0 commit comments