# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """Snippet Abstract Syntax Tree (AST) nodes and definitions.""" import re BACKSLASH_REPLACE_REGEX = re.compile(r'(\\)([^\\\s])') # ------------------------ Misc functions ------------------------------------- def _compute_offset_str(offset, value): line, col = offset mark_for_position = True if value == '\n': line += 1 col = 0 mark_for_position = False elif value == '\r': mark_for_position = False else: col += len(value) return (line, col), mark_for_position # ------------------------ ASTNode identifiers -------------------------------- class SnippetKind: TABSTOP = 'tabstop' PLACEHOLDER = 'placeholder' CHOICE = 'choice' VARIABLE = 'variable' VARIABLE_PLACEHOLDER = 'variable_placeholder' REGEX = 'regex' class FormatKind: SIMPLE = 'simple' IF = 'if' IF_ELSE = 'if_else' ELSE = 'else' class NodeKind: TEXT = 'text' LEAF = 'leaf' FORMAT = 'format' # ------------------------- Base AST Node classes ----------------------------- class ASTNode: """ Base class that represents a node on a snippet AST. All other nodes should extend this class directly or indirectly. """ # Node string identifier # Status: Required KIND = None def __init__(self, position=((0, 0), (0, 0))): self.position = position self.parent = None self.mark_for_position = True self.index_in_parent = -1 self.to_delete = False self.depth = 0 self.name = '' self.value = '' def update_position(self, position): """Updates node text position.""" self.position = position def compute_position(self, offset): """Given a (line, col) position, compute actual node position.""" return offset def update(self, value): """ Update a node value or representation. Downstream classes can override this method if necessary. """ pass def text(self): """ This function should return a string that represents the current node. Downstream classes can override this method if necessary. """ pass def delete(self): """Mark an AST node for deletion.""" self.to_delete = True def accept(self, visitor): """Accept visitor to iterate through the AST.""" visitor.visit(self) class TextNode(ASTNode): """ AST node representing a text sequence. The sequence is composed of one or more LeafNodes or any ASTNode. """ KIND = NodeKind.TEXT def __init__(self, *tokens): ASTNode.__init__(self) self._tokens = tokens for i, token in enumerate(tokens): token.index_in_parent = i token.parent = self token.depth = self.depth + 1 @property def tokens(self): return self._tokens @tokens.setter def tokens(self, tokens): self._tokens = tokens for i, token in enumerate(tokens): token.index_in_parent = i token.depth = self.depth + 1 token.parent = self def compute_position(self, offset): polygon = [] current_offset = offset for i, token in enumerate(self._tokens): token.depth = self.depth + 1 current_offset = token.compute_position(current_offset) if token.mark_for_position: position = token.position if i == len(self._tokens) - 1: if len(position) == 1: if isinstance(position, list): position = position[0] x, y = position[0] position = ((x, y), (x, y + 1)) if isinstance(token, LeafNode): if token.name == 'EPSILON': position = ((x, y), (x, y)) polygon += list(position) flatten_polygon = [] for segment in polygon: if isinstance(segment, list): flatten_polygon += segment else: flatten_polygon.append(segment) segments = [] current_segment = [] current_x = None current_y = None previous_x = None for x, y in flatten_polygon: if current_x is None: previous_x = x current_segment.append((x, y)) elif x == current_x + 1: current_segment.append((current_x, current_y)) segments.append(current_segment) current_segment = [(x, y)] previous_x = x current_x, current_y = x, y if current_x == previous_x: if len(current_segment) > 0: current_segment.append((current_x, current_y)) if len(current_segment) > 0: segments.append(current_segment) self.position = segments return current_offset def text(self): return ''.join([token.text() for token in self._tokens]) def accept(self, visitor): visitor.visit(self) for token in self._tokens: token.accept(visitor) def delete(self): self.to_delete = True for token in self.tokens: token.delete() class LeafNode(ASTNode): """Node that represents a terminal symbol.""" KIND = NodeKind.LEAF def __init__(self, name='EPSILON', value=''): ASTNode.__init__(self) self.name = name self.value = value def compute_position(self, offset): value = self.text() new_offset, mark_for_position = _compute_offset_str(offset, value) self.mark_for_position = mark_for_position if len(self.value) == 1: self.position = (offset,) else: self.position = (offset, new_offset) return new_offset def text(self): text = BACKSLASH_REPLACE_REGEX.sub(r'\2', self.value) if self.name == 'left_curly_name': text = text[1:] return text def __str__(self): return 'LeafNode({0}: {1})'.format(self.name, self.value) def __repr__(self): return r'{0}'.format(self.__str__()) class SnippetASTNode(ASTNode): """ Stub node that represents an actual snippet. Used to unify type hierarchies between int and variable snippets. """ pass class FormatNode(ASTNode): """ Base regex formatting node. All regex formatting nodes should extend this class. """ def transform_regex(self, regex_result): """ Transform a regex match. This method takes a regex result and applies some transformation to return a new string. """ return '' # -------------------------- Int snippet node classes ------------------------- class TabstopSnippetNode(SnippetASTNode): """ Node that represents an int tabstop snippet. This node represents the expressions ${int} or $int. """ KIND = SnippetKind.TABSTOP # DEFAULT_PLACEHOLDER = TextNode(LeafNode()) def __init__(self, number, placeholder=None): SnippetASTNode.__init__(self) default_placeholder = TextNode(LeafNode()) self.number = int(number.value) self._placeholder = (placeholder if placeholder is not None else default_placeholder) self._placeholder.parent = self self._placeholder.depth = self.depth + 1 @property def placeholder(self): return self._placeholder @placeholder.setter def placeholder(self, placeholder): self._placeholder = placeholder self._placeholder.depth = self.depth + 1 self._placeholder.parent = self def compute_position(self, offset): if isinstance(self._placeholder, ASTNode): self._placeholder.depth = self.depth + 1 end_position = self._placeholder.compute_position(offset) elif isinstance(self._placeholder, str): end_position, _ = _compute_offset_str(offset, self._placeholder) # self.position = (offset, end_position) self.position = self._placeholder.position return end_position def update(self, new_placeholder): self._placeholder = new_placeholder def text(self): return self._placeholder.text() def accept(self, visitor): visitor.visit(self) self._placeholder.accept(visitor) def delete(self): self.to_delete = True self._placeholder.delete() class PlaceholderNode(TabstopSnippetNode): """ Node that represents an int tabstop placeholder snippet. This node represents the expression ${int: placeholder}, where placeholder can be a snippet or text. """ KIND = SnippetKind.PLACEHOLDER def __init__(self, number, placeholder=''): TabstopSnippetNode.__init__(self, number, placeholder) def text(self): if isinstance(self._placeholder, str): return self._placeholder elif isinstance(self._placeholder, ASTNode): return self._placeholder.text() else: raise ValueError('Placeholder should be of type ' 'SnippetASTNode or str, got {0}'.format( type(self._placeholder))) class ChoiceNode(TabstopSnippetNode): """ Node that represents an int tabstop choice snippet. This node represents the expression ${int:|options|}, where options are text sequences separated by comma. """ KIND = SnippetKind.CHOICE def __init__(self, number, *choices): TabstopSnippetNode.__init__(self, number, choices[0]) self.current_choice = choices[0] self.choices = choices def update(self, choice): if choice not in self.choices: # TODO: Maybe we should display this as a warning # instead of raising an exception. raise LookupError('Choice {0} is not a valid value for this ' 'snippet, expected any of {1}'.format( choice, self.choices)) self.current_choice = choice self._placeholder = choice # --------------------- Variable snippet node classes ------------------------- class VariableSnippetNode(SnippetASTNode): """ Node that represents a variable snippet. This node represents the expression ${var} or $var, where var is some variable qualified name. """ KIND = SnippetKind.VARIABLE def __init__(self, variable): SnippetASTNode.__init__(self) self.variable = variable self.value = variable def update(self, value): self.value = value def text(self): return self.value class VariablePlaceholderNode(VariableSnippetNode): """ Node that represents a variable placeholder snippet. This node represents the expression ${var: placeholder}, where placeholder can be a snippet or text. """ KIND = SnippetKind.VARIABLE_PLACEHOLDER def __init__(self, variable, placeholder): VariableSnippetNode.__init__(self, variable) self._placeholder = placeholder def update(self, placeholder): self._placeholder = placeholder def text(self): if isinstance(self._placeholder, str): return self._placeholder elif isinstance(self._placeholder, ASTNode): # FIXME: Implement placeholder composition once # microsoft/language-server-protocol#801 is clarified return self._placeholder.text() class RegexNode(VariableSnippetNode): """ Node that represents a variable regex transformation snippet. This node represents the expression ${var/regex/format/options}, where regex is a PCRE-valid regex expression, format corresponds to a FormatNode and options is a TextNode containing valid regex options. """ KIND = SnippetKind.REGEX def __init__(self, variable, regex, fmt, options): VariableSnippetNode.__init__(self, variable) self.regex = re.compile(regex.text()) self.format = fmt self.options = options def text(self): # FIXME: Implement regex variable placeholder composition once # microsoft/language-server-protocol#801 is clarified raise NotImplementedError('Regex variable snippets are ' 'not currently implemented') # -------------------- Regex formatting node classes -------------------------- class FormatSequenceNode(FormatNode): """Node that represents a sequence of formatting or text nodes.""" KIND = FormatKind.SIMPLE def __init__(self, *formatting_nodes): FormatNode.__init__(self) self.formatting_nodes = formatting_nodes def add_format(self, fmt): self.formatting_nodes.append(fmt) def transform_regex(self, regex_result): result = '' for fmt in self.formatting_nodes: if isinstance(fmt, TextNode): result += fmt.text() elif isinstance(fmt, FormatNode): result += fmt.transform_regex(regex_result) return result def accept(self, visitor): visitor.visit(self) for fmt in self.formatting_nodes: visitor.visit(fmt) class SimpleFormatNode(FormatNode): """ Extract a single group from a regex match. This node represents the expression $int or ${int} where int corresponds to a group on a regex match. """ KIND = NodeKind.FORMAT def __init__(self, group_number): FormatNode.__init__(self) self.group_number = group_number def transform_regex(self, regex_result): return regex_result.group(self.group_number) class IfFormatNode(SimpleFormatNode): """ Choose a string if a regex group was found. This node represents the expression ${group :+ value_if_exists}, where value_if_exists is evaluated if $group is present on the regex match. """ KIND = FormatKind.IF def __init__(self, group_number, positive_match): SimpleFormatNode.__init__(self, group_number) self.positive_match = positive_match def transform_regex(self, regex_result): result = '' if regex_result.group(self.group_number) is not None: result = self.positive_match.transform_regex(regex_result) return result class IfElseNode(SimpleFormatNode): """ Choose a string if a regex group was found, otherwise choose other. This node represents the expression ${group ?: value_if_exists : value_otherwise}, where value_if_exists is evaluated if $group is present on the regex match, otherwise, the node value_otherwise is evaluated. """ KIND = FormatKind.IF_ELSE def __init__(self, group_number, positive_match, negative_match): SimpleFormatNode.__init__(self, group_number) self.positive_match = positive_match self.negative_match = negative_match def transform_regex(self, regex_result): result = '' if regex_result.group(self.group_number) is not None: result = self.positive_match.transform_regex(regex_result) else: result = self.negative_match.transform_regex(regex_result) return result class ElseNode(SimpleFormatNode): """ Choose a string if a regex group was not found. This node represents the expression ${group :- value_if_not_exists}, where value_if_not_exists is evaluated if $group is not present on the regex match, otherwise the group value is returned. """ KIND = FormatKind.ELSE def __init__(self, group_number, negative_match): SimpleFormatNode.__init__(self, group_number) self.negative_match = negative_match def transform_regex(self, regex_result): result = '' if regex_result.group(self.group_number) is None: result = self.negative_match.transform_regex(regex_result) else: result = regex_result.group(self.group_number) return result