Repository: spyder-ide/spyder

Issue:
Shifting multiple lines of code with ctrl + cmd + down-arrow-key doesn't work properly after first shift
When using ctrl + cmd + down-arrow-key to shift multiple lines of code, entire code block moves once as expected, but is then automatically deselected. Strangely, ctrl + cmd + up-arrow-key works as expected.

**What steps will reproduce the problem?**

1. Select multiple lines.
2. Move using __ctrl + cmd + down-arrow-key__.
3. Lines will be deselected.

## Versions and main components

* Spyder Version: 3.1.4
* Python Version: 3.6.1
* Qt Version: 5.6.2
* PyQt Version: 5.6.0
* Operating system: OS X El Capitan 10.11.6


## Dependencies

jedi >=0.9.0     :  0.10.2 (OK)
matplotlib >=1.0 :  2.0.2 (OK)
nbconvert >=4.0  :  5.2.1 (OK)
numpy >=1.7      :  1.13.1 (OK)
pandas >=0.13.1  :  0.20.2 (OK)
pep8 >=0.6       :  1.7.0 (OK)
psutil >=0.3     :  5.2.2 (OK)
pyflakes >=0.6.0 :  1.5.0 (OK)
pygments >=2.0   :  2.2.0 (OK)
pylint >=0.25    :  1.6.4 (OK)
qtconsole >=4.2.0:  4.3.0 (OK)
rope >=0.9.4     :  0.9.4-1 (OK)
sphinx >=0.6.6   :  1.6.2 (OK)
sympy >=0.7.3    :  1.1 (OK)



Discussion:
Thanks for reporting. We'll take a look at this one after we release Spyder 3.2
@ccordoba12 
Commenting lines [885 ](https://github.com/spyder-ide/spyder/blob/3.1.x/spyder/widgets/sourcecode/base.py#L885) to [892 ](https://github.com/spyder-ide/spyder/blob/3.1.x/spyder/widgets/sourcecode/base.py#L892) in [base.py](https://github.com/spyder-ide/spyder/blob/3.1.x/spyder/widgets/sourcecode/base.py#L892) solves this issue for me in branch 3.1.x. and 3.x
```
if len(text) == 0:
    #If the next line is blank
    sel_text = sel_text[0:-1]
    sel_text = os.linesep + sel_text
if not text:
    cursor.insertText(sel_text)
    cursor.endEditBlock()
    return
```
I've tried to understand why those lines were necessary in the first place and could not find any reason... The rest of the code seems to handle pretty well the empty-line and end-of-file cases without the need for this block of code. So... I'm probably missing something here, but it is a start in the good direction to solve this issue.
@rlaverde maybe you know?
@jnsebgosselin I invited you to the gitter chat room, there we can chat less asynchronously in case you have questions. Thanks for all the work you are doing :-)

You can download an app https://gitter.im/apps, or use the web interface https://gitter.im/spyder-ide/public
>maybe you know?

No, I haven't changed that behavior, looking at the history that changes was introduced in  #3817 and #2382 for fixing errors with blanks lines and in the end of the file
@rlaverde thanks, this explains a lot. I will look again into this issue taking into consideration the PR you just linked.

Resolving patch:
diff --git a/spyder/widgets/sourcecode/base.py b/spyder/widgets/sourcecode/base.py
--- a/spyder/widgets/sourcecode/base.py
+++ b/spyder/widgets/sourcecode/base.py
@@ -886,61 +886,70 @@ def __move_line_or_selection(self, after_current_line=True):
         cursor = self.textCursor()
         cursor.beginEditBlock()
         start_pos, end_pos = self.__save_selection()
