302 lines
9.1 KiB
Python
302 lines
9.1 KiB
Python
import os
|
|
from modulefinder import ModuleFinder
|
|
|
|
from spyder.plugins.editor.widgets.codeeditor import CodeEditor
|
|
from PySide6.QtCore import Signal as pyqtSignal
|
|
from PySide6.QtCore import QFileSystemWatcher, QTimerEvent, QTimer
|
|
|
|
from PySide6.QtWidgets import QFileDialog
|
|
from PySide6.QtGui import QFontDatabase, QAction
|
|
from path import Path
|
|
|
|
import sys
|
|
|
|
from pyqtgraph.parametertree import Parameter
|
|
|
|
from ..mixins import ComponentMixin
|
|
from ..utils import get_save_filename, get_open_filename, confirm
|
|
|
|
from ..icons import icon
|
|
|
|
|
|
class Editor(CodeEditor, ComponentMixin):
|
|
name = "Code Editor"
|
|
|
|
# This signal is emitted whenever the currently-open file changes and
|
|
# autoreload is enabled.
|
|
triggerRerender = pyqtSignal(bool)
|
|
sigFilenameChanged = pyqtSignal(str)
|
|
|
|
preferences = Parameter.create(
|
|
name="Preferences",
|
|
children=[
|
|
{"name": "Font size", "type": "int", "value": 12},
|
|
{"name": "Autoreload", "type": "bool", "value": False},
|
|
{"name": "Autoreload delay", "type": "int", "value": 50},
|
|
{
|
|
"name": "Autoreload: watch imported modules",
|
|
"type": "bool",
|
|
"value": False,
|
|
},
|
|
{"name": "Line wrap", "type": "bool", "value": False},
|
|
{
|
|
"name": "Color scheme",
|
|
"type": "list",
|
|
"values": ["Spyder", "Monokai", "Zenburn"],
|
|
"value": "Spyder",
|
|
},
|
|
],
|
|
)
|
|
|
|
EXTENSIONS = "py"
|
|
|
|
def __init__(self, parent=None):
|
|
self._watched_file = None
|
|
|
|
super(Editor, self).__init__(parent)
|
|
ComponentMixin.__init__(self)
|
|
|
|
self.setup_editor(
|
|
linenumbers=True,
|
|
markers=True,
|
|
edge_line=False,
|
|
tab_mode=False,
|
|
show_blanks=True,
|
|
font=QFontDatabase.systemFont(QFontDatabase.FixedFont),
|
|
language="Python",
|
|
filename="",
|
|
)
|
|
|
|
self._actions = {
|
|
"File": [
|
|
QAction(
|
|
icon("new"), "New", self, shortcut="ctrl+N", triggered=self.new
|
|
),
|
|
QAction(
|
|
icon("open"), "Open", self, shortcut="ctrl+O", triggered=self.open
|
|
),
|
|
QAction(
|
|
icon("save"), "Save", self, shortcut="ctrl+S", triggered=self.save
|
|
),
|
|
QAction(
|
|
icon("save_as"),
|
|
"Save as",
|
|
self,
|
|
shortcut="ctrl+shift+S",
|
|
triggered=self.save_as,
|
|
),
|
|
QAction(
|
|
icon("autoreload"),
|
|
"Automatic reload and preview",
|
|
self,
|
|
triggered=self.autoreload,
|
|
checkable=True,
|
|
checked=False,
|
|
objectName="autoreload",
|
|
),
|
|
]
|
|
}
|
|
|
|
for a in self._actions.values():
|
|
self.addActions(a)
|
|
|
|
self._fixContextMenu()
|
|
|
|
# autoreload support
|
|
self._file_watcher = QFileSystemWatcher(self)
|
|
# we wait for 50ms after a file change for the file to be written completely
|
|
self._file_watch_timer = QTimer(self)
|
|
self._file_watch_timer.setInterval(self.preferences["Autoreload delay"])
|
|
self._file_watch_timer.setSingleShot(True)
|
|
self._file_watcher.fileChanged.connect(
|
|
lambda val: self._file_watch_timer.start()
|
|
)
|
|
self._file_watch_timer.timeout.connect(self._file_changed)
|
|
|
|
self.updatePreferences()
|
|
|
|
def _fixContextMenu(self):
|
|
menu = self.menu
|
|
|
|
menu.removeAction(self.run_cell_action)
|
|
menu.removeAction(self.run_cell_and_advance_action)
|
|
menu.removeAction(self.run_selection_action)
|
|
menu.removeAction(self.re_run_last_cell_action)
|
|
|
|
def updatePreferences(self, *args):
|
|
self.set_color_scheme(self.preferences["Color scheme"])
|
|
|
|
font = self.font()
|
|
font.setPointSize(self.preferences["Font size"])
|
|
self.set_font(font)
|
|
|
|
self.findChild(QAction, "autoreload").setChecked(self.preferences["Autoreload"])
|
|
|
|
self._file_watch_timer.setInterval(self.preferences["Autoreload delay"])
|
|
|
|
self.toggle_wrap_mode(self.preferences["Line wrap"])
|
|
|
|
self._clear_watched_paths()
|
|
self._watch_paths()
|
|
|
|
def confirm_discard(self):
|
|
if self.modified:
|
|
rv = confirm(
|
|
self,
|
|
"Please confirm",
|
|
"Current document is not saved - do you want to continue?",
|
|
)
|
|
else:
|
|
rv = True
|
|
|
|
return rv
|
|
|
|
def new(self):
|
|
if not self.confirm_discard():
|
|
return
|
|
|
|
self.set_text("")
|
|
self.filename = ""
|
|
self.reset_modified()
|
|
|
|
def open(self):
|
|
if not self.confirm_discard():
|
|
return
|
|
|
|
curr_dir = Path(self.filename).abspath().dirname()
|
|
fname = get_open_filename(self.EXTENSIONS, curr_dir)
|
|
if fname != "":
|
|
self.load_from_file(fname)
|
|
|
|
def load_from_file(self, fname):
|
|
self.set_text_from_file(fname)
|
|
self.filename = fname
|
|
self.reset_modified()
|
|
|
|
def save(self):
|
|
if self._filename != "":
|
|
if self.preferences["Autoreload"]:
|
|
self._file_watcher.blockSignals(True)
|
|
self._file_watch_timer.stop()
|
|
|
|
with open(self._filename, "w") as f:
|
|
f.write(self.toPlainText())
|
|
|
|
if self.preferences["Autoreload"]:
|
|
self._file_watcher.blockSignals(False)
|
|
self.triggerRerender.emit(True)
|
|
|
|
self.reset_modified()
|
|
|
|
else:
|
|
self.save_as()
|
|
|
|
def save_as(self):
|
|
fname = get_save_filename(self.EXTENSIONS)
|
|
if fname != "":
|
|
with open(fname, "w") as f:
|
|
f.write(self.toPlainText())
|
|
self.filename = fname
|
|
|
|
self.reset_modified()
|
|
|
|
def _update_filewatcher(self):
|
|
if self._watched_file and (
|
|
self._watched_file != self.filename or not self.preferences["Autoreload"]
|
|
):
|
|
self._clear_watched_paths()
|
|
self._watched_file = None
|
|
if (
|
|
self.preferences["Autoreload"]
|
|
and self.filename
|
|
and self.filename != self._watched_file
|
|
):
|
|
self._watched_file = self._filename
|
|
self._watch_paths()
|
|
|
|
@property
|
|
def filename(self):
|
|
return self._filename
|
|
|
|
@filename.setter
|
|
def filename(self, fname):
|
|
self._filename = fname
|
|
self._update_filewatcher()
|
|
self.sigFilenameChanged.emit(fname)
|
|
|
|
def _clear_watched_paths(self):
|
|
paths = self._file_watcher.files()
|
|
if paths:
|
|
self._file_watcher.removePaths(paths)
|
|
|
|
def _watch_paths(self):
|
|
if Path(self._filename).exists():
|
|
self._file_watcher.addPath(self._filename)
|
|
if self.preferences["Autoreload: watch imported modules"]:
|
|
module_paths = self.get_imported_module_paths(self._filename)
|
|
if module_paths:
|
|
self._file_watcher.addPaths(module_paths)
|
|
|
|
# callback triggered by QFileSystemWatcher
|
|
def _file_changed(self):
|
|
# neovim writes a file by removing it first so must re-add each time
|
|
self._watch_paths()
|
|
self.set_text_from_file(self._filename)
|
|
self.triggerRerender.emit(True)
|
|
|
|
# Turn autoreload on/off.
|
|
def autoreload(self, enabled):
|
|
self.preferences["Autoreload"] = enabled
|
|
self._update_filewatcher()
|
|
|
|
def reset_modified(self):
|
|
self.document().setModified(False)
|
|
|
|
@property
|
|
def modified(self):
|
|
return self.document().isModified()
|
|
|
|
def saveComponentState(self, store):
|
|
if self.filename != "":
|
|
store.setValue(self.name + "/state", self.filename)
|
|
|
|
def restoreComponentState(self, store):
|
|
filename = store.value(self.name + "/state")
|
|
|
|
if filename and self.filename == "":
|
|
try:
|
|
self.load_from_file(filename)
|
|
except IOError:
|
|
self._logger.warning(f"could not open {filename}")
|
|
|
|
def get_imported_module_paths(self, module_path):
|
|
finder = ModuleFinder([os.path.dirname(module_path)])
|
|
imported_modules = []
|
|
|
|
try:
|
|
finder.run_script(module_path)
|
|
except SyntaxError as err:
|
|
self._logger.warning(f"Syntax error in {module_path}: {err}")
|
|
except Exception as err:
|
|
self._logger.warning(
|
|
f"Cannot determine imported modules in {module_path}: {type(err).__name__} {err}"
|
|
)
|
|
else:
|
|
for module_name, module in finder.modules.items():
|
|
if module_name != "__main__":
|
|
path = getattr(module, "__file__", None)
|
|
if path is not None and os.path.isfile(path):
|
|
imported_modules.append(path)
|
|
|
|
return imported_modules
|
|
|
|
|
|
if __name__ == "__main__":
|
|
from PySide6.QtWidgets import QApplication
|
|
|
|
app = QApplication(sys.argv)
|
|
editor = Editor()
|
|
editor.show()
|
|
|
|
sys.exit(app.exec_())
|