Repository: spyder-ide/spyder

Issue:
File not saved when calling runfile from console
## Description

### What steps will reproduce the problem?

<!--- You can use Markdown here --->

When pressing the green "Run file" arrow, the file is automatically saved. It would be cool if the same appened when I type `runfile('file.py')` in the console.

## Versions

* Spyder version: 4.0.0.dev0 f1daab7a4
* Python version: 3.7.3
* Qt version: 5.9.1
* PyQt5 version: 5.9
* Operating System: Darwin 18.5.0

### Dependencies

```
pygments >=2.0     :  2.3.1 (OK)
qdarkstyle >=2.6.4 :  2.6.5 (OK)
sphinx >=0.6.6     :  1.8.5 (OK)
psutil >=0.3       :  5.6.1 (OK)
pyls >=0.19.0;<0.25:  0.25.0 (NOK)
nbconvert >=4.0    :  5.4.1 (OK)
pandas >=0.13.1    :  0.24.2 (OK)
numpy >=1.7        :  1.16.2 (OK)
sympy >=0.7.3      :  1.3 (OK)
cython >=0.21      :  0.29.6 (OK)
qtconsole >=4.2.0  :  4.5.0.dev0 (OK)
IPython >=4.0      :  7.3.0 (OK)
matplotlib >=2.0.0 :  3.0.3 (OK)
pylint >=0.25      :  2.3.1 (OK)
```



Discussion:
We really need to add Comms to qtconsole to expand the things we can do between the Spyder interface and the kernel.

Trying to do something else is brittle and hacky.

Resolving patch:
diff --git a/spyder/plugins/ipythonconsole/comms/kernelcomm.py b/spyder/plugins/ipythonconsole/comms/kernelcomm.py
--- a/spyder/plugins/ipythonconsole/comms/kernelcomm.py
+++ b/spyder/plugins/ipythonconsole/comms/kernelcomm.py
@@ -37,6 +37,8 @@ def __init__(self):
 
         self.register_call_handler(
             'set_debug_state', self._handle_debug_state)
+        self.register_call_handler(
+            '_async_error', self._async_error)
 
     def set_kernel_client(self, kernel_client):
         """Register new kernel client and open comm."""
diff --git a/spyder/plugins/ipythonconsole/widgets/client.py b/spyder/plugins/ipythonconsole/widgets/client.py
--- a/spyder/plugins/ipythonconsole/widgets/client.py
+++ b/spyder/plugins/ipythonconsole/widgets/client.py
@@ -116,6 +116,7 @@ def __init__(self, plugin, id_,
         SaveHistoryMixin.__init__(self, history_filename)
 
         # --- Init attrs
+        self.plugin = plugin
         self.id_ = id_
         self.connection_file = connection_file
         self.hostname = hostname
diff --git a/spyder/plugins/ipythonconsole/widgets/shell.py b/spyder/plugins/ipythonconsole/widgets/shell.py
--- a/spyder/plugins/ipythonconsole/widgets/shell.py
+++ b/spyder/plugins/ipythonconsole/widgets/shell.py
@@ -98,6 +98,7 @@ def __init__(self, ipyclient, additional_options, interpreter_versions,
             'pdb_state': self.set_pdb_state,
             'pdb_continue': self.pdb_continue,
             'get_breakpoints': self.get_spyder_breakpoints,
+            'save_files': self.handle_save_files,
         }
 
         for request_id in handlers:
@@ -452,6 +453,20 @@ def change_mpl_backend(self, command):
             if not 'inline' in command:
                 self.silent_execute(command)
 
+    # ---- Spyder-kernels methods -------------------------------------------
+    def get_editorstack(self):
+        """Get the current editorstack."""
+        plugin = self.ipyclient.plugin
+        if plugin.main.editor is not None:
+            editor = plugin.main.editor
+            return editor.get_current_editorstack()
+
+    def handle_save_files(self):
+        """Save the open files."""
+        editorstack = self.get_editorstack()
+        if editorstack is not None:
+            editorstack.save()
+
     # ---- Private methods (overrode by us) ---------------------------------
 
     def _handle_error(self, msg):


Test patch:
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -54,6 +54,7 @@
 from spyder.py3compat import PY2, to_text_string
 from spyder.utils.programs import is_module_installed
 from spyder.widgets.dock import DockTitleBar
+from spyder.utils.misc import remove_backslashes
 
 # For testing various Spyder urls
 if not PY2:
@@ -1821,6 +1822,31 @@ def test_custom_layouts(main_window, qtbot):
                                 print(widget)  # spyder: test-skip
                                 assert widget.isVisible()
 
+# @pytest.mark.slow
+@flaky(max_runs=3)
+def test_save_on_runfile(main_window, qtbot):
+    """Test that layout are showing the expected widgets visible."""
+    # Load test file
+    test_file = osp.join(LOCATION, 'script.py')
+    test_file_copy = test_file[:-3] + '_copy.py'
+    shutil.copyfile(test_file, test_file_copy)
+    main_window.editor.load(test_file_copy)
+    code_editor = main_window.editor.get_focus_widget()
+
+    # Verify result
+    shell = main_window.ipyconsole.get_current_shellwidget()
+    qtbot.waitUntil(lambda: shell._prompt_html is not None,
+                    timeout=SHELL_TIMEOUT)
+
+    qtbot.keyClicks(code_editor, 'test_var = 123', delay=100)
+    filename = code_editor.filename
+    with qtbot.waitSignal(shell.sig_prompt_ready):
+        shell.execute('runfile("{}")'.format(remove_backslashes(filename)))
+
+    assert shell.get_value('test_var') == 123
+    main_window.editor.close_file()
+    os.remove(test_file_copy)
+
 
 # @pytest.mark.slow
 def test_pylint_follows_file(qtbot, tmpdir, main_window):