Repository: spyder-ide/spyder

Issue:
F9 does not auto advance when it's at the last line
## Description of your problem

**What steps will reproduce the problem?**

When the cursor is at the last line of code, F9 will execute that line but does not advance to a new (blank) line

**What is the expected output? What do you see instead?**

F9 should always advance to the new blank line given the interactive data analysis workflow. I expect to be able to write new line of code, hit F9 to execute and advance to a new line at the same time. (Rstudio also has this behavior).

Currently, the analyst has to write, hit Enter to advance to a new line, then after a while go back to execute all the lines that have just been written. The third step is unnecessary and breaks the workflow.
## Versions and main components
- Spyder Version: 3.0 beta
- Python Version: 2.7
- Operating system: Ubuntu 14.04



Discussion:
Just to be clear: what you're asking us to do is to make **F9** to create a new, empty line when the cursor is at the last line of a file?

Yes! And thanks so much for making F9 auto-advance. It single-handedly convert me to use Spyder regularly.

Makes sense to me, though maybe we should change it to: make **F9** create a new, empty line when the cursor is at the last line of a file, _if that line is non-empty_.

That should not make a difference in the situation that you are thinking of, where you are writing a script and executing it at the same time. 

However, when you are executing line-by-line a script that is already written (which is the situation we were thinking of when implementing the auto-advance), you probably don't want extra lines added to it when you hit **F9** too often.

@jitseniesen That's even better!

@jitseniesen, could you work on this one?


Resolving patch:
diff --git a/runtests.py b/runtests.py
--- a/runtests.py
+++ b/runtests.py
@@ -8,8 +8,11 @@
 File for running tests programmatically.
 """
 
+# Standard library imports
+import sys
+
 # Third party imports
-import qtpy
+import qtpy  # to ensure that Qt4 uses API v2
 import pytest
 
 
@@ -17,9 +20,9 @@ def main():
     """
     Run pytest tests.
     """
-    pytest.main(['-x', 'spyderlib',  '-v', '-rw', '--durations=10',
-                 '--cov=spyderlib', '--cov-report=term-missing'])
-
+    errno = pytest.main(['-x', 'spyderlib',  '-v', '-rw', '--durations=10',
+                         '--cov=spyderlib', '--cov-report=term-missing'])
+    sys.exit(errno)
 
 if __name__ == '__main__':
     main()
diff --git a/spyderlib/widgets/editor.py b/spyderlib/widgets/editor.py
--- a/spyderlib/widgets/editor.py
+++ b/spyderlib/widgets/editor.py
@@ -1817,20 +1817,28 @@ def fix_indentation(self, index=None):
     #------ Run
     def run_selection(self):
         """
-        Run selected text or current line in console. If no text is selected, 
-        then advance cursor to next line.
+        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:
-            move_next_line = False
-        else:
-            editor = self.get_current_editor()
-            line = editor.get_current_line()
-            text = line.lstrip()
-            move_next_line = True
-        self.exec_in_extconsole.emit(text, self.focus_to_editor)
-        if move_next_line:
-            editor.move_cursor_to_next('line', 'down')
+            self.exec_in_extconsole.emit(text, 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):
         """Run current cell"""


Test patch:
diff --git a/spyderlib/utils/tests/test_programs.py b/spyderlib/utils/tests/test_programs.py
--- a/spyderlib/utils/tests/test_programs.py
+++ b/spyderlib/utils/tests/test_programs.py
@@ -5,12 +5,14 @@
 
 """Tests for programs.py"""
 
+import os
 import time
 
 import pytest
 
 from spyderlib.utils.programs import run_python_script_in_terminal
 
+@pytest.mark.skipif(os.name == 'nt', reason='gets stuck on Windows') # FIXME
 def test_run_python_script_in_terminal(tmpdir):
     scriptpath = tmpdir.join('write-done.py')
     outfilepath = tmpdir.join('out.txt')
@@ -23,6 +25,7 @@ def test_run_python_script_in_terminal(tmpdir):
     res = outfilepath.read()
     assert res == 'done'
 
