# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)
"""Editor Widget"""
# pylint: disable=C0103
# pylint: disable=R0903
# pylint: disable=R0911
# pylint: disable=R0201
# Standard library imports
import logging
import os
import os.path as osp
import sys
import functools
import unicodedata
# Third party imports
import qstylizer.style
from qtpy.compat import getsavefilename
from qtpy.QtCore import (QByteArray, QFileInfo, QPoint, QSize, Qt, QTimer,
Signal, Slot)
from qtpy.QtGui import QFont
from qtpy.QtWidgets import (QAction, QApplication, QFileDialog, QHBoxLayout,
QLabel, QMainWindow, QMessageBox, QMenu,
QSplitter, QVBoxLayout, QWidget, QListWidget,
QListWidgetItem, QSizePolicy, QToolBar)
# Local imports
from spyder.api.panel import Panel
from spyder.config.base import _, running_under_pytest
from spyder.config.manager import CONF
from spyder.config.utils import (get_edit_filetypes, get_edit_filters,
get_filter, is_kde_desktop, is_anaconda)
from spyder.plugins.editor.utils.autosave import AutosaveForStack
from spyder.plugins.editor.utils.editor import get_file_language
from spyder.plugins.editor.utils.switcher import EditorSwitcherManager
from spyder.plugins.editor.widgets import codeeditor
from spyder.plugins.editor.widgets.editorstack_helpers import (
ThreadManager, FileInfo, StackHistory)
from spyder.plugins.editor.widgets.status import (CursorPositionStatus,
EncodingStatus, EOLStatus,
ReadWriteStatus, VCSStatus)
from spyder.plugins.explorer.widgets.explorer import (
show_in_external_file_explorer)
from spyder.plugins.outlineexplorer.main_widget import OutlineExplorerWidget
from spyder.plugins.outlineexplorer.editor import OutlineExplorerProxyEditor
from spyder.plugins.outlineexplorer.api import cell_name
from spyder.py3compat import qbytearray_to_str, to_text_string
from spyder.utils import encoding, sourcecode, syntaxhighlighters
from spyder.utils.icon_manager import ima
from spyder.utils.palette import QStylePalette
from spyder.utils.qthelpers import (add_actions, create_action,
create_toolbutton, MENU_SEPARATOR,
mimedata2url, set_menu_icons,
create_waitspinner)
from spyder.utils.stylesheet import (
APP_STYLESHEET, APP_TOOLBAR_STYLESHEET, PANES_TABBAR_STYLESHEET)
from spyder.widgets.findreplace import FindReplace
from spyder.widgets.tabs import BaseTabs
logger = logging.getLogger(__name__)
class TabSwitcherWidget(QListWidget):
"""Show tabs in mru order and change between them."""
def __init__(self, parent, stack_history, tabs):
QListWidget.__init__(self, parent)
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog)
self.editor = parent
self.stack_history = stack_history
self.tabs = tabs
self.setSelectionMode(QListWidget.SingleSelection)
self.itemActivated.connect(self.item_selected)
self.id_list = []
self.load_data()
size = CONF.get('main', 'completion/size')
self.resize(*size)
self.set_dialog_position()
self.setCurrentRow(0)
CONF.config_shortcut(lambda: self.select_row(-1), context='Editor',
name='Go to previous file', parent=self)
CONF.config_shortcut(lambda: self.select_row(1), context='Editor',
name='Go to next file', parent=self)
def load_data(self):
"""Fill ListWidget with the tabs texts.
Add elements in inverse order of stack_history.
"""
for index in reversed(self.stack_history):
text = self.tabs.tabText(index)
text = text.replace('&', '')
item = QListWidgetItem(ima.icon('TextFileIcon'), text)
self.addItem(item)
def item_selected(self, item=None):
"""Change to the selected document and hide this widget."""
if item is None:
item = self.currentItem()
# stack history is in inverse order
try:
index = self.stack_history[-(self.currentRow()+1)]
except IndexError:
pass
else:
self.editor.set_stack_index(index)
self.editor.current_changed(index)
self.hide()
def select_row(self, steps):
"""Move selected row a number of steps.
Iterates in a cyclic behaviour.
"""
row = (self.currentRow() + steps) % self.count()
self.setCurrentRow(row)
def set_dialog_position(self):
"""Positions the tab switcher in the top-center of the editor."""
left = self.editor.geometry().width()/2 - self.width()/2
top = (self.editor.tabs.tabBar().geometry().height() +
self.editor.fname_label.geometry().height())
self.move(self.editor.mapToGlobal(QPoint(left, top)))
def keyReleaseEvent(self, event):
"""Reimplement Qt method.
Handle "most recent used" tab behavior,
When ctrl is released and tab_switcher is visible, tab will be changed.
"""
if self.isVisible():
qsc = CONF.get_shortcut(context='Editor', name='Go to next file')
for key in qsc.split('+'):
key = key.lower()
if ((key == 'ctrl' and event.key() == Qt.Key_Control) or
(key == 'alt' and event.key() == Qt.Key_Alt)):
self.item_selected()
event.accept()
def keyPressEvent(self, event):
"""Reimplement Qt method to allow cyclic behavior."""
if event.key() == Qt.Key_Down:
self.select_row(1)
elif event.key() == Qt.Key_Up:
self.select_row(-1)
def focusOutEvent(self, event):
"""Reimplement Qt method to close the widget when loosing focus."""
event.ignore()
if sys.platform == "darwin":
if event.reason() != Qt.ActiveWindowFocusReason:
self.close()
else:
self.close()
class EditorStack(QWidget):
reset_statusbar = Signal()
readonly_changed = Signal(bool)
encoding_changed = Signal(str)
sig_editor_cursor_position_changed = Signal(int, int)
sig_refresh_eol_chars = Signal(str)
sig_refresh_formatting = Signal(bool)
starting_long_process = Signal(str)
ending_long_process = Signal(str)
redirect_stdio = Signal(bool)
exec_in_extconsole = Signal(str, bool)
run_cell_in_ipyclient = Signal(str, object, str, bool)
debug_cell_in_ipyclient = Signal(str, object, str, bool)
update_plugin_title = Signal()
editor_focus_changed = Signal()
zoom_in = Signal()
zoom_out = Signal()
zoom_reset = Signal()
sig_open_file = Signal(dict)
sig_close_file = Signal(str, str)
file_saved = Signal(str, str, str)
file_renamed_in_data = Signal(str, str, str)
opened_files_list_changed = Signal()
active_languages_stats = Signal(set)
todo_results_changed = Signal()
update_code_analysis_actions = Signal()
refresh_file_dependent_actions = Signal()
refresh_save_all_action = Signal()
sig_breakpoints_saved = Signal()
text_changed_at = Signal(str, int)
current_file_changed = Signal(str, int, int, int)
plugin_load = Signal((str,), ())
edit_goto = Signal(str, int, str)
sig_split_vertically = Signal()
sig_split_horizontally = Signal()
sig_new_file = Signal((str,), ())
sig_save_as = Signal()
sig_prev_edit_pos = Signal()
sig_prev_cursor = Signal()
sig_next_cursor = Signal()
sig_prev_warning = Signal()
sig_next_warning = Signal()
sig_go_to_definition = Signal(str, int, int)
sig_perform_completion_request = Signal(str, str, dict)
sig_option_changed = Signal(str, object) # config option needs changing
sig_save_bookmark = Signal(int)
sig_load_bookmark = Signal(int)
sig_save_bookmarks = Signal(str, str)
sig_help_requested = Signal(dict)
"""
This signal is emitted to request help on a given object `name`.
Parameters
----------
help_data: dict
Dictionary required by the Help pane to render a docstring.
Examples
--------
>>> help_data = {
'obj_text': str,
'name': str,
'argspec': str,
'note': str,
'docstring': str,
'force_refresh': bool,
'path': str,
}
See Also
--------
:py:meth:spyder.plugins.editor.widgets.editor.EditorStack.send_to_help
"""
def __init__(self, parent, actions):
QWidget.__init__(self, parent)
self.setAttribute(Qt.WA_DeleteOnClose)
self.threadmanager = ThreadManager(self)
self.new_window = False
self.horsplit_action = None
self.versplit_action = None
self.close_action = None
self.__get_split_actions()
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
self.menu = None
self.switcher_dlg = None
self.switcher_manager = None
self.tabs = None
self.tabs_switcher = None
self.stack_history = StackHistory(self)
# External panels
self.external_panels = []
self.setup_editorstack(parent, layout)
self.find_widget = None
self.data = []
switcher_action = create_action(
self,
_("File switcher..."),
icon=ima.icon('filelist'),
triggered=self.open_switcher_dlg)
symbolfinder_action = create_action(
self,
_("Find symbols in file..."),
icon=ima.icon('symbol_find'),
triggered=self.open_symbolfinder_dlg)
copy_to_cb_action = create_action(self, _("Copy path to clipboard"),
icon=ima.icon('editcopy'),
triggered=lambda:
QApplication.clipboard().setText(self.get_current_filename()))
close_right = create_action(self, _("Close all to the right"),
triggered=self.close_all_right)
close_all_but_this = create_action(self, _("Close all but this"),
triggered=self.close_all_but_this)
sort_tabs = create_action(self, _("Sort tabs alphabetically"),
triggered=self.sort_file_tabs_alphabetically)
if sys.platform == 'darwin':
text = _("Show in Finder")
else:
text = _("Show in external file explorer")
external_fileexp_action = create_action(
self, text,
triggered=self.show_in_external_file_explorer,
shortcut=CONF.get_shortcut(context="Editor",
name="show in external file explorer"),
context=Qt.WidgetShortcut)
self.menu_actions = actions + [external_fileexp_action,
None, switcher_action,
symbolfinder_action,
copy_to_cb_action, None, close_right,
close_all_but_this, sort_tabs]
self.outlineexplorer = None
self.is_closable = False
self.new_action = None
self.open_action = None
self.save_action = None
self.revert_action = None
self.tempfile_path = None
self.title = _("Editor")
self.todolist_enabled = True
self.is_analysis_done = False
self.linenumbers_enabled = True
self.blanks_enabled = False
self.scrollpastend_enabled = False
self.edgeline_enabled = True
self.edgeline_columns = (79,)
self.close_parentheses_enabled = True
self.close_quotes_enabled = True
self.add_colons_enabled = True
self.auto_unindent_enabled = True
self.indent_chars = " "*4
self.tab_stop_width_spaces = 4
self.show_class_func_dropdown = False
self.help_enabled = False
self.default_font = None
self.wrap_enabled = False
self.tabmode_enabled = False
self.stripmode_enabled = False
self.intelligent_backspace_enabled = True
self.automatic_completions_enabled = True
self.automatic_completion_chars = 3
self.automatic_completion_ms = 300
self.completions_hint_enabled = True
self.completions_hint_after_ms = 500
self.hover_hints_enabled = True
self.format_on_save = False
self.code_snippets_enabled = True
self.code_folding_enabled = True
self.underline_errors_enabled = False
self.highlight_current_line_enabled = False
self.highlight_current_cell_enabled = False
self.occurrence_highlighting_enabled = True
self.occurrence_highlighting_timeout = 1500
self.checkeolchars_enabled = True
self.always_remove_trailing_spaces = False
self.add_newline = False
self.remove_trailing_newlines = False
self.convert_eol_on_save = False
self.convert_eol_on_save_to = 'LF'
self.focus_to_editor = True
self.run_cell_copy = False
self.create_new_file_if_empty = True
self.indent_guides = False
ccs = 'spyder/dark'
if ccs not in syntaxhighlighters.COLOR_SCHEME_NAMES:
ccs = syntaxhighlighters.COLOR_SCHEME_NAMES[0]
self.color_scheme = ccs
self.__file_status_flag = False
# Real-time code analysis
self.analysis_timer = QTimer(self)
self.analysis_timer.setSingleShot(True)
self.analysis_timer.setInterval(1000)
self.analysis_timer.timeout.connect(self.analyze_script)
# Update filename label
self.editor_focus_changed.connect(self.update_fname_label)
# Accepting drops
self.setAcceptDrops(True)
# Local shortcuts
self.shortcuts = self.create_shortcuts()
# For opening last closed tabs
self.last_closed_files = []
# Reference to save msgbox and avoid memory to be freed.
self.msgbox = None
# File types and filters used by the Save As dialog
self.edit_filetypes = None
self.edit_filters = None
# For testing
self.save_dialog_on_tests = not running_under_pytest()
# Autusave component
self.autosave = AutosaveForStack(self)
self.last_cell_call = None
@Slot()
def show_in_external_file_explorer(self, fnames=None):
"""Show file in external file explorer"""
if fnames is None:
fnames = self.get_current_filename()
try:
show_in_external_file_explorer(fnames)
except FileNotFoundError as error:
file = str(error).split("'")[1]
if "xdg-open" in file:
msg_title = _("Warning")
msg = _("Spyder can't show this file in the external file "
"explorer because the xdg-utils package is "
"not available on your system.")
QMessageBox.information(self, msg_title, msg,
QMessageBox.Ok)
def create_shortcuts(self):
"""Create local shortcuts"""
# --- Configurable shortcuts
inspect = CONF.config_shortcut(
self.inspect_current_object,
context='Editor',
name='Inspect current object',
parent=self)
set_breakpoint = CONF.config_shortcut(
self.set_or_clear_breakpoint,
context='Editor',
name='Breakpoint',
parent=self)
set_cond_breakpoint = CONF.config_shortcut(
self.set_or_edit_conditional_breakpoint,
context='Editor',
name='Conditional breakpoint',
parent=self)
gotoline = CONF.config_shortcut(
self.go_to_line,
context='Editor',
name='Go to line',
parent=self)
tab = CONF.config_shortcut(
lambda: self.tab_navigation_mru(forward=False),
context='Editor',
name='Go to previous file',
parent=self)
tabshift = CONF.config_shortcut(
self.tab_navigation_mru,
context='Editor',
name='Go to next file',
parent=self)
prevtab = CONF.config_shortcut(
lambda: self.tabs.tab_navigate(-1),
context='Editor',
name='Cycle to previous file',
parent=self)
nexttab = CONF.config_shortcut(
lambda: self.tabs.tab_navigate(1),
context='Editor',
name='Cycle to next file',
parent=self)
run_selection = CONF.config_shortcut(
self.run_selection,
context='Editor',
name='Run selection',
parent=self)
new_file = CONF.config_shortcut(
lambda: self.sig_new_file[()].emit(),
context='Editor',
name='New file',
parent=self)
open_file = CONF.config_shortcut(
lambda: self.plugin_load[()].emit(),
context='Editor',
name='Open file',
parent=self)
save_file = CONF.config_shortcut(
self.save,
context='Editor',
name='Save file',
parent=self)
save_all = CONF.config_shortcut(
self.save_all,
context='Editor',
name='Save all',
parent=self)
save_as = CONF.config_shortcut(
lambda: self.sig_save_as.emit(),
context='Editor',
name='Save As',
parent=self)
close_all = CONF.config_shortcut(
self.close_all_files,
context='Editor',
name='Close all',
parent=self)
prev_edit_pos = CONF.config_shortcut(
lambda: self.sig_prev_edit_pos.emit(),
context="Editor",
name="Last edit location",
parent=self)
prev_cursor = CONF.config_shortcut(
lambda: self.sig_prev_cursor.emit(),
context="Editor",
name="Previous cursor position",
parent=self)
next_cursor = CONF.config_shortcut(
lambda: self.sig_next_cursor.emit(),
context="Editor",
name="Next cursor position",
parent=self)
zoom_in_1 = CONF.config_shortcut(
lambda: self.zoom_in.emit(),
context="Editor",
name="zoom in 1",
parent=self)
zoom_in_2 = CONF.config_shortcut(
lambda: self.zoom_in.emit(),
context="Editor",
name="zoom in 2",
parent=self)
zoom_out = CONF.config_shortcut(
lambda: self.zoom_out.emit(),
context="Editor",
name="zoom out",
parent=self)
zoom_reset = CONF.config_shortcut(
lambda: self.zoom_reset.emit(),
context="Editor",
name="zoom reset",
parent=self)
close_file_1 = CONF.config_shortcut(
self.close_file,
context="Editor",
name="close file 1",
parent=self)
close_file_2 = CONF.config_shortcut(
self.close_file,
context="Editor",
name="close file 2",
parent=self)
run_cell = CONF.config_shortcut(
self.run_cell,
context="Editor",
name="run cell",
parent=self)
debug_cell = CONF.config_shortcut(
self.debug_cell,
context="Editor",
name="debug cell",
parent=self)
run_cell_and_advance = CONF.config_shortcut(
self.run_cell_and_advance,
context="Editor",
name="run cell and advance",
parent=self)
go_to_next_cell = CONF.config_shortcut(
self.advance_cell,
context="Editor",
name="go to next cell",
parent=self)
go_to_previous_cell = CONF.config_shortcut(
lambda: self.advance_cell(reverse=True),
context="Editor",
name="go to previous cell",
parent=self)
re_run_last_cell = CONF.config_shortcut(
self.re_run_last_cell,
context="Editor",
name="re-run last cell",
parent=self)
prev_warning = CONF.config_shortcut(
lambda: self.sig_prev_warning.emit(),
context="Editor",
name="Previous warning",
parent=self)
next_warning = CONF.config_shortcut(
lambda: self.sig_next_warning.emit(),
context="Editor",
name="Next warning",
parent=self)
split_vertically = CONF.config_shortcut(
lambda: self.sig_split_vertically.emit(),
context="Editor",
name="split vertically",
parent=self)
split_horizontally = CONF.config_shortcut(
lambda: self.sig_split_horizontally.emit(),
context="Editor",
name="split horizontally",
parent=self)
close_split = CONF.config_shortcut(
self.close_split,
context="Editor",
name="close split panel",
parent=self)
external_fileexp = CONF.config_shortcut(
self.show_in_external_file_explorer,
context="Editor",
name="show in external file explorer",
parent=self)
# Return configurable ones
return [inspect, set_breakpoint, set_cond_breakpoint, gotoline, tab,
tabshift, run_selection, new_file, open_file, save_file,
save_all, save_as, close_all, prev_edit_pos, prev_cursor,
next_cursor, zoom_in_1, zoom_in_2, zoom_out, zoom_reset,
close_file_1, close_file_2, run_cell, debug_cell,
run_cell_and_advance,
go_to_next_cell, go_to_previous_cell, re_run_last_cell,
prev_warning, next_warning, split_vertically,
split_horizontally, close_split,
prevtab, nexttab, external_fileexp]
def get_shortcut_data(self):
"""
Returns shortcut data, a list of tuples (shortcut, text, default)
shortcut (QShortcut or QAction instance)
text (string): action/shortcut description
default (string): default key sequence
"""
return [sc.data for sc in self.shortcuts]
def setup_editorstack(self, parent, layout):
"""Setup editorstack's layout"""
layout.setSpacing(0)
# Create filename label, spinner and the toolbar that contains them
self.create_top_widgets()
# Add top toolbar
layout.addWidget(self.top_toolbar)
# Tabbar
menu_btn = create_toolbutton(self, icon=ima.icon('tooloptions'),
tip=_('Options'))
menu_btn.setStyleSheet(str(PANES_TABBAR_STYLESHEET))
self.menu = QMenu(self)
menu_btn.setMenu(self.menu)
menu_btn.setPopupMode(menu_btn.InstantPopup)
self.menu.aboutToShow.connect(self.__setup_menu)
corner_widgets = {Qt.TopRightCorner: [menu_btn]}
self.tabs = BaseTabs(self, menu=self.menu, menu_use_tooltips=True,
corner_widgets=corner_widgets)
self.tabs.set_close_function(self.close_file)
self.tabs.tabBar().tabMoved.connect(self.move_editorstack_data)
self.tabs.setMovable(True)
self.stack_history.refresh()
if hasattr(self.tabs, 'setDocumentMode') \
and not sys.platform == 'darwin':
# Don't set document mode to true on OSX because it generates
# a crash when the editor is detached from the main window
# Fixes spyder-ide/spyder#561.
self.tabs.setDocumentMode(True)
self.tabs.currentChanged.connect(self.current_changed)
tab_container = QWidget()
tab_container.setObjectName('tab-container')
tab_layout = QHBoxLayout(tab_container)
tab_layout.setContentsMargins(0, 0, 0, 0)
tab_layout.addWidget(self.tabs)
layout.addWidget(tab_container)
# Show/hide icons in plugin menus for Mac
if sys.platform == 'darwin':
self.menu.aboutToHide.connect(
lambda menu=self.menu:
set_menu_icons(menu, False))
def create_top_widgets(self):
# Filename label
self.fname_label = QLabel()
# Spacer
spacer = QWidget()
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
# Spinner
self.spinner = create_waitspinner(size=16, parent=self.fname_label)
# Add widgets to toolbar
self.top_toolbar = QToolBar(self)
self.top_toolbar.addWidget(self.fname_label)
self.top_toolbar.addWidget(spacer)
self.top_toolbar.addWidget(self.spinner)
# Set toolbar style
css = qstylizer.style.StyleSheet()
css.QToolBar.setValues(
margin='0px',
padding='4px',
borderBottom=f'1px solid {QStylePalette.COLOR_BACKGROUND_4}'
)
self.top_toolbar.setStyleSheet(css.toString())
def hide_tooltip(self):
"""Hide any open tooltips."""
for finfo in self.data:
finfo.editor.hide_tooltip()
@Slot()
def update_fname_label(self):
"""Update file name label."""
filename = to_text_string(self.get_current_filename())
if len(filename) > 100:
shorten_filename = u'...' + filename[-100:]
else:
shorten_filename = filename
self.fname_label.setText(shorten_filename)
def add_corner_widgets_to_tabbar(self, widgets):
self.tabs.add_corner_widgets(widgets)
@Slot()
def close_split(self):
"""Closes the editorstack if it is not the last one opened."""
if self.is_closable:
self.close()
def closeEvent(self, event):
"""Overrides QWidget closeEvent()."""
self.threadmanager.close_all_threads()
self.analysis_timer.timeout.disconnect(self.analyze_script)
# Remove editor references from the outline explorer settings
if self.outlineexplorer is not None:
for finfo in self.data:
self.outlineexplorer.remove_editor(finfo.editor.oe_proxy)
for finfo in self.data:
if not finfo.editor.is_cloned:
finfo.editor.notify_close()
QWidget.closeEvent(self, event)
def clone_editor_from(self, other_finfo, set_current):
fname = other_finfo.filename
enc = other_finfo.encoding
new = other_finfo.newly_created
finfo = self.create_new_editor(fname, enc, "",
set_current=set_current, new=new,
cloned_from=other_finfo.editor)
finfo.set_todo_results(other_finfo.todo_results)
return finfo.editor
def clone_from(self, other):
"""Clone EditorStack from other instance"""
for other_finfo in other.data:
self.clone_editor_from(other_finfo, set_current=True)
self.set_stack_index(other.get_stack_index())
@Slot()
@Slot(str)
def open_switcher_dlg(self, initial_text=''):
"""Open file list management dialog box"""
if not self.tabs.count():
return
if self.switcher_dlg is not None and self.switcher_dlg.isVisible():
self.switcher_dlg.hide()
self.switcher_dlg.clear()
return
if self.switcher_dlg is None:
from spyder.widgets.switcher import Switcher
self.switcher_dlg = Switcher(self)
self.switcher_manager = EditorSwitcherManager(
self.get_plugin(),
self.switcher_dlg,
lambda: self.get_current_editor(),
lambda: self,
section=self.get_plugin_title())
self.switcher_dlg.set_search_text(initial_text)
self.switcher_dlg.setup()
self.switcher_dlg.show()
# Note: the +1 pixel on the top makes it look better
delta_top = (self.tabs.tabBar().geometry().height() +
self.fname_label.geometry().height() + 1)
self.switcher_dlg.set_position(delta_top)
@Slot()
def open_symbolfinder_dlg(self):
self.open_switcher_dlg(initial_text='@')
def get_plugin(self):
"""Get the plugin of the parent widget."""
# Needed for the editor stack to use its own switcher instance.
# See spyder-ide/spyder#10684.
return self.parent().plugin
def get_plugin_title(self):
"""Get the plugin title of the parent widget."""
# Needed for the editor stack to use its own switcher instance.
# See spyder-ide/spyder#9469.
return self.get_plugin().get_plugin_title()
def go_to_line(self, line=None):
"""Go to line dialog"""
if line is not None:
# When this method is called from the flileswitcher, a line
# number is specified, so there is no need for the dialog.
self.get_current_editor().go_to_line(line)
else:
if self.data:
self.get_current_editor().exec_gotolinedialog()
def set_or_clear_breakpoint(self):
"""Set/clear breakpoint"""
if self.data:
editor = self.get_current_editor()
editor.debugger.toogle_breakpoint()
def set_or_edit_conditional_breakpoint(self):
"""Set conditional breakpoint"""
if self.data:
editor = self.get_current_editor()
editor.debugger.toogle_breakpoint(edit_condition=True)
def set_bookmark(self, slot_num):
"""Bookmark current position to given slot."""
if self.data:
editor = self.get_current_editor()
editor.add_bookmark(slot_num)
def inspect_current_object(self, pos=None):
"""Inspect current object in the Help plugin"""
editor = self.get_current_editor()
editor.sig_display_object_info.connect(self.display_help)
cursor = None
offset = editor.get_position('cursor')
if pos:
cursor = editor.get_last_hover_cursor()
if cursor:
offset = cursor.position()
else:
return
line, col = editor.get_cursor_line_column(cursor)
editor.request_hover(line, col, offset,
show_hint=False, clicked=bool(pos))
@Slot(str, bool)
def display_help(self, help_text, clicked):
editor = self.get_current_editor()
if clicked:
name = editor.get_last_hover_word()
else:
name = editor.get_current_word(help_req=True)
try:
editor.sig_display_object_info.disconnect(self.display_help)
except TypeError:
# Needed to prevent an error after some time in idle.
# See spyder-ide/spyder#11228
pass
self.send_to_help(name, help_text, force=True)
# ------ Editor Widget Settings
def set_closable(self, state):
"""Parent widget must handle the closable state"""
self.is_closable = state
def set_io_actions(self, new_action, open_action,
save_action, revert_action):
self.new_action = new_action
self.open_action = open_action
self.save_action = save_action
self.revert_action = revert_action
def set_find_widget(self, find_widget):
self.find_widget = find_widget
def set_outlineexplorer(self, outlineexplorer):
self.outlineexplorer = outlineexplorer
def add_outlineexplorer_button(self, editor_plugin):
oe_btn = create_toolbutton(editor_plugin)
oe_btn.setDefaultAction(self.outlineexplorer.visibility_action)
self.add_corner_widgets_to_tabbar([5, oe_btn])
def set_tempfile_path(self, path):
self.tempfile_path = path
def set_title(self, text):
self.title = text
def set_classfunc_dropdown_visible(self, state):
self.show_class_func_dropdown = state
if self.data:
for finfo in self.data:
if finfo.editor.is_python_like():
finfo.editor.classfuncdropdown.setVisible(state)
def __update_editor_margins(self, editor):
editor.linenumberarea.setup_margins(
linenumbers=self.linenumbers_enabled, markers=self.has_markers())
def has_markers(self):
"""Return True if this editorstack has a marker margin for TODOs or
code analysis"""
return self.todolist_enabled
def set_todolist_enabled(self, state, current_finfo=None):
# CONF.get(self.CONF_SECTION, 'todo_list')
self.todolist_enabled = state
if self.data:
for finfo in self.data:
self.__update_editor_margins(finfo.editor)
finfo.cleanup_todo_results()
if state and current_finfo is not None:
if current_finfo is not finfo:
finfo.run_todo_finder()
def set_linenumbers_enabled(self, state, current_finfo=None):
# CONF.get(self.CONF_SECTION, 'line_numbers')
self.linenumbers_enabled = state
if self.data:
for finfo in self.data:
self.__update_editor_margins(finfo.editor)
def set_blanks_enabled(self, state):
self.blanks_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_blanks_enabled(state)
def set_scrollpastend_enabled(self, state):
self.scrollpastend_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_scrollpastend_enabled(state)
def set_edgeline_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'edge_line')
self.edgeline_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.edge_line.set_enabled(state)
def set_edgeline_columns(self, columns):
# CONF.get(self.CONF_SECTION, 'edge_line_column')
self.edgeline_columns = columns
if self.data:
for finfo in self.data:
finfo.editor.edge_line.set_columns(columns)
def set_indent_guides(self, state):
self.indent_guides = state
if self.data:
for finfo in self.data:
finfo.editor.toggle_identation_guides(state)
def set_close_parentheses_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'close_parentheses')
self.close_parentheses_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_close_parentheses_enabled(state)
def set_close_quotes_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'close_quotes')
self.close_quotes_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_close_quotes_enabled(state)
def set_add_colons_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'add_colons')
self.add_colons_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_add_colons_enabled(state)
def set_auto_unindent_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'auto_unindent')
self.auto_unindent_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_auto_unindent_enabled(state)
def set_indent_chars(self, indent_chars):
# CONF.get(self.CONF_SECTION, 'indent_chars')
indent_chars = indent_chars[1:-1] # removing the leading/ending '*'
self.indent_chars = indent_chars
if self.data:
for finfo in self.data:
finfo.editor.set_indent_chars(indent_chars)
def set_tab_stop_width_spaces(self, tab_stop_width_spaces):
# CONF.get(self.CONF_SECTION, 'tab_stop_width')
self.tab_stop_width_spaces = tab_stop_width_spaces
if self.data:
for finfo in self.data:
finfo.editor.tab_stop_width_spaces = tab_stop_width_spaces
finfo.editor.update_tab_stop_width_spaces()
def set_help_enabled(self, state):
self.help_enabled = state
def set_default_font(self, font, color_scheme=None):
self.default_font = font
if color_scheme is not None:
self.color_scheme = color_scheme
if self.data:
for finfo in self.data:
finfo.editor.set_font(font, color_scheme)
def set_color_scheme(self, color_scheme):
self.color_scheme = color_scheme
if self.data:
for finfo in self.data:
finfo.editor.set_color_scheme(color_scheme)
def set_wrap_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'wrap')
self.wrap_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.toggle_wrap_mode(state)
def set_tabmode_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'tab_always_indent')
self.tabmode_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_tab_mode(state)
def set_stripmode_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'strip_trailing_spaces_on_modify')
self.stripmode_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_strip_mode(state)
def set_intelligent_backspace_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'intelligent_backspace')
self.intelligent_backspace_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.toggle_intelligent_backspace(state)
def set_code_snippets_enabled(self, state):
self.code_snippets_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.toggle_code_snippets(state)
def set_code_folding_enabled(self, state):
self.code_folding_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.toggle_code_folding(state)
def set_automatic_completions_enabled(self, state):
self.automatic_completions_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.toggle_automatic_completions(state)
def set_automatic_completions_after_chars(self, chars):
self.automatic_completion_chars = chars
if self.data:
for finfo in self.data:
finfo.editor.set_automatic_completions_after_chars(chars)
def set_automatic_completions_after_ms(self, ms):
self.automatic_completion_ms = ms
if self.data:
for finfo in self.data:
finfo.editor.set_automatic_completions_after_ms(ms)
def set_completions_hint_enabled(self, state):
self.completions_hint_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.toggle_completions_hint(state)
def set_completions_hint_after_ms(self, ms):
self.completions_hint_after_ms = ms
if self.data:
for finfo in self.data:
finfo.editor.set_completions_hint_after_ms(ms)
def set_hover_hints_enabled(self, state):
self.hover_hints_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.toggle_hover_hints(state)
def set_format_on_save(self, state):
self.format_on_save = state
if self.data:
for finfo in self.data:
finfo.editor.toggle_format_on_save(state)
def set_occurrence_highlighting_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'occurrence_highlighting')
self.occurrence_highlighting_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_occurrence_highlighting(state)
def set_occurrence_highlighting_timeout(self, timeout):
# CONF.get(self.CONF_SECTION, 'occurrence_highlighting/timeout')
self.occurrence_highlighting_timeout = timeout
if self.data:
for finfo in self.data:
finfo.editor.set_occurrence_timeout(timeout)
def set_underline_errors_enabled(self, state):
self.underline_errors_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_underline_errors_enabled(state)
def set_highlight_current_line_enabled(self, state):
self.highlight_current_line_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_highlight_current_line(state)
def set_highlight_current_cell_enabled(self, state):
self.highlight_current_cell_enabled = state
if self.data:
for finfo in self.data:
finfo.editor.set_highlight_current_cell(state)
def set_checkeolchars_enabled(self, state):
# CONF.get(self.CONF_SECTION, 'check_eol_chars')
self.checkeolchars_enabled = state
def set_always_remove_trailing_spaces(self, state):
# CONF.get(self.CONF_SECTION, 'always_remove_trailing_spaces')
self.always_remove_trailing_spaces = state
if self.data:
for finfo in self.data:
finfo.editor.set_remove_trailing_spaces(state)
def set_add_newline(self, state):
self.add_newline = state
if self.data:
for finfo in self.data:
finfo.editor.set_add_newline(state)
def set_remove_trailing_newlines(self, state):
self.remove_trailing_newlines = state
if self.data:
for finfo in self.data:
finfo.editor.set_remove_trailing_newlines(state)
def set_convert_eol_on_save(self, state):
"""If `state` is `True`, saving files will convert line endings."""
# CONF.get(self.CONF_SECTION, 'convert_eol_on_save')
self.convert_eol_on_save = state
def set_convert_eol_on_save_to(self, state):
"""`state` can be one of ('LF', 'CRLF', 'CR')"""
# CONF.get(self.CONF_SECTION, 'convert_eol_on_save_to')
self.convert_eol_on_save_to = state
def set_focus_to_editor(self, state):
self.focus_to_editor = state
def set_run_cell_copy(self, state):
"""If `state` is ``True``, code cells will be copied to the console."""
self.run_cell_copy = state
def set_current_project_path(self, root_path=None):
"""
Set the current active project root path.
Parameters
----------
root_path: str or None, optional
Path to current project root path. Default is None.
"""
for finfo in self.data:
finfo.editor.set_current_project_path(root_path)
# ------ Stacked widget management
def get_stack_index(self):
return self.tabs.currentIndex()
def get_current_finfo(self):
if self.data:
return self.data[self.get_stack_index()]
def get_current_editor(self):
return self.tabs.currentWidget()
def get_stack_count(self):
return self.tabs.count()
def set_stack_index(self, index, instance=None):
if instance == self or instance == None:
self.tabs.setCurrentIndex(index)
def set_tabbar_visible(self, state):
self.tabs.tabBar().setVisible(state)
def remove_from_data(self, index):
self.tabs.blockSignals(True)
self.tabs.removeTab(index)
self.data.pop(index)
self.tabs.blockSignals(False)
self.update_actions()
def __modified_readonly_title(self, title, is_modified, is_readonly):
if is_modified is not None and is_modified:
title += "*"
if is_readonly is not None and is_readonly:
title = "(%s)" % title
return title
def get_tab_text(self, index, is_modified=None, is_readonly=None):
"""Return tab title."""
files_path_list = [finfo.filename for finfo in self.data]
fname = self.data[index].filename
fname = sourcecode.disambiguate_fname(files_path_list, fname)
return self.__modified_readonly_title(fname,
is_modified, is_readonly)
def get_tab_tip(self, filename, is_modified=None, is_readonly=None):
"""Return tab menu title"""
text = u"%s — %s"
text = self.__modified_readonly_title(text,
is_modified, is_readonly)
if self.tempfile_path is not None\
and filename == encoding.to_unicode_from_fs(self.tempfile_path):
temp_file_str = to_text_string(_("Temporary file"))
return text % (temp_file_str, self.tempfile_path)
else:
return text % (osp.basename(filename), osp.dirname(filename))
def add_to_data(self, finfo, set_current, add_where='end'):
finfo.editor.oe_proxy = None
index = 0 if add_where == 'start' else len(self.data)
self.data.insert(index, finfo)
index = self.data.index(finfo)
editor = finfo.editor
self.tabs.insertTab(index, editor, self.get_tab_text(index))
self.set_stack_title(index, False)
if set_current:
self.set_stack_index(index)
self.current_changed(index)
self.update_actions()
def __repopulate_stack(self):
self.tabs.blockSignals(True)
self.tabs.clear()
for finfo in self.data:
if finfo.newly_created:
is_modified = True
else:
is_modified = None
index = self.data.index(finfo)
tab_text = self.get_tab_text(index, is_modified)
tab_tip = self.get_tab_tip(finfo.filename)
index = self.tabs.addTab(finfo.editor, tab_text)
self.tabs.setTabToolTip(index, tab_tip)
self.tabs.blockSignals(False)
def rename_in_data(self, original_filename, new_filename):
index = self.has_filename(original_filename)
if index is None:
return
finfo = self.data[index]
# Send close request to LSP
finfo.editor.notify_close()
# Set new filename
finfo.filename = new_filename
finfo.editor.filename = new_filename
# File type has changed!
original_ext = osp.splitext(original_filename)[1]
new_ext = osp.splitext(new_filename)[1]
if original_ext != new_ext:
# Set file language and re-run highlighter
txt = to_text_string(finfo.editor.get_text_with_eol())
language = get_file_language(new_filename, txt)
finfo.editor.set_language(language, new_filename)
finfo.editor.run_pygments_highlighter()
# If the user renamed the file to a different language, we
# need to emit sig_open_file to see if we can start a
# language server for it.
options = {
'language': language,
'filename': new_filename,
'codeeditor': finfo.editor
}
self.sig_open_file.emit(options)
# Update panels
finfo.editor.set_debug_panel(
show_debug_panel=True, language=language)
finfo.editor.cleanup_code_analysis()
finfo.editor.cleanup_folding()
else:
# If there's no language change, we simply need to request a
# document_did_open for the new file.
finfo.editor.document_did_open()
set_new_index = index == self.get_stack_index()
current_fname = self.get_current_filename()
finfo.editor.filename = new_filename
new_index = self.data.index(finfo)
self.__repopulate_stack()
if set_new_index:
self.set_stack_index(new_index)
else:
# Fixes spyder-ide/spyder#1287.
self.set_current_filename(current_fname)
if self.outlineexplorer is not None:
self.outlineexplorer.file_renamed(
finfo.editor.oe_proxy, finfo.filename)
return new_index
def set_stack_title(self, index, is_modified):
finfo = self.data[index]
fname = finfo.filename
is_modified = (is_modified or finfo.newly_created) and not finfo.default
is_readonly = finfo.editor.isReadOnly()
tab_text = self.get_tab_text(index, is_modified, is_readonly)
tab_tip = self.get_tab_tip(fname, is_modified, is_readonly)
# Only update tab text if have changed, otherwise an unwanted scrolling
# will happen when changing tabs. See spyder-ide/spyder#1170.
if tab_text != self.tabs.tabText(index):
self.tabs.setTabText(index, tab_text)
self.tabs.setTabToolTip(index, tab_tip)
# ------ Context menu
def __setup_menu(self):
"""Setup tab context menu before showing it"""
self.menu.clear()
if self.data:
actions = self.menu_actions
else:
actions = (self.new_action, self.open_action)
self.setFocus() # --> Editor.__get_focus_editortabwidget
add_actions(self.menu, list(actions) + self.__get_split_actions())
self.close_action.setEnabled(self.is_closable)
if sys.platform == 'darwin':
set_menu_icons(self.menu, True)
# ------ Hor/Ver splitting
def __get_split_actions(self):
if self.parent() is not None:
plugin = self.parent().plugin
else:
plugin = None
# New window
if plugin is not None:
self.new_window_action = create_action(
self, _("New window"),
icon=ima.icon('newwindow'),
tip=_("Create a new editor window"),
triggered=plugin.create_new_window)
# Splitting
self.versplit_action = create_action(
self,
_("Split vertically"),
icon=ima.icon('versplit'),
tip=_("Split vertically this editor window"),
triggered=lambda: self.sig_split_vertically.emit(),
shortcut=CONF.get_shortcut(context='Editor',
name='split vertically'),
context=Qt.WidgetShortcut)
self.horsplit_action = create_action(
self,
_("Split horizontally"),
icon=ima.icon('horsplit'),
tip=_("Split horizontally this editor window"),
triggered=lambda: self.sig_split_horizontally.emit(),
shortcut=CONF.get_shortcut(context='Editor',
name='split horizontally'),
context=Qt.WidgetShortcut)
self.close_action = create_action(
self,
_("Close this panel"),
icon=ima.icon('close_panel'),
triggered=self.close_split,
shortcut=CONF.get_shortcut(context='Editor',
name='close split panel'),
context=Qt.WidgetShortcut)
# Regular actions
actions = [MENU_SEPARATOR, self.versplit_action,
self.horsplit_action, self.close_action]
if self.new_window:
window = self.window()
close_window_action = create_action(
self, _("Close window"),
icon=ima.icon('close_pane'),
triggered=window.close)
actions += [MENU_SEPARATOR, self.new_window_action,
close_window_action]
elif plugin is not None:
if plugin._undocked_window is not None:
actions += [MENU_SEPARATOR, plugin._dock_action]
else:
actions += [MENU_SEPARATOR, self.new_window_action,
plugin._undock_action,
plugin._close_plugin_action]
return actions
def reset_orientation(self):
self.horsplit_action.setEnabled(True)
self.versplit_action.setEnabled(True)
def set_orientation(self, orientation):
self.horsplit_action.setEnabled(orientation == Qt.Horizontal)
self.versplit_action.setEnabled(orientation == Qt.Vertical)
def update_actions(self):
state = self.get_stack_count() > 0
self.horsplit_action.setEnabled(state)
self.versplit_action.setEnabled(state)
# ------ Accessors
def get_current_filename(self):
if self.data:
return self.data[self.get_stack_index()].filename
def get_current_language(self):
if self.data:
return self.data[self.get_stack_index()].editor.language
def get_filenames(self):
"""
Return a list with the names of all the files currently opened in
the editorstack.
"""
return [finfo.filename for finfo in self.data]
def has_filename(self, filename):
"""Return the self.data index position for the filename.
Args:
filename: Name of the file to search for in self.data.
Returns:
The self.data index for the filename. Returns None
if the filename is not found in self.data.
"""
fixpath = lambda path: osp.normcase(osp.realpath(path))
for index, finfo in enumerate(self.data):
if fixpath(filename) == fixpath(finfo.filename):
return index
return None
def set_current_filename(self, filename, focus=True):
"""Set current filename and return the associated editor instance."""
# This is necessary to catch an error on Windows for files in a
# directory junction pointing to a symlink whose target is on a
# network drive that is unavailable at startup.
# Fixes spyder-ide/spyder#15714
try:
index = self.has_filename(filename)
except FileNotFoundError:
index = None
if index is not None:
if focus:
self.set_stack_index(index)
editor = self.data[index].editor
if focus:
editor.setFocus()
else:
self.stack_history.remove_and_append(index)
return editor
def is_file_opened(self, filename=None):
"""Return if filename is in the editor stack.
Args:
filename: Name of the file to search for. If filename is None,
then checks if any file is open.
Returns:
True: If filename is None and a file is open.
False: If filename is None and no files are open.
None: If filename is not None and the file isn't found.
integer: Index of file name in editor stack.
"""
if filename is None:
# Is there any file opened?
return len(self.data) > 0
else:
return self.has_filename(filename)
def get_index_from_filename(self, filename):
"""
Return the position index of a file in the tab bar of the editorstack
from its name.
"""
filenames = [d.filename for d in self.data]
return filenames.index(filename)
@Slot(int, int)
def move_editorstack_data(self, start, end):
"""Reorder editorstack.data so it is synchronized with the tab bar when
tabs are moved."""
if start < 0 or end < 0:
return
else:
steps = abs(end - start)
direction = (end-start) // steps # +1 for right, -1 for left
data = self.data
self.blockSignals(True)
for i in range(start, end, direction):
data[i], data[i+direction] = data[i+direction], data[i]
self.blockSignals(False)
self.refresh()
# ------ Close file, tabwidget...
def close_file(self, index=None, force=False):
"""Close file (index=None -> close current file)
Keep current file index unchanged (if current file
that is being closed)"""
current_index = self.get_stack_index()
count = self.get_stack_count()
if index is None:
if count > 0:
index = current_index
else:
self.find_widget.set_editor(None)
return
new_index = None
if count > 1:
if current_index == index:
new_index = self._get_previous_file_index()
else:
new_index = current_index
can_close_file = self.parent().plugin.can_close_file(
self.data[index].filename) if self.parent() else True
is_ok = (force or self.save_if_changed(cancelable=True, index=index)
and can_close_file)
if is_ok:
finfo = self.data[index]
self.threadmanager.close_threads(finfo)
# Removing editor reference from outline explorer settings:
if self.outlineexplorer is not None:
self.outlineexplorer.remove_editor(finfo.editor.oe_proxy)
filename = self.data[index].filename
self.remove_from_data(index)
finfo.editor.notify_close()
# We pass self object ID as a QString, because otherwise it would
# depend on the platform: long for 64bit, int for 32bit. Replacing
# by long all the time is not working on some 32bit platforms.
# See spyder-ide/spyder#1094 and spyder-ide/spyder#1098.
self.sig_close_file.emit(str(id(self)), filename)
self.opened_files_list_changed.emit()
self.update_code_analysis_actions.emit()
self.refresh_file_dependent_actions.emit()
self.update_plugin_title.emit()
editor = self.get_current_editor()
if editor:
editor.setFocus()
if new_index is not None:
if index < new_index:
new_index -= 1
self.set_stack_index(new_index)
self.add_last_closed_file(finfo.filename)
if finfo.filename in self.autosave.file_hashes:
del self.autosave.file_hashes[finfo.filename]
if self.get_stack_count() == 0 and self.create_new_file_if_empty:
self.sig_new_file[()].emit()
return False
self.__modify_stack_title()
return is_ok
def register_completion_capabilities(self, capabilities, language):
"""
Register completion server capabilities across all editors.
Parameters
----------
capabilities: dict
Capabilities supported by a language server.
language: str
Programming language for the language server (it has to be
in small caps).
"""
for index in range(self.get_stack_count()):
editor = self.tabs.widget(index)
if editor.language.lower() == language:
editor.register_completion_capabilities(capabilities)
def start_completion_services(self, language):
"""Notify language server availability to code editors."""
for index in range(self.get_stack_count()):
editor = self.tabs.widget(index)
if editor.language.lower() == language:
editor.start_completion_services()
def stop_completion_services(self, language):
"""Notify language server unavailability to code editors."""
for index in range(self.get_stack_count()):
editor = self.tabs.widget(index)
if editor.language.lower() == language:
editor.stop_completion_services()
def close_all_files(self):
"""Close all opened scripts"""
while self.close_file():
pass
def close_all_right(self):
""" Close all files opened to the right """
num = self.get_stack_index()
n = self.get_stack_count()
for __ in range(num, n-1):
self.close_file(num+1)
def close_all_but_this(self):
"""Close all files but the current one"""
self.close_all_right()
for __ in range(0, self.get_stack_count() - 1):
self.close_file(0)
def sort_file_tabs_alphabetically(self):
"""Sort open tabs alphabetically."""
while self.sorted() is False:
for i in range(0, self.tabs.tabBar().count()):
if(self.tabs.tabBar().tabText(i) >
self.tabs.tabBar().tabText(i + 1)):
self.tabs.tabBar().moveTab(i, i + 1)
def sorted(self):
"""Utility function for sort_file_tabs_alphabetically()."""
for i in range(0, self.tabs.tabBar().count() - 1):
if (self.tabs.tabBar().tabText(i) >
self.tabs.tabBar().tabText(i + 1)):
return False
return True
def add_last_closed_file(self, fname):
"""Add to last closed file list."""
if fname in self.last_closed_files:
self.last_closed_files.remove(fname)
self.last_closed_files.insert(0, fname)
if len(self.last_closed_files) > 10:
self.last_closed_files.pop(-1)
def get_last_closed_files(self):
return self.last_closed_files
def set_last_closed_files(self, fnames):
self.last_closed_files = fnames
# ------ Save
def save_if_changed(self, cancelable=False, index=None):
"""Ask user to save file if modified.
Args:
cancelable: Show Cancel button.
index: File to check for modification.
Returns:
False when save() fails or is cancelled.
True when save() is successful, there are no modifications,
or user selects No or NoToAll.
This function controls the message box prompt for saving
changed files. The actual save is performed in save() for
each index processed. This function also removes autosave files
corresponding to files the user chooses not to save.
"""
if index is None:
indexes = list(range(self.get_stack_count()))
else:
indexes = [index]
buttons = QMessageBox.Yes | QMessageBox.No
if cancelable:
buttons |= QMessageBox.Cancel
unsaved_nb = 0
for index in indexes:
if self.data[index].editor.document().isModified():
unsaved_nb += 1
if not unsaved_nb:
# No file to save
return True
if unsaved_nb > 1:
buttons |= int(QMessageBox.YesToAll | QMessageBox.NoToAll)
yes_all = no_all = False
for index in indexes:
self.set_stack_index(index)
finfo = self.data[index]
if finfo.filename == self.tempfile_path or yes_all:
if not self.save(index):
return False
elif no_all:
self.autosave.remove_autosave_file(finfo)
elif (finfo.editor.document().isModified() and
self.save_dialog_on_tests):
self.msgbox = QMessageBox(
QMessageBox.Question,
self.title,
_("%s has been modified."
"
Do you want to save changes?"
) % osp.basename(finfo.filename),
buttons,
parent=self)
answer = self.msgbox.exec_()
if answer == QMessageBox.Yes:
if not self.save(index):
return False
elif answer == QMessageBox.No:
self.autosave.remove_autosave_file(finfo.filename)
elif answer == QMessageBox.YesToAll:
if not self.save(index):
return False
yes_all = True
elif answer == QMessageBox.NoToAll:
self.autosave.remove_autosave_file(finfo.filename)
no_all = True
elif answer == QMessageBox.Cancel:
return False
return True
def compute_hash(self, fileinfo):
"""Compute hash of contents of editor.
Args:
fileinfo: FileInfo object associated to editor whose hash needs
to be computed.
Returns:
int: computed hash.
"""
txt = to_text_string(fileinfo.editor.get_text_with_eol())
return hash(txt)
def _write_to_file(self, fileinfo, filename):
"""Low-level function for writing text of editor to file.
Args:
fileinfo: FileInfo object associated to editor to be saved
filename: str with filename to save to
This is a low-level function that only saves the text to file in the
correct encoding without doing any error handling.
"""
txt = to_text_string(fileinfo.editor.get_text_with_eol())
fileinfo.encoding = encoding.write(txt, filename, fileinfo.encoding)
def save(self, index=None, force=False, save_new_files=True):
"""Write text of editor to a file.
Args:
index: self.data index to save. If None, defaults to
currentIndex().
force: Force save regardless of file state.
Returns:
True upon successful save or when file doesn't need to be saved.
False if save failed.
If the text isn't modified and it's not newly created, then the save
is aborted. If the file hasn't been saved before, then save_as()
is invoked. Otherwise, the file is written using the file name
currently in self.data. This function doesn't change the file name.
"""
if index is None:
# Save the currently edited file
if not self.get_stack_count():
return
index = self.get_stack_index()
finfo = self.data[index]
if not (finfo.editor.document().isModified() or
finfo.newly_created) and not force:
return True
if not osp.isfile(finfo.filename) and not force:
# File has not been saved yet
if save_new_files:
return self.save_as(index=index)
# The file doesn't need to be saved
return True
if self.always_remove_trailing_spaces:
self.remove_trailing_spaces(index)
if self.remove_trailing_newlines:
self.trim_trailing_newlines(index)
if self.add_newline:
self.add_newline_to_file(index)
if self.convert_eol_on_save:
# hack to account for the fact that the config file saves
# CR/LF/CRLF while set_os_eol_chars wants the os.name value.
osname_lookup = {'LF': 'posix', 'CRLF': 'nt', 'CR': 'mac'}
osname = osname_lookup[self.convert_eol_on_save_to]
self.set_os_eol_chars(osname=osname)
try:
if self.format_on_save and finfo.editor.formatting_enabled:
# Autoformat document and then save
finfo.editor.sig_stop_operation_in_progress.connect(
functools.partial(self._save_file, finfo, index))
finfo.editor.format_document()
else:
self._save_file(finfo, index)
return True
except EnvironmentError as error:
self.msgbox = QMessageBox(
QMessageBox.Critical,
_("Save Error"),
_("Unable to save file '%s'"
"
Error message:
%s"
) % (osp.basename(finfo.filename),
str(error)),
parent=self)
self.msgbox.exec_()
return False
def _save_file(self, finfo, index):
self._write_to_file(finfo, finfo.filename)
file_hash = self.compute_hash(finfo)
self.autosave.file_hashes[finfo.filename] = file_hash
self.autosave.remove_autosave_file(finfo.filename)
finfo.newly_created = False
self.encoding_changed.emit(finfo.encoding)
finfo.lastmodified = QFileInfo(finfo.filename).lastModified()
# We pass self object ID as a QString, because otherwise it would
# depend on the platform: long for 64bit, int for 32bit. Replacing
# by long all the time is not working on some 32bit platforms.
# See spyder-ide/spyder#1094 and spyder-ide/spyder#1098.
# The filename is passed instead of an index in case the tabs
# have been rearranged. See spyder-ide/spyder#5703.
self.file_saved.emit(str(id(self)),
finfo.filename, finfo.filename)
finfo.editor.document().setModified(False)
self.modification_changed(index=index)
self.analyze_script(index)
finfo.editor.notify_save()
def file_saved_in_other_editorstack(self, original_filename, filename):
"""
File was just saved in another editorstack, let's synchronize!
This avoids file being automatically reloaded.
The original filename is passed instead of an index in case the tabs
on the editor stacks were moved and are now in a different order - see
spyder-ide/spyder#5703.
Filename is passed in case file was just saved as another name.
"""
index = self.has_filename(original_filename)
if index is None:
return
finfo = self.data[index]
finfo.newly_created = False
finfo.filename = to_text_string(filename)
finfo.lastmodified = QFileInfo(finfo.filename).lastModified()
def select_savename(self, original_filename):
"""Select a name to save a file.
Args:
original_filename: Used in the dialog to display the current file
path and name.
Returns:
Normalized path for the selected file name or None if no name was
selected.
"""
if self.edit_filetypes is None:
self.edit_filetypes = get_edit_filetypes()
if self.edit_filters is None:
self.edit_filters = get_edit_filters()
# Don't use filters on KDE to not make the dialog incredible
# slow
# Fixes spyder-ide/spyder#4156.
if is_kde_desktop() and not is_anaconda():
filters = ''
selectedfilter = ''
else:
filters = self.edit_filters
selectedfilter = get_filter(self.edit_filetypes,
osp.splitext(original_filename)[1])
self.redirect_stdio.emit(False)
filename, _selfilter = getsavefilename(self, _("Save file"),
original_filename,
filters=filters,
selectedfilter=selectedfilter,
options=QFileDialog.HideNameFilterDetails)
self.redirect_stdio.emit(True)
if filename:
return osp.normpath(filename)
return None
def save_as(self, index=None):
"""Save file as...
Args:
index: self.data index for the file to save.
Returns:
False if no file name was selected or if save() was unsuccessful.
True is save() was successful.
Gets the new file name from select_savename(). If no name is chosen,
then the save_as() aborts. Otherwise, the current stack is checked
to see if the selected name already exists and, if so, then the tab
with that name is closed.
The current stack (self.data) and current tabs are updated with the
new name and other file info. The text is written with the new
name using save() and the name change is propagated to the other stacks
via the file_renamed_in_data signal.
"""
if index is None:
# Save the currently edited file
index = self.get_stack_index()
finfo = self.data[index]
original_newly_created = finfo.newly_created
# The next line is necessary to avoid checking if the file exists
# While running __check_file_status
# See spyder-ide/spyder#3678 and spyder-ide/spyder#3026.
finfo.newly_created = True
original_filename = finfo.filename
filename = self.select_savename(original_filename)
if filename:
ao_index = self.has_filename(filename)
# Note: ao_index == index --> saving an untitled file
if ao_index is not None and ao_index != index:
if not self.close_file(ao_index):
return
if ao_index < index:
index -= 1
new_index = self.rename_in_data(original_filename,
new_filename=filename)
# We pass self object ID as a QString, because otherwise it would
# depend on the platform: long for 64bit, int for 32bit. Replacing
# by long all the time is not working on some 32bit platforms
# See spyder-ide/spyder#1094 and spyder-ide/spyder#1098.
self.file_renamed_in_data.emit(str(id(self)),
original_filename, filename)
ok = self.save(index=new_index, force=True)
self.refresh(new_index)
self.set_stack_index(new_index)
return ok
else:
finfo.newly_created = original_newly_created
return False
def save_copy_as(self, index=None):
"""Save copy of file as...
Args:
index: self.data index for the file to save.
Returns:
False if no file name was selected or if save() was unsuccessful.
True is save() was successful.
Gets the new file name from select_savename(). If no name is chosen,
then the save_copy_as() aborts. Otherwise, the current stack is
checked to see if the selected name already exists and, if so, then the
tab with that name is closed.
Unlike save_as(), this calls write() directly instead of using save().
The current file and tab aren't changed at all. The copied file is
opened in a new tab.
"""
if index is None:
# Save the currently edited file
index = self.get_stack_index()
finfo = self.data[index]
original_filename = finfo.filename
filename = self.select_savename(original_filename)
if filename:
ao_index = self.has_filename(filename)
# Note: ao_index == index --> saving an untitled file
if ao_index is not None and ao_index != index:
if not self.close_file(ao_index):
return
if ao_index < index:
index -= 1
try:
self._write_to_file(finfo, filename)
# open created copy file
self.plugin_load.emit(filename)
return True
except EnvironmentError as error:
self.msgbox = QMessageBox(
QMessageBox.Critical,
_("Save Error"),
_("Unable to save file '%s'"
"
Error message:
%s"
) % (osp.basename(finfo.filename),
str(error)),
parent=self)
self.msgbox.exec_()
else:
return False
def save_all(self, save_new_files=True):
"""Save all opened files.
Iterate through self.data and call save() on any modified files.
"""
all_saved = True
for index in range(self.get_stack_count()):
if self.data[index].editor.document().isModified():
all_saved &= self.save(index, save_new_files=save_new_files)
return all_saved
#------ Update UI
def start_stop_analysis_timer(self):
self.is_analysis_done = False
self.analysis_timer.stop()
self.analysis_timer.start()
def analyze_script(self, index=None):
"""Analyze current script for TODOs."""
if self.is_analysis_done:
return
if index is None:
index = self.get_stack_index()
if self.data and len(self.data) > index:
finfo = self.data[index]
if self.todolist_enabled:
finfo.run_todo_finder()
self.is_analysis_done = True
def set_todo_results(self, filename, todo_results):
"""Synchronize todo results between editorstacks"""
index = self.has_filename(filename)
if index is None:
return
self.data[index].set_todo_results(todo_results)
def get_todo_results(self):
if self.data:
return self.data[self.get_stack_index()].todo_results
def current_changed(self, index):
"""Stack index has changed"""
editor = self.get_current_editor()
if index != -1:
editor.setFocus()
logger.debug("Set focus to: %s" % editor.filename)
else:
self.reset_statusbar.emit()
self.opened_files_list_changed.emit()
self.stack_history.refresh()
self.stack_history.remove_and_append(index)
# Needed to avoid an error generated after moving/renaming
# files outside Spyder while in debug mode.
# See spyder-ide/spyder#8749.
try:
logger.debug("Current changed: %d - %s" %
(index, self.data[index].editor.filename))
except IndexError:
pass
self.update_plugin_title.emit()
# Make sure that any replace happens in the editor on top
# See spyder-ide/spyder#9688.
self.find_widget.set_editor(editor, refresh=False)
if editor is not None:
# Needed in order to handle the close of files open in a directory
# that has been renamed. See spyder-ide/spyder#5157.
try:
line, col = editor.get_cursor_line_column()
self.current_file_changed.emit(self.data[index].filename,
editor.get_position('cursor'),
line, col)
except IndexError:
pass
def _get_previous_file_index(self):
"""Return the penultimate element of the stack history."""
try:
return self.stack_history[-2]
except IndexError:
return None
def tab_navigation_mru(self, forward=True):
"""
Tab navigation with "most recently used" behaviour.
It's fired when pressing 'go to previous file' or 'go to next file'
shortcuts.
forward:
True: move to next file
False: move to previous file
"""
self.tabs_switcher = TabSwitcherWidget(self, self.stack_history,
self.tabs)
self.tabs_switcher.show()
self.tabs_switcher.select_row(1 if forward else -1)
self.tabs_switcher.setFocus()
def focus_changed(self):
"""Editor focus has changed"""
fwidget = QApplication.focusWidget()
for finfo in self.data:
if fwidget is finfo.editor:
if finfo.editor.operation_in_progress:
self.spinner.start()
else:
self.spinner.stop()
self.refresh()
self.editor_focus_changed.emit()
def _refresh_outlineexplorer(self, index=None, update=True, clear=False):
"""Refresh outline explorer panel"""
oe = self.outlineexplorer
if oe is None:
return
if index is None:
index = self.get_stack_index()
if self.data and len(self.data) > index:
finfo = self.data[index]
oe.setEnabled(True)
oe.set_current_editor(finfo.editor.oe_proxy,
update=update, clear=clear)
if index != self.get_stack_index():
# The last file added to the outline explorer is not the
# currently focused one in the editor stack. Therefore,
# we need to force a refresh of the outline explorer to set
# the current editor to the currently focused one in the
# editor stack. See spyder-ide/spyder#8015.
self._refresh_outlineexplorer(update=False)
return
self._sync_outlineexplorer_file_order()
def _sync_outlineexplorer_file_order(self):
"""
Order the root file items of the outline explorer as in the tabbar
of the current EditorStack.
"""
if self.outlineexplorer is not None:
self.outlineexplorer.treewidget.set_editor_ids_order(
[finfo.editor.get_document_id() for finfo in self.data])
def __refresh_statusbar(self, index):
"""Refreshing statusbar widgets"""
if self.data and len(self.data) > index:
finfo = self.data[index]
self.encoding_changed.emit(finfo.encoding)
# Refresh cursor position status:
line, index = finfo.editor.get_cursor_line_column()
self.sig_editor_cursor_position_changed.emit(line, index)
def __refresh_readonly(self, index):
if self.data and len(self.data) > index:
finfo = self.data[index]
read_only = not QFileInfo(finfo.filename).isWritable()
if not osp.isfile(finfo.filename):
# This is an 'untitledX.py' file (newly created)
read_only = False
elif os.name == 'nt':
try:
# Try to open the file to see if its permissions allow
# to write on it
# Fixes spyder-ide/spyder#10657
fd = os.open(finfo.filename, os.O_RDWR)
os.close(fd)
except (IOError, OSError):
read_only = True
finfo.editor.setReadOnly(read_only)
self.readonly_changed.emit(read_only)
def __check_file_status(self, index):
"""Check if file has been changed in any way outside Spyder:
1. removed, moved or renamed outside Spyder
2. modified outside Spyder"""
if self.__file_status_flag:
# Avoid infinite loop: when the QMessageBox.question pops, it
# gets focus and then give it back to the CodeEditor instance,
# triggering a refresh cycle which calls this method
return
self.__file_status_flag = True
if len(self.data) <= index:
index = self.get_stack_index()
finfo = self.data[index]
name = osp.basename(finfo.filename)
if finfo.newly_created:
# File was just created (not yet saved): do nothing
# (do not return because of the clean-up at the end of the method)
pass
elif not osp.isfile(finfo.filename):
# File doesn't exist (removed, moved or offline):
self.msgbox = QMessageBox(
QMessageBox.Warning,
self.title,
_("%s is unavailable "
"(this file may have been removed, moved "
"or renamed outside Spyder)."
"
Do you want to close it?") % name,
QMessageBox.Yes | QMessageBox.No,
self)
answer = self.msgbox.exec_()
if answer == QMessageBox.Yes:
self.close_file(index)
else:
finfo.newly_created = True
finfo.editor.document().setModified(True)
self.modification_changed(index=index)
else:
# Else, testing if it has been modified elsewhere:
lastm = QFileInfo(finfo.filename).lastModified()
if to_text_string(lastm.toString()) \
!= to_text_string(finfo.lastmodified.toString()):
if finfo.editor.document().isModified():
self.msgbox = QMessageBox(
QMessageBox.Question,
self.title,
_("%s has been modified outside Spyder."
"
Do you want to reload it and lose all "
"your changes?") % name,
QMessageBox.Yes | QMessageBox.No,
self)
answer = self.msgbox.exec_()
if answer == QMessageBox.Yes:
self.reload(index)
else:
finfo.lastmodified = lastm
else:
self.reload(index)
# Finally, resetting temporary flag:
self.__file_status_flag = False
def __modify_stack_title(self):
for index, finfo in enumerate(self.data):
state = finfo.editor.document().isModified()
self.set_stack_title(index, state)
def refresh(self, index=None):
"""Refresh tabwidget"""
if index is None:
index = self.get_stack_index()
# Set current editor
if self.get_stack_count():
index = self.get_stack_index()
finfo = self.data[index]
editor = finfo.editor
editor.setFocus()
self._refresh_outlineexplorer(index, update=False)
self.update_code_analysis_actions.emit()
self.__refresh_statusbar(index)
self.__refresh_readonly(index)
self.__check_file_status(index)
self.__modify_stack_title()
self.update_plugin_title.emit()
else:
editor = None
# Update the modification-state-dependent parameters
self.modification_changed()
# Update FindReplace binding
self.find_widget.set_editor(editor, refresh=False)
def modification_changed(self, state=None, index=None, editor_id=None):
"""
Current editor's modification state has changed
--> change tab title depending on new modification state
--> enable/disable save/save all actions
"""
if editor_id is not None:
for index, _finfo in enumerate(self.data):
if id(_finfo.editor) == editor_id:
break
# This must be done before refreshing save/save all actions:
# (otherwise Save/Save all actions will always be enabled)
self.opened_files_list_changed.emit()
# --
if index is None:
index = self.get_stack_index()
if index == -1:
return
finfo = self.data[index]
if state is None:
state = finfo.editor.document().isModified() or finfo.newly_created
self.set_stack_title(index, state)
# Toggle save/save all actions state
self.save_action.setEnabled(state)
self.refresh_save_all_action.emit()
# Refreshing eol mode
eol_chars = finfo.editor.get_line_separator()
self.refresh_eol_chars(eol_chars)
self.stack_history.refresh()
def refresh_eol_chars(self, eol_chars):
os_name = sourcecode.get_os_name_from_eol_chars(eol_chars)
self.sig_refresh_eol_chars.emit(os_name)
# ------ Load, reload
def reload(self, index):
"""Reload file from disk."""
finfo = self.data[index]
logger.debug("Reloading {}".format(finfo.filename))
txt, finfo.encoding = encoding.read(finfo.filename)
finfo.lastmodified = QFileInfo(finfo.filename).lastModified()
position = finfo.editor.get_position('cursor')
finfo.editor.set_text(txt)
finfo.editor.document().setModified(False)
self.autosave.file_hashes[finfo.filename] = hash(txt)
finfo.editor.set_cursor_position(position)
#XXX CodeEditor-only: re-scan the whole text to rebuild outline
# explorer data from scratch (could be optimized because
# rehighlighting text means searching for all syntax coloring
# patterns instead of only searching for class/def patterns which
# would be sufficient for outline explorer data.
finfo.editor.rehighlight()
def revert(self):
"""Revert file from disk."""
index = self.get_stack_index()
finfo = self.data[index]
logger.debug("Reverting {}".format(finfo.filename))
filename = finfo.filename
if finfo.editor.document().isModified():
self.msgbox = QMessageBox(
QMessageBox.Warning,
self.title,
_("All changes to %s will be lost."
"
Do you want to revert file from disk?"
) % osp.basename(filename),
QMessageBox.Yes | QMessageBox.No,
self)
answer = self.msgbox.exec_()
if answer != QMessageBox.Yes:
return
self.reload(index)
def create_new_editor(self, fname, enc, txt, set_current, new=False,
cloned_from=None, add_where='end'):
"""
Create a new editor instance
Returns finfo object (instead of editor as in previous releases)
"""
editor = codeeditor.CodeEditor(self)
editor.go_to_definition.connect(
lambda fname, line, column: self.sig_go_to_definition.emit(
fname, line, column))
finfo = FileInfo(fname, enc, editor, new, self.threadmanager)
self.add_to_data(finfo, set_current, add_where)
finfo.sig_send_to_help.connect(self.send_to_help)
finfo.sig_show_object_info.connect(self.inspect_current_object)
finfo.todo_results_changed.connect(
lambda: self.todo_results_changed.emit())
finfo.edit_goto.connect(lambda fname, lineno, name:
self.edit_goto.emit(fname, lineno, name))
finfo.sig_save_bookmarks.connect(lambda s1, s2:
self.sig_save_bookmarks.emit(s1, s2))
editor.sig_run_selection.connect(self.run_selection)
editor.sig_run_cell.connect(self.run_cell)
editor.sig_debug_cell.connect(self.debug_cell)
editor.sig_run_cell_and_advance.connect(self.run_cell_and_advance)
editor.sig_re_run_last_cell.connect(self.re_run_last_cell)
editor.sig_new_file.connect(self.sig_new_file.emit)
editor.sig_breakpoints_saved.connect(self.sig_breakpoints_saved)
editor.sig_process_code_analysis.connect(
lambda: self.update_code_analysis_actions.emit())
editor.sig_refresh_formatting.connect(self.sig_refresh_formatting)
language = get_file_language(fname, txt)
editor.setup_editor(
linenumbers=self.linenumbers_enabled,
show_blanks=self.blanks_enabled,
underline_errors=self.underline_errors_enabled,
scroll_past_end=self.scrollpastend_enabled,
edge_line=self.edgeline_enabled,
edge_line_columns=self.edgeline_columns,
language=language,
markers=self.has_markers(),
font=self.default_font,
color_scheme=self.color_scheme,
wrap=self.wrap_enabled,
tab_mode=self.tabmode_enabled,
strip_mode=self.stripmode_enabled,
intelligent_backspace=self.intelligent_backspace_enabled,
automatic_completions=self.automatic_completions_enabled,
automatic_completions_after_chars=self.automatic_completion_chars,
automatic_completions_after_ms=self.automatic_completion_ms,
code_snippets=self.code_snippets_enabled,
completions_hint=self.completions_hint_enabled,
completions_hint_after_ms=self.completions_hint_after_ms,
hover_hints=self.hover_hints_enabled,
highlight_current_line=self.highlight_current_line_enabled,
highlight_current_cell=self.highlight_current_cell_enabled,
occurrence_highlighting=self.occurrence_highlighting_enabled,
occurrence_timeout=self.occurrence_highlighting_timeout,
close_parentheses=self.close_parentheses_enabled,
close_quotes=self.close_quotes_enabled,
add_colons=self.add_colons_enabled,
auto_unindent=self.auto_unindent_enabled,
indent_chars=self.indent_chars,
tab_stop_width_spaces=self.tab_stop_width_spaces,
cloned_from=cloned_from,
filename=fname,
show_class_func_dropdown=self.show_class_func_dropdown,
indent_guides=self.indent_guides,
folding=self.code_folding_enabled,
remove_trailing_spaces=self.always_remove_trailing_spaces,
remove_trailing_newlines=self.remove_trailing_newlines,
add_newline=self.add_newline,
format_on_save=self.format_on_save
)
if cloned_from is None:
editor.set_text(txt)
editor.document().setModified(False)
finfo.text_changed_at.connect(
lambda fname, position:
self.text_changed_at.emit(fname, position))
editor.sig_cursor_position_changed.connect(
self.editor_cursor_position_changed)
editor.textChanged.connect(self.start_stop_analysis_timer)
# Register external panels
for panel_class, args, kwargs, position in self.external_panels:
self.register_panel(
panel_class, *args, position=position, **kwargs)
def perform_completion_request(lang, method, params):
self.sig_perform_completion_request.emit(lang, method, params)
editor.sig_perform_completion_request.connect(
perform_completion_request)
editor.sig_start_operation_in_progress.connect(self.spinner.start)
editor.sig_stop_operation_in_progress.connect(self.spinner.stop)
editor.modificationChanged.connect(
lambda state: self.modification_changed(
state, editor_id=id(editor)))
editor.focus_in.connect(self.focus_changed)
editor.zoom_in.connect(lambda: self.zoom_in.emit())
editor.zoom_out.connect(lambda: self.zoom_out.emit())
editor.zoom_reset.connect(lambda: self.zoom_reset.emit())
editor.sig_eol_chars_changed.connect(
lambda eol_chars: self.refresh_eol_chars(eol_chars))
self.find_widget.set_editor(editor)
self.refresh_file_dependent_actions.emit()
self.modification_changed(index=self.data.index(finfo))
# To update the outline explorer.
editor.oe_proxy = OutlineExplorerProxyEditor(editor, editor.filename)
if self.outlineexplorer is not None:
self.outlineexplorer.register_editor(editor.oe_proxy)
# Needs to reset the highlighting on startup in case the PygmentsSH
# is in use
editor.run_pygments_highlighter()
options = {
'language': editor.language,
'filename': editor.filename,
'codeeditor': editor
}
self.sig_open_file.emit(options)
if self.get_stack_index() == 0:
self.current_changed(0)
return finfo
def editor_cursor_position_changed(self, line, index):
"""Cursor position of one of the editor in the stack has changed"""
self.sig_editor_cursor_position_changed.emit(line, index)
@Slot(str, str, bool)
def send_to_help(self, name, signature, force=False):
"""qstr1: obj_text, qstr2: argpspec, qstr3: note, qstr4: doc_text"""
if not force and not self.help_enabled:
return
editor = self.get_current_editor()
language = editor.language.lower()
signature = to_text_string(signature)
signature = unicodedata.normalize("NFKD", signature)
parts = signature.split('\n\n')
definition = parts[0]
documentation = '\n\n'.join(parts[1:])
args = ''
if '(' in definition and language == 'python':
args = definition[definition.find('('):]
else:
documentation = signature
doc = {
'obj_text': '',
'name': name,
'argspec': args,
'note': '',
'docstring': documentation,
'force_refresh': force,
'path': editor.filename
}
self.sig_help_requested.emit(doc)
def new(self, filename, encoding, text, default_content=False,
empty=False):
"""
Create new filename with *encoding* and *text*
"""
finfo = self.create_new_editor(filename, encoding, text,
set_current=False, new=True)
finfo.editor.set_cursor_position('eof')
if not empty:
finfo.editor.insert_text(os.linesep)
if default_content:
finfo.default = True
finfo.editor.document().setModified(False)
return finfo
def load(self, filename, set_current=True, add_where='end',
processevents=True):
"""
Load filename, create an editor instance and return it
This also sets the hash of the loaded file in the autosave component.
*Warning* This is loading file, creating editor but not executing
the source code analysis -- the analysis must be done by the editor
plugin (in case multiple editorstack instances are handled)
"""
filename = osp.abspath(to_text_string(filename))
if processevents:
self.starting_long_process.emit(_("Loading %s...") % filename)
text, enc = encoding.read(filename)
self.autosave.file_hashes[filename] = hash(text)
finfo = self.create_new_editor(filename, enc, text, set_current,
add_where=add_where)
index = self.data.index(finfo)
if processevents:
self.ending_long_process.emit("")
if self.isVisible() and self.checkeolchars_enabled \
and sourcecode.has_mixed_eol_chars(text):
name = osp.basename(filename)
self.msgbox = QMessageBox(
QMessageBox.Warning,
self.title,
_("%s contains mixed end-of-line "
"characters.
Spyder will fix this "
"automatically.") % name,
QMessageBox.Ok,
self)
self.msgbox.exec_()
self.set_os_eol_chars(index)
self.is_analysis_done = False
self.analyze_script(index)
finfo.editor.set_sync_symbols_and_folding_timeout()
return finfo
def set_os_eol_chars(self, index=None, osname=None):
"""Sets the EOL character(s) based on the operating system.
If `osname` is None, then the default line endings for the current
operating system (`os.name` value) will be used.
`osname` can be one of:
('posix', 'nt', 'java')
"""
if osname is None:
osname = os.name
if index is None:
index = self.get_stack_index()
finfo = self.data[index]
eol_chars = sourcecode.get_eol_chars_from_os_name(osname)
logger.debug(f"Set OS eol chars {eol_chars} for file {finfo.filename}")
finfo.editor.set_eol_chars(eol_chars)
finfo.editor.document().setModified(True)
def remove_trailing_spaces(self, index=None):
"""Remove trailing spaces"""
if index is None:
index = self.get_stack_index()
finfo = self.data[index]
logger.debug(f"Remove trailing spaces for file {finfo.filename}")
finfo.editor.trim_trailing_spaces()
def trim_trailing_newlines(self, index=None):
if index is None:
index = self.get_stack_index()
finfo = self.data[index]
logger.debug(f"Trim trailing new lines for file {finfo.filename}")
finfo.editor.trim_trailing_newlines()
def add_newline_to_file(self, index=None):
if index is None:
index = self.get_stack_index()
finfo = self.data[index]
logger.debug(f"Add new line to file {finfo.filename}")
finfo.editor.add_newline_to_file()
def fix_indentation(self, index=None):
"""Replace tab characters by spaces"""
if index is None:
index = self.get_stack_index()
finfo = self.data[index]
logger.debug(f"Fix indentation for file {finfo.filename}")
finfo.editor.fix_indentation()
def format_document_or_selection(self, index=None):
if index is None:
index = self.get_stack_index()
finfo = self.data[index]
logger.debug(f"Run formatting in file {finfo.filename}")
finfo.editor.format_document_or_range()
# ------ Run
def run_selection(self):
"""
Run selected text or current line in console.
If some text is selected, then execute that text in console.
If no text is selected, then execute current line, unless current line
is empty. Then, advance cursor to next line. If cursor is on last line
and that line is not empty, then add a new blank line and move the
cursor there. If cursor is on last line and that line is empty, then do
not move cursor.
"""
text = self.get_current_editor().get_selection_as_executable_code()
if text:
self.exec_in_extconsole.emit(text.rstrip(), self.focus_to_editor)
return
editor = self.get_current_editor()
line = editor.get_current_line()
text = line.lstrip()
if text:
self.exec_in_extconsole.emit(text, self.focus_to_editor)
if editor.is_cursor_on_last_line() and text:
editor.append(editor.get_line_separator())
editor.move_cursor_to_next('line', 'down')
def run_cell(self, debug=False):
"""Run current cell."""
text, block = self.get_current_editor().get_cell_as_executable_code()
finfo = self.get_current_finfo()
editor = self.get_current_editor()
name = cell_name(block)
filename = finfo.filename
self._run_cell_text(text, editor, (filename, name), debug)
def debug_cell(self):
"""Debug current cell."""
self.run_cell(debug=True)
def run_cell_and_advance(self):
"""Run current cell and advance to the next one"""
self.run_cell()
self.advance_cell()
def advance_cell(self, reverse=False):
"""Advance to the next cell.
reverse = True --> go to previous cell.
"""
if not reverse:
move_func = self.get_current_editor().go_to_next_cell
else:
move_func = self.get_current_editor().go_to_previous_cell
if self.focus_to_editor:
move_func()
else:
term = QApplication.focusWidget()
move_func()
term.setFocus()
term = QApplication.focusWidget()
move_func()
term.setFocus()
def re_run_last_cell(self):
"""Run the previous cell again."""
if self.last_cell_call is None:
return
filename, cell_name = self.last_cell_call
index = self.has_filename(filename)
if index is None:
return
editor = self.data[index].editor
try:
text = editor.get_cell_code(cell_name)
except RuntimeError:
return
self._run_cell_text(text, editor, (filename, cell_name))
def _run_cell_text(self, text, editor, cell_id, debug=False):
"""Run cell code in the console.
Cell code is run in the console by copying it to the console if
`self.run_cell_copy` is ``True`` otherwise by using the `run_cell`
function.
Parameters
----------
text : str
The code in the cell as a string.
line : int
The starting line number of the cell in the file.
"""
(filename, cell_name) = cell_id
if editor.is_python_or_ipython():
args = (text, cell_name, filename, self.run_cell_copy)
if debug:
self.debug_cell_in_ipyclient.emit(*args)
else:
self.run_cell_in_ipyclient.emit(*args)
if self.focus_to_editor:
editor.setFocus()
else:
console = QApplication.focusWidget()
console.setFocus()
# ------ Drag and drop
def dragEnterEvent(self, event):
"""
Reimplemented Qt method.
Inform Qt about the types of data that the widget accepts.
"""
logger.debug("dragEnterEvent was received")
source = event.mimeData()
# The second check is necessary on Windows, where source.hasUrls()
# can return True but source.urls() is []
# The third check is needed since a file could be dropped from
# compressed files. In Windows mimedata2url(source) returns None
# Fixes spyder-ide/spyder#5218.
has_urls = source.hasUrls()
has_text = source.hasText()
urls = source.urls()
all_urls = mimedata2url(source)
logger.debug("Drag event source has_urls: {}".format(has_urls))
logger.debug("Drag event source urls: {}".format(urls))
logger.debug("Drag event source all_urls: {}".format(all_urls))
logger.debug("Drag event source has_text: {}".format(has_text))
if has_urls and urls and all_urls:
text = [encoding.is_text_file(url) for url in all_urls]
logger.debug("Accept proposed action?: {}".format(any(text)))
if any(text):
event.acceptProposedAction()
else:
event.ignore()
elif source.hasText():
event.acceptProposedAction()
elif os.name == 'nt':
# This covers cases like dragging from compressed files,
# which can be opened by the Editor if they are plain
# text, but doesn't come with url info.
# Fixes spyder-ide/spyder#2032.
logger.debug("Accept proposed action on Windows")
event.acceptProposedAction()
else:
logger.debug("Ignore drag event")
event.ignore()
def dropEvent(self, event):
"""
Reimplement Qt method.
Unpack dropped data and handle it.
"""
logger.debug("dropEvent was received")
source = event.mimeData()
# The second check is necessary when mimedata2url(source)
# returns None.
# Fixes spyder-ide/spyder#7742.
if source.hasUrls() and mimedata2url(source):
files = mimedata2url(source)
files = [f for f in files if encoding.is_text_file(f)]
files = set(files or [])
for fname in files:
self.plugin_load.emit(fname)
elif source.hasText():
editor = self.get_current_editor()
if editor is not None:
editor.insert_text(source.text())
else:
event.ignore()
event.acceptProposedAction()
def register_panel(self, panel_class, *args,
position=Panel.Position.LEFT, **kwargs):
"""Register a panel in all codeeditors."""
if (panel_class, args, kwargs, position) not in self.external_panels:
self.external_panels.append((panel_class, args, kwargs, position))
for finfo in self.data:
cur_panel = finfo.editor.panels.register(
panel_class(*args, **kwargs), position=position)
if not cur_panel.isVisible():
cur_panel.setVisible(True)
class EditorSplitter(QSplitter):
"""QSplitter for editor windows."""
def __init__(self, parent, plugin, menu_actions, first=False,
register_editorstack_cb=None, unregister_editorstack_cb=None):
"""Create a splitter for dividing an editor window into panels.
Adds a new EditorStack instance to this splitter. If it's not
the first splitter, clones the current EditorStack from the plugin.
Args:
parent: Parent widget.
plugin: Plugin this widget belongs to.
menu_actions: QActions to include from the parent.
first: Boolean if this is the first splitter in the editor.
register_editorstack_cb: Callback to register the EditorStack.
Defaults to plugin.register_editorstack() to
register the EditorStack with the Editor plugin.
unregister_editorstack_cb: Callback to unregister the EditorStack.
Defaults to plugin.unregister_editorstack() to
unregister the EditorStack with the Editor plugin.
"""
QSplitter.__init__(self, parent)
self.setAttribute(Qt.WA_DeleteOnClose)
self.setChildrenCollapsible(False)
self.toolbar_list = None
self.menu_list = None
self.plugin = plugin
if register_editorstack_cb is None:
register_editorstack_cb = self.plugin.register_editorstack
self.register_editorstack_cb = register_editorstack_cb
if unregister_editorstack_cb is None:
unregister_editorstack_cb = self.plugin.unregister_editorstack
self.unregister_editorstack_cb = unregister_editorstack_cb
self.menu_actions = menu_actions
self.editorstack = EditorStack(self, menu_actions)
self.register_editorstack_cb(self.editorstack)
if not first:
self.plugin.clone_editorstack(editorstack=self.editorstack)
self.editorstack.destroyed.connect(lambda: self.editorstack_closed())
self.editorstack.sig_split_vertically.connect(
lambda: self.split(orientation=Qt.Vertical))
self.editorstack.sig_split_horizontally.connect(
lambda: self.split(orientation=Qt.Horizontal))
self.addWidget(self.editorstack)
if not running_under_pytest():
self.editorstack.set_color_scheme(plugin.get_color_scheme())
self.setStyleSheet(self._stylesheet)
def closeEvent(self, event):
"""Override QWidget closeEvent().
This event handler is called with the given event when Qt
receives a window close request from a top-level widget.
"""
QSplitter.closeEvent(self, event)
def __give_focus_to_remaining_editor(self):
focus_widget = self.plugin.get_focus_widget()
if focus_widget is not None:
focus_widget.setFocus()
def editorstack_closed(self):
logger.debug("method 'editorstack_closed':")
logger.debug(" self : %r" % self)
try:
self.unregister_editorstack_cb(self.editorstack)
self.editorstack = None
close_splitter = self.count() == 1
except (RuntimeError, AttributeError):
# editorsplitter has been destroyed (happens when closing a
# EditorMainWindow instance)
return
if close_splitter:
# editorstack just closed was the last widget in this QSplitter
self.close()
return
self.__give_focus_to_remaining_editor()
def editorsplitter_closed(self):
logger.debug("method 'editorsplitter_closed':")
logger.debug(" self : %r" % self)
try:
close_splitter = self.count() == 1 and self.editorstack is None
except RuntimeError:
# editorsplitter has been destroyed (happens when closing a
# EditorMainWindow instance)
return
if close_splitter:
# editorsplitter just closed was the last widget in this QSplitter
self.close()
return
elif self.count() == 2 and self.editorstack:
# back to the initial state: a single editorstack instance,
# as a single widget in this QSplitter: orientation may be changed
self.editorstack.reset_orientation()
self.__give_focus_to_remaining_editor()
def split(self, orientation=Qt.Vertical):
"""Create and attach a new EditorSplitter to the current EditorSplitter.
The new EditorSplitter widget will contain an EditorStack that
is a clone of the current EditorStack.
A single EditorSplitter instance can be split multiple times, but the
orientation will be the same for all the direct splits. If one of
the child splits is split, then that split can have a different
orientation.
"""
self.setOrientation(orientation)
self.editorstack.set_orientation(orientation)
editorsplitter = EditorSplitter(self.parent(), self.plugin,
self.menu_actions,
register_editorstack_cb=self.register_editorstack_cb,
unregister_editorstack_cb=self.unregister_editorstack_cb)
self.addWidget(editorsplitter)
editorsplitter.destroyed.connect(self.editorsplitter_closed)
current_editor = editorsplitter.editorstack.get_current_editor()
if current_editor is not None:
current_editor.setFocus()
def iter_editorstacks(self):
"""Return the editor stacks for this splitter and every first child.
Note: If a splitter contains more than one splitter as a direct
child, only the first child's editor stack is included.
Returns:
List of tuples containing (EditorStack instance, orientation).
"""
editorstacks = [(self.widget(0), self.orientation())]
if self.count() > 1:
editorsplitter = self.widget(1)
editorstacks += editorsplitter.iter_editorstacks()
return editorstacks
def get_layout_settings(self):
"""Return the layout state for this splitter and its children.
Record the current state, including file names and current line
numbers, of the splitter panels.
Returns:
A dictionary containing keys {hexstate, sizes, splitsettings}.
hexstate: String of saveState() for self.
sizes: List for size() for self.
splitsettings: List of tuples of the form
(orientation, cfname, clines) for each EditorSplitter
and its EditorStack.
orientation: orientation() for the editor
splitter (which may be a child of self).
cfname: EditorStack current file name.
clines: Current line number for each file in the
EditorStack.
"""
splitsettings = []
for editorstack, orientation in self.iter_editorstacks():
clines = []
cfname = ''
# XXX - this overrides value from the loop to always be False?
orientation = False
if hasattr(editorstack, 'data'):
clines = [finfo.editor.get_cursor_line_number()
for finfo in editorstack.data]
cfname = editorstack.get_current_filename()
splitsettings.append((orientation == Qt.Vertical, cfname, clines))
return dict(hexstate=qbytearray_to_str(self.saveState()),
sizes=self.sizes(), splitsettings=splitsettings)
def set_layout_settings(self, settings, dont_goto=None):
"""Restore layout state for the splitter panels.
Apply the settings to restore a saved layout within the editor. If
the splitsettings key doesn't exist, then return without restoring
any settings.
The current EditorSplitter (self) calls split() for each element
in split_settings, thus recreating the splitter panels from the saved
state. split() also clones the editorstack, which is then
iterated over to restore the saved line numbers on each file.
The size and positioning of each splitter panel is restored from
hexstate.
Args:
settings: A dictionary with keys {hexstate, sizes, orientation}
that define the layout for the EditorSplitter panels.
dont_goto: Defaults to None, which positions the cursor to the
end of the editor. If there's a value, positions the
cursor on the saved line number for each editor.
"""
splitsettings = settings.get('splitsettings')
if splitsettings is None:
return
splitter = self
editor = None
for i, (is_vertical, cfname, clines) in enumerate(splitsettings):
if i > 0:
splitter.split(Qt.Vertical if is_vertical else Qt.Horizontal)
splitter = splitter.widget(1)
editorstack = splitter.widget(0)
for j, finfo in enumerate(editorstack.data):
editor = finfo.editor
# TODO: go_to_line is not working properly (the line it jumps
# to is not the corresponding to that file). This will be fixed
# in a future PR (which will fix spyder-ide/spyder#3857).
if dont_goto is not None:
# Skip go to line for first file because is already there.
pass
else:
try:
editor.go_to_line(clines[j])
except IndexError:
pass
hexstate = settings.get('hexstate')
if hexstate is not None:
self.restoreState( QByteArray().fromHex(
str(hexstate).encode('utf-8')) )
sizes = settings.get('sizes')
if sizes is not None:
self.setSizes(sizes)
if editor is not None:
editor.clearFocus()
editor.setFocus()
@property
def _stylesheet(self):
css = qstylizer.style.StyleSheet()
css.QSplitter.setValues(
background=QStylePalette.COLOR_BACKGROUND_1
)
return css.toString()
class EditorWidget(QSplitter):
CONF_SECTION = 'editor'
def __init__(self, parent, plugin, menu_actions):
QSplitter.__init__(self, parent)
self.setAttribute(Qt.WA_DeleteOnClose)
statusbar = parent.statusBar() # Create a status bar
self.vcs_status = VCSStatus(self)
self.cursorpos_status = CursorPositionStatus(self)
self.encoding_status = EncodingStatus(self)
self.eol_status = EOLStatus(self)
self.readwrite_status = ReadWriteStatus(self)
statusbar.insertPermanentWidget(0, self.readwrite_status)
statusbar.insertPermanentWidget(0, self.eol_status)
statusbar.insertPermanentWidget(0, self.encoding_status)
statusbar.insertPermanentWidget(0, self.cursorpos_status)
statusbar.insertPermanentWidget(0, self.vcs_status)
self.editorstacks = []
self.plugin = plugin
self.find_widget = FindReplace(self, enable_replace=True)
self.plugin.register_widget_shortcuts(self.find_widget)
self.find_widget.hide()
# TODO: Check this initialization once the editor is migrated to the
# new API
self.outlineexplorer = OutlineExplorerWidget(
'outline_explorer',
plugin,
self,
context=f'editor_window_{str(id(self))}'
)
self.outlineexplorer.edit_goto.connect(
lambda filenames, goto, word:
plugin.load(filenames=filenames, goto=goto, word=word,
editorwindow=self.parent()))
editor_widgets = QWidget(self)
editor_layout = QVBoxLayout()
editor_layout.setContentsMargins(0, 0, 0, 0)
editor_widgets.setLayout(editor_layout)
editorsplitter = EditorSplitter(self, plugin, menu_actions,
register_editorstack_cb=self.register_editorstack,
unregister_editorstack_cb=self.unregister_editorstack)
self.editorsplitter = editorsplitter
editor_layout.addWidget(editorsplitter)
editor_layout.addWidget(self.find_widget)
splitter = QSplitter(self)
splitter.setContentsMargins(0, 0, 0, 0)
splitter.addWidget(editor_widgets)
splitter.addWidget(self.outlineexplorer)
splitter.setStretchFactor(0, 5)
splitter.setStretchFactor(1, 1)
def register_editorstack(self, editorstack):
self.editorstacks.append(editorstack)
logger.debug("EditorWidget.register_editorstack: %r" % editorstack)
self.__print_editorstacks()
self.plugin.last_focused_editorstack[self.parent()] = editorstack
editorstack.set_closable(len(self.editorstacks) > 1)
editorstack.set_outlineexplorer(self.outlineexplorer)
editorstack.set_find_widget(self.find_widget)
editorstack.reset_statusbar.connect(self.readwrite_status.hide)
editorstack.reset_statusbar.connect(self.encoding_status.hide)
editorstack.reset_statusbar.connect(self.cursorpos_status.hide)
editorstack.readonly_changed.connect(
self.readwrite_status.update_readonly)
editorstack.encoding_changed.connect(
self.encoding_status.update_encoding)
editorstack.sig_editor_cursor_position_changed.connect(
self.cursorpos_status.update_cursor_position)
editorstack.sig_refresh_eol_chars.connect(self.eol_status.update_eol)
self.plugin.register_editorstack(editorstack)
def __print_editorstacks(self):
logger.debug("%d editorstack(s) in editorwidget:" %
len(self.editorstacks))
for edst in self.editorstacks:
logger.debug(" %r" % edst)
def unregister_editorstack(self, editorstack):
logger.debug("EditorWidget.unregister_editorstack: %r" % editorstack)
self.plugin.unregister_editorstack(editorstack)
self.editorstacks.pop(self.editorstacks.index(editorstack))
self.__print_editorstacks()
class EditorMainWindow(QMainWindow):
def __init__(self, plugin, menu_actions, toolbar_list, menu_list):
QMainWindow.__init__(self)
self.setAttribute(Qt.WA_DeleteOnClose)
self.plugin = plugin
self.window_size = None
self.editorwidget = EditorWidget(self, plugin, menu_actions)
self.setCentralWidget(self.editorwidget)
# Setting interface theme
self.setStyleSheet(str(APP_STYLESHEET))
# Give focus to current editor to update/show all status bar widgets
editorstack = self.editorwidget.editorsplitter.editorstack
editor = editorstack.get_current_editor()
if editor is not None:
editor.setFocus()
self.setWindowTitle("Spyder - %s" % plugin.windowTitle())
self.setWindowIcon(plugin.windowIcon())
if toolbar_list:
self.toolbars = []
for title, object_name, actions in toolbar_list:
toolbar = self.addToolBar(title)
toolbar.setObjectName(object_name)
toolbar.setStyleSheet(str(APP_TOOLBAR_STYLESHEET))
toolbar.setMovable(False)
add_actions(toolbar, actions)
self.toolbars.append(toolbar)
if menu_list:
quit_action = create_action(self, _("Close window"),
icon=ima.icon("close_pane"),
tip=_("Close this window"),
triggered=self.close)
self.menus = []
for index, (title, actions) in enumerate(menu_list):
menu = self.menuBar().addMenu(title)
if index == 0:
# File menu
add_actions(menu, actions+[None, quit_action])
else:
add_actions(menu, actions)
self.menus.append(menu)
def get_toolbars(self):
"""Get the toolbars."""
return self.toolbars
def add_toolbars_to_menu(self, menu_title, actions):
"""Add toolbars to a menu."""
# Six is the position of the view menu in menus list
# that you can find in plugins/editor.py setup_other_windows.
view_menu = self.menus[6]
view_menu.setObjectName('checkbox-padding')
if actions == self.toolbars and view_menu:
toolbars = []
for toolbar in self.toolbars:
action = toolbar.toggleViewAction()
toolbars.append(action)
add_actions(view_menu, toolbars)
def load_toolbars(self):
"""Loads the last visible toolbars from the .ini file."""
toolbars_names = CONF.get('main', 'last_visible_toolbars', default=[])
if toolbars_names:
dic = {}
for toolbar in self.toolbars:
dic[toolbar.objectName()] = toolbar
toolbar.toggleViewAction().setChecked(False)
toolbar.setVisible(False)
for name in toolbars_names:
if name in dic:
dic[name].toggleViewAction().setChecked(True)
dic[name].setVisible(True)
def resizeEvent(self, event):
"""Reimplement Qt method"""
if not self.isMaximized() and not self.isFullScreen():
self.window_size = self.size()
QMainWindow.resizeEvent(self, event)
def closeEvent(self, event):
"""Reimplement Qt method"""
if self.plugin._undocked_window is not None:
self.plugin.dockwidget.setWidget(self.plugin)
self.plugin.dockwidget.setVisible(True)
self.plugin.switch_to_plugin()
QMainWindow.closeEvent(self, event)
if self.plugin._undocked_window is not None:
self.plugin._undocked_window = None
def get_layout_settings(self):
"""Return layout state"""
splitsettings = self.editorwidget.editorsplitter.get_layout_settings()
return dict(size=(self.window_size.width(), self.window_size.height()),
pos=(self.pos().x(), self.pos().y()),
is_maximized=self.isMaximized(),
is_fullscreen=self.isFullScreen(),
hexstate=qbytearray_to_str(self.saveState()),
splitsettings=splitsettings)
def set_layout_settings(self, settings):
"""Restore layout state"""
size = settings.get('size')
if size is not None:
self.resize( QSize(*size) )
self.window_size = self.size()
pos = settings.get('pos')
if pos is not None:
self.move( QPoint(*pos) )
hexstate = settings.get('hexstate')
if hexstate is not None:
self.restoreState( QByteArray().fromHex(
str(hexstate).encode('utf-8')) )
if settings.get('is_maximized'):
self.setWindowState(Qt.WindowMaximized)
if settings.get('is_fullscreen'):
self.setWindowState(Qt.WindowFullScreen)
splitsettings = settings.get('splitsettings')
if splitsettings is not None:
self.editorwidget.editorsplitter.set_layout_settings(splitsettings)
class EditorPluginExample(QSplitter):
def __init__(self):
QSplitter.__init__(self)
self._dock_action = None
self._undock_action = None
self._close_plugin_action = None
self._undocked_window = None
menu_actions = []
self.editorstacks = []
self.editorwindows = []
self.last_focused_editorstack = {} # fake
self.find_widget = FindReplace(self, enable_replace=True)
self.outlineexplorer = OutlineExplorerWidget(None, self, self)
self.outlineexplorer.edit_goto.connect(self.go_to_file)
self.editor_splitter = EditorSplitter(self, self, menu_actions,
first=True)
editor_widgets = QWidget(self)
editor_layout = QVBoxLayout()
editor_layout.setContentsMargins(0, 0, 0, 0)
editor_widgets.setLayout(editor_layout)
editor_layout.addWidget(self.editor_splitter)
editor_layout.addWidget(self.find_widget)
self.setContentsMargins(0, 0, 0, 0)
self.addWidget(editor_widgets)
self.addWidget(self.outlineexplorer)
self.setStretchFactor(0, 5)
self.setStretchFactor(1, 1)
self.menu_actions = menu_actions
self.toolbar_list = None
self.menu_list = None
self.setup_window([], [])
def go_to_file(self, fname, lineno, text='', start_column=None):
editorstack = self.editorstacks[0]
editorstack.set_current_filename(to_text_string(fname))
editor = editorstack.get_current_editor()
editor.go_to_line(lineno, word=text, start_column=start_column)
def closeEvent(self, event):
for win in self.editorwindows[:]:
win.close()
logger.debug("%d: %r" % (len(self.editorwindows), self.editorwindows))
logger.debug("%d: %r" % (len(self.editorstacks), self.editorstacks))
event.accept()
def load(self, fname):
QApplication.processEvents()
editorstack = self.editorstacks[0]
editorstack.load(fname)
editorstack.analyze_script()
def register_editorstack(self, editorstack):
logger.debug("FakePlugin.register_editorstack: %r" % editorstack)
self.editorstacks.append(editorstack)
if self.isAncestorOf(editorstack):
# editorstack is a child of the Editor plugin
editorstack.set_closable(len(self.editorstacks) > 1)
editorstack.set_outlineexplorer(self.outlineexplorer)
editorstack.set_find_widget(self.find_widget)
oe_btn = create_toolbutton(self)
editorstack.add_corner_widgets_to_tabbar([5, oe_btn])
action = QAction(self)
editorstack.set_io_actions(action, action, action, action)
font = QFont("Courier New")
font.setPointSize(10)
editorstack.set_default_font(font, color_scheme='Spyder')
editorstack.sig_close_file.connect(self.close_file_in_all_editorstacks)
editorstack.file_saved.connect(self.file_saved_in_editorstack)
editorstack.file_renamed_in_data.connect(
self.file_renamed_in_data_in_editorstack)
editorstack.plugin_load.connect(self.load)
def unregister_editorstack(self, editorstack):
logger.debug("FakePlugin.unregister_editorstack: %r" % editorstack)
self.editorstacks.pop(self.editorstacks.index(editorstack))
def clone_editorstack(self, editorstack):
editorstack.clone_from(self.editorstacks[0])
def setup_window(self, toolbar_list, menu_list):
self.toolbar_list = toolbar_list
self.menu_list = menu_list
def create_new_window(self):
window = EditorMainWindow(self, self.menu_actions,
self.toolbar_list, self.menu_list,
show_fullpath=False, show_all_files=False,
group_cells=True, show_comments=True,
sort_files_alphabetically=False)
window.resize(self.size())
window.show()
self.register_editorwindow(window)
window.destroyed.connect(lambda: self.unregister_editorwindow(window))
def register_editorwindow(self, window):
logger.debug("register_editorwindowQObject*: %r" % window)
self.editorwindows.append(window)
def unregister_editorwindow(self, window):
logger.debug("unregister_editorwindow: %r" % window)
self.editorwindows.pop(self.editorwindows.index(window))
def get_focus_widget(self):
pass
@Slot(str, str)
def close_file_in_all_editorstacks(self, editorstack_id_str, filename):
for editorstack in self.editorstacks:
if str(id(editorstack)) != editorstack_id_str:
editorstack.blockSignals(True)
index = editorstack.get_index_from_filename(filename)
editorstack.close_file(index, force=True)
editorstack.blockSignals(False)
# This method is never called in this plugin example. It's here only
# to show how to use the file_saved signal (see above).
@Slot(str, str, str)
def file_saved_in_editorstack(self, editorstack_id_str,
original_filename, filename):
"""A file was saved in editorstack, this notifies others"""
for editorstack in self.editorstacks:
if str(id(editorstack)) != editorstack_id_str:
editorstack.file_saved_in_other_editorstack(original_filename,
filename)
# This method is never called in this plugin example. It's here only
# to show how to use the file_saved signal (see above).
@Slot(str, str, str)
def file_renamed_in_data_in_editorstack(self, editorstack_id_str,
original_filename, filename):
"""A file was renamed in data in editorstack, this notifies others"""
for editorstack in self.editorstacks:
if str(id(editorstack)) != editorstack_id_str:
editorstack.rename_in_data(original_filename, filename)
def register_widget_shortcuts(self, widget):
"""Fake!"""
pass
def get_color_scheme(self):
pass
def test():
from spyder.utils.qthelpers import qapplication
from spyder.config.base import get_module_path
spyder_dir = get_module_path('spyder')
app = qapplication(test_time=8)
test = EditorPluginExample()
test.resize(900, 700)
test.show()
import time
t0 = time.time()
test.load(osp.join(spyder_dir, "widgets", "collectionseditor.py"))
test.load(osp.join(spyder_dir, "plugins", "editor", "widgets",
"editor.py"))
test.load(osp.join(spyder_dir, "plugins", "explorer", "widgets",
'explorer.py'))
test.load(osp.join(spyder_dir, "plugins", "editor", "widgets",
"codeeditor.py"))
print("Elapsed time: %.3f s" % (time.time()-t0)) # spyder: test-skip
sys.exit(app.exec_())
if __name__ == "__main__":
test()