diff --git a/qutebrowser/misc/editor.py b/qutebrowser/misc/editor.py index 553c700e7..62d4a2d6f 100644 --- a/qutebrowser/misc/editor.py +++ b/qutebrowser/misc/editor.py @@ -22,7 +22,8 @@ import os import tempfile -from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QProcess +from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QProcess, + QFileSystemWatcher) from qutebrowser.config import config from qutebrowser.utils import message, log @@ -39,6 +40,7 @@ class ExternalEditor(QObject): _remove_file: Whether the file should be removed when the editor is closed. _proc: The GUIProcess of the editor. + _watcher: A QFileSystemWatcher to watch the edited file for changes. """ editing_finished = pyqtSignal(str) @@ -48,10 +50,12 @@ class ExternalEditor(QObject): self._filename = None self._proc = None self._remove_file = None + self._watcher = QFileSystemWatcher(parent=self) def _cleanup(self): """Clean up temporary files after the editor closed.""" assert self._remove_file is not None + self._watcher.fileChanged.disconnect() if self._filename is None or not self._remove_file: # Could not create initial file. return @@ -75,22 +79,7 @@ class ExternalEditor(QObject): # No error/cleanup here, since we already handle this in # on_proc_error. return - try: - if exitcode != 0: - return - encoding = config.val.editor.encoding - try: - with open(self._filename, 'r', encoding=encoding) as f: - text = f.read() - except OSError as e: - # NOTE: Do not replace this with "raise CommandError" as it's - # executed async. - message.error("Failed to read back edited file: {}".format(e)) - return - log.procs.debug("Read back: {}".format(text)) - self.editing_finished.emit(text) - finally: - self._cleanup() + self._cleanup() @pyqtSlot(QProcess.ProcessError) def on_proc_error(self, _err): @@ -128,6 +117,19 @@ class ExternalEditor(QObject): line, column = self._calc_line_and_column(text, caret_position) self._start_editor(line=line, column=column) + def _on_file_changed(self, path): + encoding = config.val.editor.encoding + try: + with open(path, 'r', encoding=encoding) as f: + text = f.read() + except OSError as e: + # NOTE: Do not replace this with "raise CommandError" as it's + # executed async. + message.error("Failed to read back edited file: {}".format(e)) + return + log.procs.debug("Read back: {}".format(text)) + self.editing_finished.emit(text) + def edit_file(self, filename): """Edit the file with the given filename.""" self._filename = filename @@ -147,6 +149,9 @@ class ExternalEditor(QObject): editor = config.val.editor.command executable = editor[0] + self._watcher.addPath(self._filename) + self._watcher.fileChanged.connect(self._on_file_changed) + args = [self._sub_placeholder(arg, line, column) for arg in editor[1:]] log.procs.debug("Calling \"{}\" with args {}".format(executable, args)) self._proc.start(executable, args) diff --git a/tests/end2end/features/editor.feature b/tests/end2end/features/editor.feature index d01e3d229..489fc7200 100644 --- a/tests/end2end/features/editor.feature +++ b/tests/end2end/features/editor.feature @@ -119,7 +119,7 @@ Feature: Opening external editors # There's no guarantee that the tab gets deleted... @posix @flaky Scenario: Spawning an editor and closing the tab - When I set up a fake editor that waits + When I set up a fake editor that writes "foobar" on save And I open data/editor.html And I run :click-element id qute-textarea And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log @@ -129,6 +129,18 @@ Feature: Opening external editors And I kill the waiting editor Then the error "Edited element vanished" should be shown + # Could not get signals working on Windows + @posix + Scenario: Spawning an editor and saving + When I set up a fake editor that writes "foobar" on save + And I open data/editor.html + And I run :click-element id qute-textarea + And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log + And I run :open-editor + And I save without exiting the editor + And I run :click-element id qute-button + Then the javascript message "text: foobar" should be logged + Scenario: Spawning an editor in caret mode When I set up a fake editor returning "foobar" And I open data/editor.html diff --git a/tests/end2end/features/test_editor_bdd.py b/tests/end2end/features/test_editor_bdd.py index 260197a46..18551271b 100644 --- a/tests/end2end/features/test_editor_bdd.py +++ b/tests/end2end/features/test_editor_bdd.py @@ -70,8 +70,9 @@ def set_up_editor_empty(quteproc, tmpdir): set_up_editor(quteproc, tmpdir, "") -@bdd.when(bdd.parsers.parse('I set up a fake editor that waits')) -def set_up_editor_wait(quteproc, tmpdir): +@bdd.when(bdd.parsers.parse('I set up a fake editor that writes "{text}" on ' + 'save')) +def set_up_editor_wait(quteproc, tmpdir, text): """Set up editor.command to a small python script inserting a text.""" assert not utils.is_windows pidfile = tmpdir / 'editor_pid' @@ -82,12 +83,19 @@ def set_up_editor_wait(quteproc, tmpdir): import time import signal + def handle(sig, frame): + with open(sys.argv[1], 'w', encoding='utf-8') as f: + f.write({text!r}) + if sig == signal.SIGUSR1: + sys.exit(0) + with open(r'{pidfile}', 'w') as f: f.write(str(os.getpid())) - signal.signal(signal.SIGUSR1, lambda s, f: sys.exit(0)) + signal.signal(signal.SIGUSR1, handle) + signal.signal(signal.SIGUSR2, handle) time.sleep(100) - """.format(pidfile=pidfile))) + """.format(pidfile=pidfile, text=text))) editor = json.dumps([sys.executable, str(script), '{}']) quteproc.set_setting('editor.command', editor) @@ -101,3 +109,14 @@ def kill_editor_wait(tmpdir): # for posix, there IS a member so we need to ignore useless-suppression # pylint: disable=no-member,useless-suppression os.kill(pid, signal.SIGUSR1) + + +@bdd.when(bdd.parsers.parse('I save without exiting the editor')) +def save_editor_wait(tmpdir): + """Trigger the waiting editor to write without exiting.""" + pidfile = tmpdir / 'editor_pid' + pid = int(pidfile.read()) + # windows has no SIGUSR2, but we don't run this on windows anyways + # for posix, there IS a member so we need to ignore useless-suppression + # pylint: disable=no-member,useless-suppression + os.kill(pid, signal.SIGUSR2)