-        add_linesep = False
-        if to_text_string(cursor.selectedText()):
-            # Check if start_pos is at the start of a block
-            cursor.setPosition(start_pos)
-            cursor.movePosition(QTextCursor.StartOfBlock)
-            start_pos = cursor.position()
-
-            cursor.setPosition(end_pos)
-            # Check if end_pos is at the start of a block: if so, starting
-            # changes from the previous block
-            cursor.movePosition(QTextCursor.StartOfBlock,
-                                QTextCursor.KeepAnchor)
-            if to_text_string(cursor.selectedText()):
-                cursor.movePosition(QTextCursor.NextBlock)
-                end_pos = cursor.position()
-        else:
-            cursor.movePosition(QTextCursor.StartOfBlock)
-            start_pos = cursor.position()
+        last_line = False
+        
+        # ------ Select text
+        
+        # Get selection start location
+        cursor.setPosition(start_pos)
+        cursor.movePosition(QTextCursor.StartOfBlock)
+        start_pos = cursor.position()
+        
+        # Get selection end location
+        cursor.setPosition(end_pos)
+        if not cursor.atBlockStart() or end_pos == start_pos:
+            cursor.movePosition(QTextCursor.EndOfBlock)
             cursor.movePosition(QTextCursor.NextBlock)
-            end_pos = cursor.position()
-            # check if on last line
-            if end_pos == start_pos:
-                cursor.movePosition(QTextCursor.End)
-                end_pos = cursor.position()
-                if start_pos == end_pos:
-                    cursor.endEditBlock()
-                    return
-                add_linesep = True
+        end_pos = cursor.position()
+        
+        # Check if selection ends on the last line of the document
+        if cursor.atEnd():
+            if not cursor.atBlockStart() or end_pos == start_pos:
+                last_line = True
+                
+        # ------ Stop if at document boundary
+        
         cursor.setPosition(start_pos)
+        if cursor.atStart() and not after_current_line:
+            # Stop if selection is already at top of the file while moving up
+            cursor.endEditBlock()
+            self.setTextCursor(cursor)
+            self.__restore_selection(start_pos, end_pos)
+            return
+                
         cursor.setPosition(end_pos, QTextCursor.KeepAnchor)
-
+        if last_line and after_current_line:
+            # Stop if selection is already at end of the file while moving down
+            cursor.endEditBlock()
+            self.setTextCursor(cursor)
+            self.__restore_selection(start_pos, end_pos)
+            return
+        
+        # ------ Move text
+        
         sel_text = to_text_string(cursor.selectedText())
-        if add_linesep:
-            sel_text += os.linesep
         cursor.removeSelectedText()
-
+        
+        
         if after_current_line:
-            text = to_text_string(cursor.block().text())
-            if len(text) == 0:
-                #If the next line is blank
-                sel_text = sel_text[0:-1]
-                sel_text = os.linesep + sel_text
-            if not text:
-                cursor.insertText(sel_text)
-                cursor.endEditBlock()
-                return
+            # Shift selection down
+            text = to_text_string(cursor.block().text())  
+            sel_text = os.linesep + sel_text[0:-1]  # Move linesep at the start
+            cursor.movePosition(QTextCursor.EndOfBlock)
             start_pos += len(text)+1
-            end_pos += len(text)+1
-            cursor.movePosition(QTextCursor.NextBlock)
-            if cursor.position() < start_pos:
-                cursor.movePosition(QTextCursor.End)
-                sel_text = os.linesep + sel_text
-                end_pos -= 1
+            end_pos += len(text)
+            if not cursor.atEnd():
+                end_pos += 1        
         else:
-            cursor.movePosition(QTextCursor.PreviousBlock)
+            # Shift selection up
+            if last_line:
+                # Remove the last linesep and add it to the selected text
+                cursor.deletePreviousChar()
+                sel_text = sel_text + os.linesep
+                cursor.movePosition(QTextCursor.StartOfBlock)
+                end_pos += 1
+            else:
+                cursor.movePosition(QTextCursor.PreviousBlock)
             text = to_text_string(cursor.block().text())
             start_pos -= len(text)+1
             end_pos -= len(text)+1


Test patch:
diff --git a/spyder/widgets/tests/test_editor.py b/spyder/widgets/tests/test_editor.py
--- a/spyder/widgets/tests/test_editor.py
+++ b/spyder/widgets/tests/test_editor.py
@@ -17,6 +17,7 @@
 # Third party imports
 import pytest
 from qtpy.QtCore import Qt