+@pytest.mark.skipif(os.name == 'nt', reason='gets stuck on Windows') # FIXME
 def test_run_python_script_in_terminal_with_wdir_empty(tmpdir):
     scriptpath = tmpdir.join('write-done.py')
     outfilepath = tmpdir.join('out.txt')
diff --git a/spyderlib/widgets/tests/test_editor.py b/spyderlib/widgets/tests/test_editor.py
new file mode 100644
--- /dev/null
+++ b/spyderlib/widgets/tests/test_editor.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2016- The Spyder Development Team
+# Licensed under the terms of the MIT License
+#
+
+"""
+Tests for editor.py
+"""
+
+# Standard library imports
+try:
+    from unittest.mock import Mock
+except ImportError:
+    from mock import Mock # Python 2
+
+# Third party imports
+import pytest
+
+# Local imports
+from spyderlib.widgets.editor import EditorStack
+
+def setup_editor(qtbot):
+    """
+    Set up EditorStack with CodeEditor containing some Python code.
+    The cursor is at the empty line below the code.
+    Returns tuple with EditorStack and CodeEditor.
+    """
+    text = ('a = 1\n'
+            'print(a)\n'
+            '\n'
+            'x = 2')  # a newline is added at end
+    editorStack = EditorStack(None, [])
+    editorStack.set_introspector(Mock())
+    editorStack.set_find_widget(Mock())
+    editorStack.set_io_actions(Mock(), Mock(), Mock(), Mock())
+    finfo = editorStack.new('foo.py', 'utf-8', text)
+    qtbot.addWidget(editorStack)
+    return editorStack, finfo.editor
+
+def test_run_top_line(qtbot):
+    editorStack, editor = setup_editor(qtbot)
+    editor.go_to_line(1) # line number is one based
+    editor.move_cursor(3)
+    with qtbot.waitSignal(editorStack.exec_in_extconsole) as blocker:
+        editorStack.run_selection()
+    assert blocker.signal_triggered
+    assert blocker.args[0] == 'a = 1'
+    # check cursor moves to start of next line; note line number is zero based
+    assert editor.get_cursor_line_column() == (1, 0) 
+
+def test_run_last_nonempty_line(qtbot):
+    editorStack, editor = setup_editor(qtbot)
+    editor.go_to_line(4)
+    with qtbot.waitSignal(editorStack.exec_in_extconsole) as blocker:
+        editorStack.run_selection()
+    assert blocker.signal_triggered
+    assert blocker.args[0] == 'x = 2'
+    assert editor.get_cursor_line_column() == (4, 0) # check cursor moves down
+
+def test_run_empty_line_in_middle(qtbot):
+    editorStack, editor = setup_editor(qtbot)
+    editor.go_to_line(3)
+    with qtbot.assertNotEmitted(editorStack.exec_in_extconsole):
+        editorStack.run_selection()
+    assert editor.get_cursor_line_column() == (3, 0) # check cursor moves down
+
+
+def test_run_last_line_when_empty(qtbot):
+    editorStack, editor = setup_editor(qtbot)
+    with qtbot.assertNotEmitted(editorStack.exec_in_extconsole):
+        editorStack.run_selection()
+    assert editor.get_cursor_line_column() == (4, 0) # check cursor doesn't move
+
+def test_run_last_line_when_nonempty(qtbot):
+    editorStack, editor = setup_editor(qtbot)
+    editor.stdkey_backspace() # delete empty line at end
+    old_text = editor.toPlainText()
+    with qtbot.waitSignal(editorStack.exec_in_extconsole) as blocker:
+        editorStack.run_selection()
+    assert blocker.signal_triggered
+    assert blocker.args[0] == 'x = 2'
+    expected_new_text = old_text + editor.get_line_separator()
+    assert editor.toPlainText() == expected_new_text # check blank line got added
+    assert editor.get_cursor_line_column() == (4, 0) # check cursor moves down
+
+    
+if __name__ == "__main__":
+    pytest.main()