def _on_key_pressed(self, event): """ Override key press to select the current scope if the user wants to deleted a folded scope (without selecting it). """ delete_request = event.key() in [Qt.Key_Backspace, Qt.Key_Delete] if event.text() or delete_request: cursor = self.editor.textCursor() if cursor.hasSelection(): # change selection to encompass the whole scope. positions_to_check = cursor.selectionStart( ), cursor.selectionEnd() else: positions_to_check = (cursor.position(), ) for pos in positions_to_check: block = self.editor.document().findBlock(pos) th = TextBlockHelper() if th.is_fold_trigger(block) and th.is_collapsed(block): self.toggle_fold_trigger(block) if delete_request and cursor.hasSelection(): scope = FoldScope(self.find_parent_scope(block)) tc = TextHelper( self.editor).select_lines(*scope.get_range()) if tc.selectionStart() > cursor.selectionStart(): start = cursor.selectionStart() else: start = tc.selectionStart() if tc.selectionEnd() < cursor.selectionEnd(): end = cursor.selectionEnd() else: end = tc.selectionEnd() tc.setPosition(start) tc.setPosition(end, tc.KeepAnchor) self.editor.setTextCursor(tc)
def find_parent_scope(block): """ Find parent scope, if the block is not a fold trigger. :param block: block from which the research will start """ # if we moved up for more than n lines, just give up otherwise this # would take too much time. limit = 5000 counter = 0 original = block if not TextBlockHelper.is_fold_trigger(block): # search level of next non blank line while block.text().strip() == '' and block.isValid(): block = block.next() ref_lvl = TextBlockHelper.get_fold_lvl(block) - 1 block = original while (block.blockNumber() and counter < limit and (not TextBlockHelper.is_fold_trigger(block) or TextBlockHelper.get_fold_lvl(block) > ref_lvl)): counter += 1 block = block.previous() if counter < limit: return block return None
def print_tree(editor, file=sys.stdout, print_blocks=False, return_list=False): """ Prints the editor fold tree to stdout, for debugging purpose. :param editor: CodeEditor instance. :param file: file handle where the tree will be printed. Default is stdout. :param print_blocks: True to print all blocks, False to only print blocks that are fold triggers """ output_list = [] block = editor.document().firstBlock() while block.isValid(): trigger = TextBlockHelper().is_fold_trigger(block) trigger_state = TextBlockHelper().is_collapsed(block) lvl = TextBlockHelper().get_fold_lvl(block) visible = 'V' if block.isVisible() else 'I' if trigger: trigger = '+' if trigger_state else '-' if return_list: output_list.append([block.blockNumber() + 1, lvl, visible]) else: print('l%d:%s%s%s' % (block.blockNumber() + 1, lvl, trigger, visible), file=file) elif print_blocks: if return_list: output_list.append([block.blockNumber() + 1, lvl, visible]) else: print('l%d:%s%s' % (block.blockNumber() + 1, lvl, visible), file=file) block = block.next() if return_list: return output_list
def collapse_all(self): """ Collapses all triggers and makes all blocks with fold level > 0 invisible. """ self._clear_block_deco() block = self.editor.document().firstBlock() last = self.editor.document().lastBlock() while block.isValid(): lvl = TextBlockHelper.get_fold_lvl(block) trigger = TextBlockHelper.is_fold_trigger(block) if trigger: if lvl == 0: self._show_previous_blank_lines(block) TextBlockHelper.set_collapsed(block, True) block.setVisible(lvl == 0) if block == last and block.text().strip() == '': block.setVisible(True) self._show_previous_blank_lines(block) block = block.next() self._refresh_editor_and_scrollbars() tc = self.editor.textCursor() tc.movePosition(tc.Start) self.editor.setTextCursor(tc) self.collapse_all_triggered.emit()
def fold(self): """Folds the region.""" start, end = self.get_range() TextBlockHelper.set_collapsed(self._trigger, True) block = self._trigger.next() while block.blockNumber() <= end and block.isValid(): block.setVisible(False) block = block.next()
def unfold(self): """Unfolds the region.""" # set all direct child blocks which are not triggers to be visible self._trigger.setVisible(True) TextBlockHelper.set_collapsed(self._trigger, False) for block in self.blocks(ignore_blank_lines=False): block.setVisible(True) if TextBlockHelper.is_fold_trigger(block): TextBlockHelper.set_collapsed(block, False)
def expand_all(self): """Expands all fold triggers.""" block = self.editor.document().firstBlock() while block.isValid(): TextBlockHelper.set_collapsed(block, False) block.setVisible(True) block = block.next() self._clear_block_deco() self._refresh_editor_and_scrollbars() self.expand_all_triggered.emit()
def child_regions(self): """This generator generates the list of direct child regions.""" start, end = self.get_range() block = self._trigger.next() ref_lvl = self.scope_level while block.blockNumber() <= end and block.isValid(): lvl = TextBlockHelper.get_fold_lvl(block) trigger = TextBlockHelper.is_fold_trigger(block) if lvl == ref_lvl and trigger: yield FoldScope(block) block = block.next()
def detect_fold_level(self, prev_block, block): if prev_block: prev_text = prev_block.text().strip() else: prev_text = '' text = block.text().strip() if text in self.open_chars: return TextBlockHelper.get_fold_lvl(prev_block) + 1 if prev_text.endswith(self.open_chars) and prev_text not in \ self.open_chars: return TextBlockHelper.get_fold_lvl(prev_block) + 1 if self.close_chars in prev_text: return TextBlockHelper.get_fold_lvl(prev_block) - 1 return TextBlockHelper.get_fold_lvl(prev_block)
def find_parent_scope(block): """Find parent scope, if the block is not a fold trigger.""" original = block if not TextBlockHelper.is_fold_trigger(block): # search level of next non blank line while block.text().strip() == '' and block.isValid(): block = block.next() ref_lvl = TextBlockHelper.get_fold_lvl(block) - 1 block = original while (block.blockNumber() and (not TextBlockHelper.is_fold_trigger(block) or TextBlockHelper.get_fold_lvl(block) > ref_lvl)): block = block.previous() return block
def paintEvent(self, event): """Override Qt method.""" painter = QPainter(self) color = QColor(self.color) color.setAlphaF(.5) painter.setPen(color) offset = self.editor.document().documentMargin() + \ self.editor.contentOffset().x() for _, line_number, block in self.editor.visible_blocks: indentation = TextBlockHelper.get_fold_lvl(block) ref_lvl = indentation block = block.next() last_line = block.blockNumber() lvl = TextBlockHelper.get_fold_lvl(block) if ref_lvl == lvl: # for zone set programmatically such as imports # in pyqode.python ref_lvl -= 1 while (block.isValid() and TextBlockHelper.get_fold_lvl(block) > ref_lvl): last_line = block.blockNumber() block = block.next() end_of_sub_fold = block if last_line: block = block.document().findBlockByNumber(last_line) while ((block.blockNumber()) and (block.text().strip() == '' or block.text().strip().startswith('#'))): block = block.previous() last_line = block.blockNumber() block = self.editor.document().findBlockByNumber(line_number) top = int(self.editor.blockBoundingGeometry(block).translated( self.editor.contentOffset()).top()) bottom = top + int(self.editor.blockBoundingRect(block).height()) indentation = TextBlockHelper.get_fold_lvl(block) for i in range(1, indentation): if (line_number > last_line and TextBlockHelper.get_fold_lvl(end_of_sub_fold) <= i): continue else: x = self.editor.fontMetrics().width(i * self.i_width * '9') + offset painter.drawLine(x, top, x, bottom)
def get_range(self, ignore_blank_lines=True): """ Gets the fold region range (start and end line). .. note:: Start line do no encompass the trigger line. :param ignore_blank_lines: True to ignore blank lines at the end of the scope (the method will rewind to find that last meaningful block that is part of the fold scope). :returns: tuple(int, int) """ ref_lvl = self.trigger_level first_line = self._trigger.blockNumber() block = self._trigger.next() last_line = block.blockNumber() lvl = self.scope_level if ref_lvl == lvl: # for zone set programmatically such as imports # in pyqode.python ref_lvl -= 1 while (block.isValid() and TextBlockHelper.get_fold_lvl(block) > ref_lvl): last_line = block.blockNumber() block = block.next() if ignore_blank_lines and last_line: block = block.document().findBlockByNumber(last_line) while block.blockNumber() and block.text().strip() == '': block = block.previous() last_line = block.blockNumber() return first_line, last_line
def _get_fold_levels(editor): """ Return a list of all the class/function definition ranges. Parameters ---------- editor : :class:`spyder.plugins.editor.widgets.codeeditor.CodeEditor` Returns ------- folds : list of :class:`FoldScopeHelper` A list of all the class or function defintion fold points. """ folds = [] parents = [] prev = None for oedata in editor.outlineexplorer_data_list(): if TextBlockHelper.is_fold_trigger(oedata.block): try: if oedata.def_type in (OED.CLASS, OED.FUNCTION): fsh = FoldScopeHelper(FoldScope(oedata.block), oedata) # Determine the parents of the item using a stack. _adjust_parent_stack(fsh, prev, parents) # Update the parents of this FoldScopeHelper item fsh.parents = copy.copy(parents) folds.append(fsh) prev = fsh except KeyError: pass return folds
def detect_fold_level(self, prev_block, block): """ Detects fold level by looking at the block indentation. :param prev_block: previous text block :param block: current block to highlight """ text = block.text() prev_lvl = TextBlockHelper().get_fold_lvl(prev_block) cont_line_regex = (r"(and|or|'|\+|\-|\*|\^|>>|<<|" r"\*|\*{2}|\||\*|//|/|,|\\|\")$") # round down to previous indentation guide to ensure contiguous block # fold level evolution. indent_len = 0 if (prev_lvl and prev_block is not None and not self.editor.is_comment(prev_block)): # ignore commented lines (could have arbitary indentation) prev_text = prev_block.text() indent_len = (len(prev_text) - len(prev_text.lstrip())) // prev_lvl # Verify if the previous line ends with a continuation line # with a regex if (re.search(cont_line_regex, prev_block.text()) and indent_len > prev_lvl): # Calculate act level of line act_lvl = (len(text) - len(text.lstrip())) // indent_len if act_lvl == prev_lvl: # If they are the same, don't change the level return prev_lvl if prev_lvl > act_lvl: return prev_lvl - 1 return prev_lvl + 1 if indent_len == 0: indent_len = len(self.editor.indent_chars) act_lvl = (len(text) - len(text.lstrip())) // indent_len return act_lvl
def scope_level(self): """ Returns the fold level of the first block of the foldable scope ( just after the trigger). :return: """ return TextBlockHelper.get_fold_lvl(self._trigger.next())
def parent(self): """ Return the parent scope. :return: FoldScope or None """ if TextBlockHelper.get_fold_lvl(self._trigger) > 0 and \ self._trigger.blockNumber(): block = self._trigger.previous() ref_lvl = self.trigger_level - 1 while (block.blockNumber() and (not TextBlockHelper.is_fold_trigger(block) or TextBlockHelper.get_fold_lvl(block) > ref_lvl)): block = block.previous() try: return FoldScope(block) except ValueError: return None return None
def paintEvent(self, event): # Paints the fold indicators and the possible fold region background # on the folding panel. super(FoldingPanel, self).paintEvent(event) painter = QPainter(self) # Draw background over the selected non collapsed fold region if self._mouse_over_line is not None: block = self.editor.document().findBlockByNumber( self._mouse_over_line) try: self._draw_fold_region_background(block, painter) except ValueError: pass # Draw fold triggers for top_position, line_number, block in self.editor.visible_blocks: if TextBlockHelper.is_fold_trigger(block): collapsed = TextBlockHelper.is_collapsed(block) mouse_over = self._mouse_over_line == line_number self._draw_fold_indicator(top_position, mouse_over, collapsed, painter) if collapsed: # check if the block already has a decoration, it might # have been folded by the parent editor/document in the # case of cloned editor for deco in self._block_decos: if deco.block == block: # no need to add a deco, just go to the next block break else: self._add_fold_decoration(block, FoldScope(block)) else: for deco in self._block_decos: # check if the block decoration has been removed, it # might have been unfolded by the parent # editor/document in the case of cloned editor if deco.block == block: # remove it and self._block_decos.remove(deco) self.editor.decorations.remove(deco) del deco break
def __init__(self, block): """ Create a fold-able region from a fold trigger block. :param block: The block **must** be a fold trigger. :type block: QTextBlock :raise: `ValueError` if the text block is not a fold trigger. """ if not TextBlockHelper.is_fold_trigger(block): raise ValueError('Not a fold trigger') self._trigger = block
def _highlight_block(self, block): """ Highlights the current fold scope. :param block: Block that starts the current fold scope. """ scope = FoldScope(block) if (self._current_scope is None or self._current_scope.get_range() != scope.get_range()): self._current_scope = scope self._clear_scope_decos() # highlight current scope with darker or lighter color start, end = scope.get_range() if not TextBlockHelper.is_collapsed(block): self._decorate_block(start, end)
def paintEvent(self, event): """Override Qt method.""" painter = QPainter(self) color = QColor(self.color) color.setAlphaF(.5) painter.setPen(color) for top, line_number, block in self.editor.visible_blocks: bottom = top + int(self.editor.blockBoundingRect(block).height()) indentation = TextBlockHelper.get_fold_lvl(block) for i in range(1, indentation): x = self.editor.fontMetrics().width(i * self.i_width * '9') painter.drawLine(x, top, x, bottom)
def toggle_fold_trigger(self, block): """ Toggle a fold trigger block (expand or collapse it). :param block: The QTextBlock to expand/collapse """ if not TextBlockHelper.is_fold_trigger(block): return region = FoldScope(block) if region.collapsed: region.unfold() if self._mouse_over_line is not None: self._decorate_block(*region.get_range()) else: region.fold() self._clear_scope_decos() self._refresh_editor_and_scrollbars() self.trigger_state_changed.emit(region._trigger, region.collapsed)
class TestFoldScopeHelper(object): test_case = """# -*- coding: utf-8 -*- def my_add(): a = 1 b = 2 return a + b """ doc = QTextDocument(test_case) sh = PythonSH(doc, color_scheme='Spyder') sh.fold_detector = IndentFoldDetector() sh.rehighlightBlock(doc.firstBlock()) block = doc.firstBlock() block = block.next() TextBlockHelper.set_fold_trigger(block, True) fold_scope = FoldScope(block) oed = block.userData().oedata def test_fold_scope_helper(self): fsh = cfd.FoldScopeHelper(None, None) assert isinstance(fsh, cfd.FoldScopeHelper) def test_fold_scope_helper_str(self): fsh = cfd.FoldScopeHelper(self.fold_scope, self.oed) assert "my_add" in str(fsh) def test_fold_scope_helper_str_with_parents(self): fsh = cfd.FoldScopeHelper(self.fold_scope, self.oed) fsh.parents = ["fake parent list!"] assert "parents:" in str(fsh) def test_fold_scope_helper_repr(self): fsh = cfd.FoldScopeHelper(self.fold_scope, self.oed) assert "(at 0x" in repr(fsh) def test_fold_scope_helper_properties(self): fsh = cfd.FoldScopeHelper(self.fold_scope, self.oed) assert fsh.range == (1, 4) assert fsh.start_line == 1 assert fsh.end_line == 4 assert fsh.name == "my_add" assert fsh.line == 1 assert fsh.def_type == OED.FUNCTION_TOKEN
def _get_fold_levels(editor): """ Return a list of all the class/function definition ranges. Parameters ---------- editor : :class:`spyder.plugins.editor.widgets.codeeditor.CodeEditor` Returns ------- folds : list of :class:`FoldScopeHelper` A list of all the class or function defintion fold points. """ block = editor.document().firstBlock() oed = editor.get_outlineexplorer_data() folds = [] parents = [] prev = None while block.isValid(): if TextBlockHelper.is_fold_trigger(block): try: data = oed[block.firstLineNumber()] if data.def_type in (OED.CLASS, OED.FUNCTION): fsh = FoldScopeHelper(FoldScope(block), data) # Determine the parents of the item using a stack. _adjust_parent_stack(fsh, prev, parents) # Update the parents of this FoldScopeHelper item fsh.parents = copy.copy(parents) folds.append(fsh) prev = fsh except KeyError: pass block = block.next() return folds
def _highlight_caret_scope(self): """ Highlight the scope of the current caret position. This get called only if :attr:` spyder.widgets.panels.FoldingPanel.highlight_care_scope` is True. """ cursor = self.editor.textCursor() block_nbr = cursor.blockNumber() if self._block_nbr != block_nbr: block = FoldScope.find_parent_scope( self.editor.textCursor().block()) try: s = FoldScope(block) except ValueError: self._clear_scope_decos() else: self._mouse_over_line = block.blockNumber() if TextBlockHelper.is_fold_trigger(block): self._highlight_block(block) self._block_nbr = block_nbr
def detect_fold_level(self, prev_block, block): """ Detects fold level by looking at the block indentation. :param prev_block: previous text block :param block: current block to highlight """ text = block.text() prev_lvl = TextBlockHelper().get_fold_lvl(prev_block) # round down to previous indentation guide to ensure contiguous block # fold level evolution. indent_len = 0 if (prev_lvl and prev_block is not None and not self.editor.is_comment(prev_block)): # ignore commented lines (could have arbitary indentation) prev_text = prev_block.text() indent_len = (len(prev_text) - len(prev_text.lstrip())) // prev_lvl if indent_len == 0: indent_len = len(self.editor.indent_chars) return (len(text) - len(text.lstrip())) // indent_len
def mouseMoveEvent(self, event): """ Detect mouser over indicator and highlight the current scope in the editor (up and down decoration arround the foldable text when the mouse is over an indicator). :param event: event """ super(FoldingPanel, self).mouseMoveEvent(event) th = TextHelper(self.editor) line = th.line_nbr_from_position(event.pos().y()) if line >= 0: block = FoldScope.find_parent_scope( self.editor.document().findBlockByNumber(line - 1)) if TextBlockHelper.is_fold_trigger(block): if self._mouse_over_line is None: # mouse enter fold scope QApplication.setOverrideCursor( QCursor(Qt.PointingHandCursor)) if self._mouse_over_line != block.blockNumber() and \ self._mouse_over_line is not None: # fold scope changed, a previous block was highlighter so # we quickly update our highlighting self._mouse_over_line = block.blockNumber() self._highlight_block(block) else: # same fold scope, request highlight self._mouse_over_line = block.blockNumber() self._highlight_runner.request_job(self._highlight_block, block) self._highight_block = block else: # no fold scope to highlight, cancel any pending requests self._highlight_runner.cancel_requests() self._mouse_over_line = None QApplication.restoreOverrideCursor() self.repaint()
def process_block(self, current_block, previous_block, text): """ Processes a block and setup its folding info. This method call ``detect_fold_level`` and handles most of the tricky corner cases so that all you have to do is focus on getting the proper fold level foreach meaningful block, skipping the blank ones. :param current_block: current block to process :param previous_block: previous block :param text: current block text """ prev_fold_level = TextBlockHelper.get_fold_lvl(previous_block) if text.strip() == '' or self.editor.is_comment(current_block): # blank or comment line always have the same level # as the previous line fold_level = prev_fold_level else: fold_level = self.detect_fold_level( previous_block, current_block) if fold_level > self.limit: fold_level = self.limit prev_fold_level = TextBlockHelper.get_fold_lvl(previous_block) if fold_level > prev_fold_level: # apply on previous blank or comment lines block = current_block.previous() while block.isValid() and (block.text().strip() == '' or self.editor.is_comment(block)): TextBlockHelper.set_fold_lvl(block, fold_level) block = block.previous() TextBlockHelper.set_fold_trigger( block, True) # update block fold level if text.strip() and not self.editor.is_comment(previous_block): TextBlockHelper.set_fold_trigger( previous_block, fold_level > prev_fold_level) TextBlockHelper.set_fold_lvl(current_block, fold_level) # user pressed enter at the beginning of a fold trigger line # the previous blank or comment line will keep the trigger state # and the new line (which actually contains the trigger) must use # the prev state (and prev state must then be reset). prev = current_block.previous() # real prev block (may be blank) if (prev and prev.isValid() and (prev.text().strip() == '' or self.editor.is_comment(prev)) and TextBlockHelper.is_fold_trigger(prev)): # prev line has the correct trigger fold state TextBlockHelper.set_collapsed( current_block, TextBlockHelper.is_collapsed( prev)) # make empty or comment line not a trigger TextBlockHelper.set_fold_trigger(prev, False) TextBlockHelper.set_collapsed(prev, False)
def trigger_level(self): """ Returns the fold level of the block trigger. :return: """ return TextBlockHelper.get_fold_lvl(self._trigger)
def collapsed(self): """Returns True if the block is collasped, False if it is expanded.""" return TextBlockHelper.is_collapsed(self._trigger)
def process_block(self, current_block, previous_block, text): """ Processes a block and setup its folding info. This method call ``detect_fold_level`` and handles most of the tricky corner cases so that all you have to do is focus on getting the proper fold level foreach meaningful block, skipping the blank ones. :param current_block: current block to process :param previous_block: previous block :param text: current block text """ prev_fold_level = TextBlockHelper.get_fold_lvl(previous_block) if text.strip() == '' or self.editor.is_comment(current_block): # blank or comment line always have the same level # as the previous line fold_level = prev_fold_level else: fold_level = self.detect_fold_level(previous_block, current_block) if fold_level > self.limit: fold_level = self.limit prev_fold_level = TextBlockHelper.get_fold_lvl(previous_block) if fold_level > prev_fold_level: # apply on previous blank or comment lines block = current_block.previous() while block.isValid() and (block.text().strip() == '' or self.editor.is_comment(block)): TextBlockHelper.set_fold_lvl(block, fold_level) block = block.previous() TextBlockHelper.set_fold_trigger(block, True) # update block fold level if text.strip() and not self.editor.is_comment(previous_block): TextBlockHelper.set_fold_trigger(previous_block, fold_level > prev_fold_level) TextBlockHelper.set_fold_lvl(current_block, fold_level) # user pressed enter at the beginning of a fold trigger line # the previous blank or comment line will keep the trigger state # and the new line (which actually contains the trigger) must use # the prev state (and prev state must then be reset). prev = current_block.previous() # real prev block (may be blank) if (prev and prev.isValid() and (prev.text().strip() == '' or self.editor.is_comment(prev)) and TextBlockHelper.is_fold_trigger(prev)): # prev line has the correct trigger fold state TextBlockHelper.set_collapsed(current_block, TextBlockHelper.is_collapsed(prev)) # make empty or comment line not a trigger TextBlockHelper.set_fold_trigger(prev, False) TextBlockHelper.set_collapsed(prev, False)