1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
|
from .render import RenderWalker, child_by_key
from .utils import is_newline, split_on_newlines, total_ordering
from copy import deepcopy
def position_to_path(tree, position):
"""Path to the node located at the given line and column
This function locates a node in the rendered source code
"""
return PositionFinder().find(tree, position)
def path_to_node(tree, path):
"""FST node located at the given path"""
if path is None:
return None
node = tree
for key in path:
node = child_by_key(node, key)
return node
def position_to_node(tree, position):
"""FST node located at the given line and column"""
return path_to_node(tree, position_to_path(tree, position))
def node_to_bounding_box(node):
"""Bounding box of the given node
The bounding box of a node represents its left most and right most
position in the rendered source code. Its left position is here
always (1, 1).
"""
return BoundingBoxFinder().compute(node)
def path_to_bounding_box(tree, path):
"""Absolute bounding box of the node located at the given path"""
return BoundingBoxFinder().compute(tree, path)
@total_ordering
class Position(object):
"""Handles a cursor's line and column
Operations requiring another Position as argument can be given an
indexable object of len >= 2 where the index 0 contains the line and
the index 1 contains the column. For example a tuple of len 2.
"""
def __init__(self, position):
if hasattr(position, 'line') and hasattr(position, 'column'):
self.line = position.line
self.column = position.column
elif len(position) >= 2:
self.line = position[0]
self.column = position[1]
else:
raise AttributeError(position)
def advance_columns(self, columns):
"""(3, 10) -> (3, 11)"""
self.column += columns
def advance_line(self):
"""(3, 10) -> (4, 1)"""
self.line += 1
self.column = 1
@property
def left(self):
"""(3, 10) -> (3, 9)"""
return Position((self.line, self.column - 1))
@property
def right(self):
"""(3, 10) -> (3, 11)"""
return Position((self.line, self.column + 1))
def __add__(self, other):
"""(1, 1) + (1, 1) -> (2, 2)"""
other = Position(other)
return Position((self.line + other.line,
self.column + other.column))
def __neg__(self):
"""(1, -1) -> (-1, 1)"""
return Position((-self.line, -self.column))
def __sub__(self, other):
"""(1, 1) - (1, 1) -> (0, 0)"""
other = Position(other)
return Position((self.line - other.line,
self.column - other.column))
def __nonzero__(self):
return self.line >= 0 and self.column >= 0
def __bool__(self):
return self.__nonzero__()
def __eq__(self, other):
"""Compares Positions or Position and tuple
Will not fail if other is an unsupported type"""
if not (hasattr(other, 'line') and hasattr(other, 'column')) and len(other) < 2:
return False
other = Position(other)
return self.line == other.line and self.column == other.column
def __lt__(self, other):
"""Compares Position with Position or indexable object"""
other = Position(other)
return (self.line, self.column) < (other.line, other.column)
def __repr__(self):
return 'Position (%s, %s)' % (str(self.line), str(self.column))
def to_tuple(self):
return (self.line, self.column)
class BoundingBox(object):
"""Handles a selection's top_left and bottom_right position
Operations requiring another BoundingBox as argument can be given an
indexable object of len >= 2 where the index 0 contains the top_left
position, either as a Position or an indexable object. The index
1 must contain, in a similar manner, the bottom_right position.
"""
def __init__(self, bounding_box):
if hasattr(bounding_box, 'top_left') and hasattr(bounding_box, 'bottom_right'):
self.top_left = Position(bounding_box.top_left)
self.bottom_right = Position(bounding_box.bottom_right)
elif len(bounding_box) >= 2:
self.top_left = Position(bounding_box[0])
self.bottom_right = Position(bounding_box[1])
else:
raise AttributeError(bounding_box)
def __eq__(self, other):
"""Compares BoundingBox with BoundingBox or indexable object"""
if not (hasattr(other, 'top_left') and hasattr(other, 'bottom_right')) and len(other) < 2:
return False
other = BoundingBox(other)
return self.top_left == other.top_left and self.bottom_right == other.bottom_right
def __repr__(self):
return 'BoundingBox (%s, %s)' % (str(self.top_left), str(self.bottom_right))
class PathWalker(RenderWalker):
"""Gives the current path while walking the rendered tree
It adds an attribute "current_path" which is updated each time the
walker takes a step.
"""
def walk(self, tree):
self.current_path = []
super(PathWalker, self).walk(tree)
def before(self, key_type, item, render_key):
if render_key is not None:
self.current_path.append(render_key)
return super(PathWalker, self).before(key_type, item, render_key)
def after(self, key_type, item, render_key):
stop = super(PathWalker, self).after(key_type, item, render_key)
if render_key is not None:
self.current_path.pop()
return stop
class PositionFinder(PathWalker):
"""Find a node by line and column and return the path to it.
First, walk through all the nodes while maintaining the current line
and column. When the targetted node is found, stop there and build
the path while going back up the tree.
"""
def find(self, tree, position):
self.current = Position((1, 1))
self.target = Position(position)
self.found_path = None
self.walk(tree)
return self.found_path
def before_constant(self, constant, key):
"""Determine if we're on the targetted node.
If the targetted column is reached, `stop` and `path_found` are
set. If the targetted line is passed, only `stop` is set. This
prevents unnecessary tree travelling when the targetted column
is out of bounds.
"""
newlines_split = split_on_newlines(constant)
for c in newlines_split:
if is_newline(c):
self.current.advance_line()
# if target line is passed
if self.current.line > self.target.line:
return self.STOP
else:
advance_by = len(c)
if self.is_on_targetted_node(advance_by):
self.found_path = deepcopy(self.current_path)
return self.STOP
self.current.advance_columns(advance_by)
before_string = before_constant
def is_on_targetted_node(self, advance_by):
return self.target.line == self.current.line \
and self.target.column >= self.current.column \
and self.target.column < self.current.column + advance_by
class BoundingBoxFinder(PathWalker):
"""Compute the bounding box of the given node.
First, walk to the target path while incrementing the position.
When reached, the top-left position is set to the current position.
Then walk the whole node, still incrementing the position. When
arriving at the end of the node, store the previous position, not
the current one, as the bottom-right position.
If no target path is given, assume the targetted node is the whole
tree.
"""
def compute(self, tree, target_path=None):
self.target_path = target_path
self.current_position = Position((1, 1))
self.left_of_current_position = Position((1, 0))
self.top_left = None
self.bottom_right = None
self.found = True if self.target_path is None or len(target_path) == 0 else False
self.walk(tree)
if self.found and self.top_left is None and self.bottom_right is None:
return BoundingBox((Position((1, 1)), self.left_of_current_position))
return BoundingBox((self.top_left, self.bottom_right))
def before(self, key_type, item, render_key):
stop = super(BoundingBoxFinder, self).before(key_type, item, render_key)
if self.current_path == self.target_path:
self.found = True
self.top_left = deepcopy(self.current_position)
if key_type not in ['constant', 'string']:
return stop
newlines_split = split_on_newlines(item)
for c in newlines_split:
if is_newline(c):
self.current_position.advance_line()
self.left_of_current_position = self.current_position.left
elif c != "":
self.current_position.advance_columns(len(c))
self.left_of_current_position = self.current_position.left
return stop
def after(self, key_type, item, render_key):
if self.bottom_right is None and self.found and self.current_path == self.target_path:
self.bottom_right = deepcopy(self.left_of_current_position)
return super(BoundingBoxFinder, self).after(key_type, item, render_key)
|