Editor triggers update on every save.

For any command that spawns an editor, tirgger an update on save, not
just on exit.

- :open-editor writes the text field on save
- :edit-url navigates on save
- :edit-url -t opens a new tab on each save
- :edit-command updates the statusbar text on save
- :edit-command --run runs a command on each save
- :config-edit reloads the config on save

Resolves #2307.
Helps mitigate #1596 by allowing users to 'save' partial work, and
notice if there was an error without closing the editor.
This commit is contained in:
Ryan Roden-Corrent 2017-12-18 14:42:05 -05:00
parent cdaf3ac097
commit 8a9b98c2dc
3 changed files with 58 additions and 22 deletions

View File

@ -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)

View File

@ -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

View File

@ -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)