You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
153 lines
6.3 KiB
Python
153 lines
6.3 KiB
Python
import bisect
|
|
from _pydevd_bundle.pydevd_constants import NULL, KeyifyList
|
|
import pydevd_file_utils
|
|
|
|
|
|
class SourceMappingEntry(object):
|
|
__slots__ = ["source_filename", "line", "end_line", "runtime_line", "runtime_source"]
|
|
|
|
def __init__(self, line, end_line, runtime_line, runtime_source):
|
|
assert isinstance(runtime_source, str)
|
|
|
|
self.line = int(line)
|
|
self.end_line = int(end_line)
|
|
self.runtime_line = int(runtime_line)
|
|
self.runtime_source = runtime_source # Something as <ipython-cell-xxx>
|
|
|
|
# Should be set after translated to server (absolute_source_filename).
|
|
# This is what's sent to the client afterwards (so, its case should not be normalized).
|
|
self.source_filename = None
|
|
|
|
def contains_line(self, i):
|
|
return self.line <= i <= self.end_line
|
|
|
|
def contains_runtime_line(self, i):
|
|
line_count = self.end_line + self.line
|
|
runtime_end_line = self.runtime_line + line_count
|
|
return self.runtime_line <= i <= runtime_end_line
|
|
|
|
def __str__(self):
|
|
return "SourceMappingEntry(%s)" % (", ".join("%s=%r" % (attr, getattr(self, attr)) for attr in self.__slots__))
|
|
|
|
__repr__ = __str__
|
|
|
|
|
|
class SourceMapping(object):
|
|
def __init__(self, on_source_mapping_changed=NULL):
|
|
self._mappings_to_server = {} # dict(normalized(file.py) to [SourceMappingEntry])
|
|
self._mappings_to_client = {} # dict(<cell> to File.py)
|
|
self._cache = {}
|
|
self._on_source_mapping_changed = on_source_mapping_changed
|
|
|
|
def set_source_mapping(self, absolute_filename, mapping):
|
|
"""
|
|
:param str absolute_filename:
|
|
The filename for the source mapping (bytes on py2 and str on py3).
|
|
|
|
:param list(SourceMappingEntry) mapping:
|
|
A list with the source mapping entries to be applied to the given filename.
|
|
|
|
:return str:
|
|
An error message if it was not possible to set the mapping or an empty string if
|
|
everything is ok.
|
|
"""
|
|
# Let's first validate if it's ok to apply that mapping.
|
|
# File mappings must be 1:N, not M:N (i.e.: if there's a mapping from file1.py to <cell1>,
|
|
# there can be no other mapping from any other file to <cell1>).
|
|
# This is a limitation to make it easier to remove existing breakpoints when new breakpoints are
|
|
# set to a file (so, any file matching that breakpoint can be removed instead of needing to check
|
|
# which lines are corresponding to that file).
|
|
for map_entry in mapping:
|
|
existing_source_filename = self._mappings_to_client.get(map_entry.runtime_source)
|
|
if existing_source_filename and existing_source_filename != absolute_filename:
|
|
return "Cannot apply mapping from %s to %s (it conflicts with mapping: %s to %s)" % (
|
|
absolute_filename,
|
|
map_entry.runtime_source,
|
|
existing_source_filename,
|
|
map_entry.runtime_source,
|
|
)
|
|
|
|
try:
|
|
absolute_normalized_filename = pydevd_file_utils.normcase(absolute_filename)
|
|
current_mapping = self._mappings_to_server.get(absolute_normalized_filename, [])
|
|
for map_entry in current_mapping:
|
|
del self._mappings_to_client[map_entry.runtime_source]
|
|
|
|
self._mappings_to_server[absolute_normalized_filename] = sorted(mapping, key=lambda entry: entry.line)
|
|
|
|
for map_entry in mapping:
|
|
self._mappings_to_client[map_entry.runtime_source] = absolute_filename
|
|
finally:
|
|
self._cache.clear()
|
|
self._on_source_mapping_changed()
|
|
return ""
|
|
|
|
def map_to_client(self, runtime_source_filename, lineno):
|
|
key = (lineno, "client", runtime_source_filename)
|
|
try:
|
|
return self._cache[key]
|
|
except KeyError:
|
|
for _, mapping in list(self._mappings_to_server.items()):
|
|
for map_entry in mapping:
|
|
if map_entry.runtime_source == runtime_source_filename: # <cell1>
|
|
if map_entry.contains_runtime_line(lineno): # matches line range
|
|
self._cache[key] = (map_entry.source_filename, map_entry.line + (lineno - map_entry.runtime_line), True)
|
|
return self._cache[key]
|
|
|
|
self._cache[key] = (runtime_source_filename, lineno, False) # Mark that no translation happened in the cache.
|
|
return self._cache[key]
|
|
|
|
def has_mapping_entry(self, runtime_source_filename):
|
|
"""
|
|
:param runtime_source_filename:
|
|
Something as <ipython-cell-xxx>
|
|
"""
|
|
# Note that we're not interested in the line here, just on knowing if a given filename
|
|
# (from the server) has a mapping for it.
|
|
key = ("has_entry", runtime_source_filename)
|
|
try:
|
|
return self._cache[key]
|
|
except KeyError:
|
|
for _absolute_normalized_filename, mapping in list(self._mappings_to_server.items()):
|
|
for map_entry in mapping:
|
|
if map_entry.runtime_source == runtime_source_filename:
|
|
self._cache[key] = True
|
|
return self._cache[key]
|
|
|
|
self._cache[key] = False
|
|
return self._cache[key]
|
|
|
|
def map_to_server(self, absolute_filename, lineno):
|
|
"""
|
|
Convert something as 'file1.py' at line 10 to '<ipython-cell-xxx>' at line 2.
|
|
|
|
Note that the name should be already normalized at this point.
|
|
"""
|
|
absolute_normalized_filename = pydevd_file_utils.normcase(absolute_filename)
|
|
|
|
changed = False
|
|
mappings = self._mappings_to_server.get(absolute_normalized_filename)
|
|
if mappings:
|
|
i = bisect.bisect(KeyifyList(mappings, lambda entry: entry.line), lineno)
|
|
if i >= len(mappings):
|
|
i -= 1
|
|
|
|
if i == 0:
|
|
entry = mappings[i]
|
|
|
|
else:
|
|
entry = mappings[i - 1]
|
|
|
|
if not entry.contains_line(lineno):
|
|
entry = mappings[i]
|
|
if not entry.contains_line(lineno):
|
|
entry = None
|
|
|
|
if entry is not None:
|
|
lineno = entry.runtime_line + (lineno - entry.line)
|
|
|
|
absolute_filename = entry.runtime_source
|
|
changed = True
|
|
|
|
return absolute_filename, lineno, changed
|