File: C:/Users/fred/anaconda3/Lib/site-packages/spyder/plugins/outlineexplorer/api.py
# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)
"""Outline explorer API.
You need to declare a OutlineExplorerProxy, and a function for handle the
edit_goto Signal.
class OutlineExplorerProxyCustom(OutlineExplorerProxy):
...
def handle_go_to(name, line, text):
...
outlineexplorer = OutlineExplorerWidget(None)
oe_proxy = OutlineExplorerProxyCustom(name)
outlineexplorer.set_current_editor(oe_proxy, update=True, clear=False)
outlineexplorer.edit_goto.connect(handle_go_to)
"""
import re
from qtpy.QtCore import Signal, QObject
from qtpy.QtGui import QTextBlock
from spyder.config.base import _
from spyder.config.base import running_under_pytest
def document_cells(block, forward=True, cell_list=None):
"""
Get cells oedata before or after block in the document.
Parameters
----------
forward : bool, optional
Whether to iterate forward or backward from the current block.
cell_list: list of tuple containing (block_number, oedata)
This is the list of all cells in a file to avoid having to parse
the file every time.
"""
if not block.isValid():
# Not a valid block
return
if forward:
block = block.next()
else:
block = block.previous()
if not block.isValid():
return
if cell_list is not None:
cell_list = sorted(cell_list)
block_line = block.blockNumber()
if forward:
for cell_line, oedata in cell_list:
if cell_line >= block_line:
yield oedata
else:
for cell_line, oedata in cell_list[::-1]:
if cell_line <= block_line:
yield oedata
return
# If the cell_list was not provided, search the cells
while block.isValid():
data = block.userData()
if (data
and data.oedata
and data.oedata.def_type == OutlineExplorerData.CELL):
yield data.oedata
if forward:
block = block.next()
else:
block = block.previous()
def is_cell_header(block):
"""Check if the given block is a cell header."""
if not block.isValid():
return False
data = block.userData()
return (data
and data.oedata
and data.oedata.def_type == OutlineExplorerData.CELL)
def cell_index(block):
"""Get the cell index of the given block."""
index = len(list(document_cells(block, forward=False)))
if is_cell_header(block):
return index + 1
return index
def cell_name(block):
"""
Get the cell name the block is in.
If the cell is unnamed, return the cell index instead.
"""
if is_cell_header(block):
header = block.userData().oedata
else:
try:
header = next(document_cells(block, forward=False))
except StopIteration:
# This cell has no header, so it is the first cell.
return 0
if header.has_name():
return header.def_name
else:
# No name, return the index
return cell_index(block)
class OutlineExplorerProxy(QObject):
"""
Proxy class between editors and OutlineExplorerWidget.
"""
sig_cursor_position_changed = Signal(int, int)
sig_outline_explorer_data_changed = Signal(list)
sig_start_outline_spinner = Signal()
def __init__(self):
super(OutlineExplorerProxy, self).__init__()
self.fname = None
def is_python(self):
"""Return whether the editor is a python file or not."""
raise NotImplementedError
def get_id(self):
"""Return an unique id, used for identify objects in a dict"""
raise NotImplementedError
def give_focus(self):
"""Give focus to the editor, called when toogling visibility of
OutlineExplorerWidget."""
raise NotImplementedError
def get_line_count(self):
"""Return the number of lines of the editor (int)."""
raise NotImplementedError
def parent(self):
"""This is used for diferenciate editors in multi-window mode."""
return None
def get_cursor_line_number(self):
"""Return the cursor line number."""
raise NotImplementedError
def outlineexplorer_data_list(self):
"""Returns a list of outline explorer data."""
raise NotImplementedError
def request_symbols(self):
"""Request current editor symbols."""
raise NotImplementedError
@property
def is_cloned(self):
"""Check if the associated editor is cloned."""
return False
class OutlineExplorerData(QObject):
CLASS, FUNCTION, STATEMENT, COMMENT, CELL = list(range(5))
FUNCTION_TOKEN = 'def'
CLASS_TOKEN = 'class'
# Emitted if the OutlineExplorerData was changed
sig_update = Signal()
def __init__(self, block, text=None, fold_level=None, def_type=None,
def_name=None, color=None):
"""
Args:
text (str)
fold_level (int)
def_type (int): [CLASS, FUNCTION, STATEMENT, COMMENT, CELL]
def_name (str)
color (PyQt.QtGui.QTextCharFormat)
"""
super(OutlineExplorerData, self).__init__()
self.text = text
self.fold_level = fold_level
self.def_type = def_type
self.def_name = def_name
self.color = color
if running_under_pytest():
# block might be a dummy
self.block = block
else:
# Copy the text block to make sure it is not deleted
self.block = QTextBlock(block)
def is_not_class_nor_function(self):
return self.def_type not in (self.CLASS, self.FUNCTION)
def is_class_or_function(self):
return self.def_type in (self.CLASS, self.FUNCTION)
def is_comment(self):
return self.def_type in (self.COMMENT, self.CELL)
def get_class_name(self):
if self.def_type == self.CLASS:
return self.def_name
def get_function_name(self):
if self.def_type == self.FUNCTION:
return self.def_name
def get_token(self):
if self.def_type == self.FUNCTION:
token = self.FUNCTION_TOKEN
elif self.def_type == self.CLASS:
token = self.CLASS_TOKEN
return token
@property
def def_name(self):
"""Get the cell name."""
# Non cell don't need unique names.
if self.def_type != self.CELL:
return self._def_name
def get_name(oedata):
name = oedata._def_name
if not name:
name = _('Unnamed Cell')
return name
self_name = get_name(self)
existing_numbers = []
def check_match(oedata):
# Look for "string"
other_name = get_name(oedata)
pattern = '^' + re.escape(self_name) + r'(?:, #(\d+))?$'
match = re.match(pattern, other_name)
if match:
# Check if already has a number
number = match.groups()[0]
if number:
existing_numbers.append(int(number))
return True
return False
# Count cells
N_prev = 0
for oedata in document_cells(self.block, forward=False):
if check_match(oedata):
N_prev += 1
N_fix_previous = len(existing_numbers)
N_next = 0
for oedata in document_cells(self.block, forward=True):
if check_match(oedata):
N_next += 1
# Get the remaining indexeswe can use
free_indexes = [idx for idx in range(N_prev + N_next + 1)
if idx + 1 not in existing_numbers]
idx = free_indexes[N_prev - N_fix_previous]
if N_prev + N_next > 0:
return self_name + ', #{}'.format(idx + 1)
return self_name
@def_name.setter
def def_name(self, value):
"""Set name."""
self._def_name = value
def update(self, other):
"""Try to update to avoid reloading everything."""
if (self.def_type == other.def_type and
self.fold_level == other.fold_level):
self.text = other.text
old_def_name = self._def_name
self._def_name = other._def_name
self.color = other.color
self.sig_update.emit()
if self.def_type == self.CELL:
if self.cell_level != other.cell_level:
return False
# Must update all other cells whose name has changed.
for oedata in document_cells(self.block, forward=True):
if oedata._def_name in [self._def_name, old_def_name]:
oedata.sig_update.emit()
return True
return False
def is_valid(self):
"""Check if the oedata has a valid block attached."""
block = self.block
if not block or not block.isValid():
return False
user_data = block.userData()
if not user_data or not hasattr(user_data, 'oedata'):
return False
return user_data.oedata == self
def has_name(self):
"""Check if cell has a name."""
if self._def_name:
return True
else:
return False
def get_block_number(self):
"""Get the block number."""
if not self.is_valid():
# Avoid calling blockNumber if not a valid block
return None
return self.block.blockNumber()