+from qtpy.QtGui import QTextCursor
 
 # Local imports
 from spyder.utils.fixtures import setup_editor
@@ -81,6 +82,107 @@ def editor_cells_bot(base_editor_bot):
 
 # Tests
 #-------------------------------
+def test_move_current_line_up(editor_bot):
+    editor_stack, editor, qtbot = editor_bot
+        
+    # Move second line up when nothing is selected.
+    editor.go_to_line(2)
+    editor.move_line_up()
+    expected_new_text = ('print(a)\n'
+                         'a = 1\n'
+                         '\n'
+                         'x = 2\n')
+    assert editor.toPlainText() == expected_new_text
+    
+    # Move line up when already at the top.
+    editor.move_line_up()
+    assert editor.toPlainText() == expected_new_text
+    
+    # Move fourth line up when part of the line is selected.
+    editor.go_to_line(4)    
+    editor.moveCursor(QTextCursor.Right, QTextCursor.MoveAnchor)
+    for i in range(2):
+        editor.moveCursor(QTextCursor.Right, QTextCursor.KeepAnchor)
+    editor.move_line_up()
+    expected_new_text = ('print(a)\n'
+                         'a = 1\n'                         
+                         'x = 2\n'
+                         '\n')
+    assert editor.toPlainText()[:] == expected_new_text
+    
+def test_move_current_line_down(editor_bot):
+    editor_stack, editor, qtbot = editor_bot
+        
+    # Move fourth line down when nothing is selected.
+    editor.go_to_line(4)
+    editor.move_line_down()
+    expected_new_text = ('a = 1\n'
+                         'print(a)\n'
+                         '\n'
+                         '\n'
+                         'x = 2')
+    assert editor.toPlainText() == expected_new_text
+    
+    # Move line down when already at the bottom.
+    editor.move_line_down()
+    assert editor.toPlainText() == expected_new_text
+        
+    # Move first line down when part of the line is selected.
+    editor.go_to_line(1)
+    editor.moveCursor(QTextCursor.Right, QTextCursor.MoveAnchor)
+    for i in range(2):
+        editor.moveCursor(QTextCursor.Right, QTextCursor.KeepAnchor)
+    editor.move_line_down()
+    expected_new_text = ('print(a)\n'
+                         'a = 1\n'
+                         '\n'
+                         '\n'
+                         'x = 2')
+    assert editor.toPlainText() == expected_new_text
+    
+def test_move_multiple_lines_up(editor_bot):
+    editor_stack, editor, qtbot = editor_bot
+    
+    # Move second and third lines up.
+    editor.go_to_line(2)
+    cursor = editor.textCursor()
+    cursor.movePosition(QTextCursor.Down, QTextCursor.KeepAnchor)
+    cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor)
+    editor.setTextCursor(cursor)
+    editor.move_line_up()
+    
+    expected_new_text = ('print(a)\n'
+                         '\n'
+                         'a = 1\n'
+                         'x = 2\n')
+    assert editor.toPlainText() == expected_new_text     
+ 
+    # Move first and second lines up (to test already at top condition).
+    editor.move_line_up()
+    assert editor.toPlainText() == expected_new_text
+
+def test_move_multiple_lines_down(editor_bot):
+    editor_stack, editor, qtbot = editor_bot
+    
+    # Move third and fourth lines down.
+    editor.go_to_line(3)
+    cursor = editor.textCursor()
+    cursor.movePosition(QTextCursor.Down, QTextCursor.KeepAnchor)
+    cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor)
+    editor.setTextCursor(cursor)
+    editor.move_line_down()
+    
+    expected_new_text = ('a = 1\n'
+                         'print(a)\n'
+                         '\n'
+                         '\n'
+                         'x = 2')
+    assert editor.toPlainText() == expected_new_text
+    
+    # Move fourht and fifth lines down (to test already at bottom condition).
+    editor.move_line_down()
+    assert editor.toPlainText() == expected_new_text
+    
 def test_run_top_line(editor_bot):
     editor_stack, editor, qtbot = editor_bot
     editor.go_to_line(1) # line number is one based