From fdfd925e82fbaf99cfb3fc8d394feaa502486277 Mon Sep 17 00:00:00 2001 From: Ludwig Neste Date: Tue, 10 Sep 2024 14:22:20 +0200 Subject: [PATCH 1/7] add copy whole line --- spyder/plugins/editor/widgets/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spyder/plugins/editor/widgets/base.py b/spyder/plugins/editor/widgets/base.py index 18532728aed..92cc690214b 100644 --- a/spyder/plugins/editor/widgets/base.py +++ b/spyder/plugins/editor/widgets/base.py @@ -480,6 +480,8 @@ def copy(self): """ if self.get_selected_text(): QApplication.clipboard().setText(self.get_selected_text()) + elif self.get_current_line(): + QApplication.clipboard().setText(self.get_current_line()) def toPlainText(self): """ From e08c0760d7782eaec00fa364ea88f638111868d6 Mon Sep 17 00:00:00 2001 From: Ludwig Neste Date: Tue, 10 Sep 2024 14:22:33 +0200 Subject: [PATCH 2/7] add cut whole line --- spyder/plugins/editor/widgets/codeeditor/codeeditor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spyder/plugins/editor/widgets/codeeditor/codeeditor.py b/spyder/plugins/editor/widgets/codeeditor/codeeditor.py index fd63a62a4fc..6d655cb8b3b 100644 --- a/spyder/plugins/editor/widgets/codeeditor/codeeditor.py +++ b/spyder/plugins/editor/widgets/codeeditor/codeeditor.py @@ -1950,7 +1950,11 @@ def cut(self): """Reimplement cut to signal listeners about changes on the text.""" has_selected_text = self.has_selected_text() if not has_selected_text: - return + # If no text is selected, the entire line will be selected and cut + cursor = self.textCursor() + cursor.select(QTextCursor.LineUnderCursor) + self.setTextCursor(cursor) + start, end = self.get_selection_start_end() self.sig_will_remove_selection.emit(start, end) self.sig_delete_requested.emit() From de9322fd5e70300ca637e2cb0477fbbff3d92173 Mon Sep 17 00:00:00 2001 From: Ludwig Neste Date: Tue, 10 Sep 2024 14:54:14 +0200 Subject: [PATCH 3/7] fix not removing empty line --- spyder/plugins/editor/widgets/codeeditor/codeeditor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyder/plugins/editor/widgets/codeeditor/codeeditor.py b/spyder/plugins/editor/widgets/codeeditor/codeeditor.py index 6d655cb8b3b..b77d65ed202 100644 --- a/spyder/plugins/editor/widgets/codeeditor/codeeditor.py +++ b/spyder/plugins/editor/widgets/codeeditor/codeeditor.py @@ -1952,7 +1952,7 @@ def cut(self): if not has_selected_text: # If no text is selected, the entire line will be selected and cut cursor = self.textCursor() - cursor.select(QTextCursor.LineUnderCursor) + cursor.select(QTextCursor.BlockUnderCursor) self.setTextCursor(cursor) start, end = self.get_selection_start_end() From f9e0434d8c9d647a2faa8eced9c036744347a07c Mon Sep 17 00:00:00 2001 From: Ludwig Neste Date: Tue, 10 Sep 2024 15:53:55 +0200 Subject: [PATCH 4/7] fix edgecases (last line, first line, only line) --- .../editor/widgets/codeeditor/codeeditor.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/spyder/plugins/editor/widgets/codeeditor/codeeditor.py b/spyder/plugins/editor/widgets/codeeditor/codeeditor.py index b77d65ed202..fa16d81645b 100644 --- a/spyder/plugins/editor/widgets/codeeditor/codeeditor.py +++ b/spyder/plugins/editor/widgets/codeeditor/codeeditor.py @@ -1952,7 +1952,21 @@ def cut(self): if not has_selected_text: # If no text is selected, the entire line will be selected and cut cursor = self.textCursor() - cursor.select(QTextCursor.BlockUnderCursor) + # Do selection manually so the newline at the end of the line is selected + # using cursor.select(QTextCursor.BlockUnderCursor) selects previous newline + cursor.movePosition(QTextCursor.StartOfBlock, QTextCursor.MoveAnchor) + if not cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor): + # if there is no next Block, we select the previous newline + if not cursor.movePosition(QTextCursor.PreviousBlock, QTextCursor.MoveAnchor): + # if there is no previous block, we can select the current line + # this is the 1-line file case + cursor.select(QTextCursor.BlockUnderCursor) + else: + # There is a previous block + cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.MoveAnchor) + cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor) + cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor) + self.setTextCursor(cursor) start, end = self.get_selection_start_end() From 3a8853d001f1b6d004f389a31860ba1234faef1c Mon Sep 17 00:00:00 2001 From: Ludwig Neste Date: Tue, 10 Sep 2024 17:30:18 +0200 Subject: [PATCH 5/7] fix more edge-cases when copying/cutting whole line --- spyder/plugins/editor/widgets/base.py | 44 ++++++++++++++++++- .../editor/widgets/codeeditor/codeeditor.py | 19 +------- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/spyder/plugins/editor/widgets/base.py b/spyder/plugins/editor/widgets/base.py index 92cc690214b..3391cd2a15f 100644 --- a/spyder/plugins/editor/widgets/base.py +++ b/spyder/plugins/editor/widgets/base.py @@ -480,8 +480,10 @@ def copy(self): """ if self.get_selected_text(): QApplication.clipboard().setText(self.get_selected_text()) - elif self.get_current_line(): - QApplication.clipboard().setText(self.get_current_line()) + else: + cursor = self.select_current_line_and_sep(set_cursor=False) + text = to_text_string(cursor.selectedText()) + QApplication.clipboard().setText(text) def toPlainText(self): """ @@ -650,6 +652,44 @@ def select_current_cell(self, cursor=None): return cursor, cell_full_file + def select_current_line_and_sep(self, cursor=None, set_cursor=True): + """ + Selects the current line, including the correct line separator to + delete or copy the whole current line. + + This means: + - If there is a next block, select the current block's newline char. + - Else if there is a previous block, select the previous newline char. + - Else select no newline char (1-line file) + + Does a similar thing to `cursor.select(QTextCursor.BlockUnderCursor)`, + which always selects the previous newline char. + """ + if cursor is None: + cursor = self.textCursor() + + cursor.movePosition(QTextCursor.StartOfBlock, QTextCursor.MoveAnchor) + + if not cursor.movePosition(QTextCursor.NextBlock, + QTextCursor.KeepAnchor): + # if there is no next Block, we select the previous newline + if cursor.movePosition(QTextCursor.PreviousBlock, + QTextCursor.MoveAnchor): + cursor.movePosition(QTextCursor.EndOfBlock, + QTextCursor.MoveAnchor) + cursor.movePosition(QTextCursor.NextBlock, + QTextCursor.KeepAnchor) + cursor.movePosition(QTextCursor.EndOfBlock, + QTextCursor.KeepAnchor) + else: + # if there is no previous block, we can select the current line + # this is the 1-line file case + cursor.select(QTextCursor.BlockUnderCursor) + + if set_cursor: + self.setTextCursor(cursor) + return cursor + def go_to_next_cell(self): """Go to the next cell of lines""" cursor = self.textCursor() diff --git a/spyder/plugins/editor/widgets/codeeditor/codeeditor.py b/spyder/plugins/editor/widgets/codeeditor/codeeditor.py index fa16d81645b..1988894c0ee 100644 --- a/spyder/plugins/editor/widgets/codeeditor/codeeditor.py +++ b/spyder/plugins/editor/widgets/codeeditor/codeeditor.py @@ -1950,24 +1950,7 @@ def cut(self): """Reimplement cut to signal listeners about changes on the text.""" has_selected_text = self.has_selected_text() if not has_selected_text: - # If no text is selected, the entire line will be selected and cut - cursor = self.textCursor() - # Do selection manually so the newline at the end of the line is selected - # using cursor.select(QTextCursor.BlockUnderCursor) selects previous newline - cursor.movePosition(QTextCursor.StartOfBlock, QTextCursor.MoveAnchor) - if not cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor): - # if there is no next Block, we select the previous newline - if not cursor.movePosition(QTextCursor.PreviousBlock, QTextCursor.MoveAnchor): - # if there is no previous block, we can select the current line - # this is the 1-line file case - cursor.select(QTextCursor.BlockUnderCursor) - else: - # There is a previous block - cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.MoveAnchor) - cursor.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor) - cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor) - - self.setTextCursor(cursor) + self.select_current_line_and_sep() start, end = self.get_selection_start_end() self.sig_will_remove_selection.emit(start, end) From 76ed32df23211c8d9b916bdcc1f9e948791fb5ee Mon Sep 17 00:00:00 2001 From: Ludwig Neste Date: Tue, 10 Sep 2024 19:29:25 +0200 Subject: [PATCH 6/7] fix newline char bug upon copy --- spyder/plugins/editor/widgets/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spyder/plugins/editor/widgets/base.py b/spyder/plugins/editor/widgets/base.py index 3391cd2a15f..2c9b397b3da 100644 --- a/spyder/plugins/editor/widgets/base.py +++ b/spyder/plugins/editor/widgets/base.py @@ -482,8 +482,7 @@ def copy(self): QApplication.clipboard().setText(self.get_selected_text()) else: cursor = self.select_current_line_and_sep(set_cursor=False) - text = to_text_string(cursor.selectedText()) - QApplication.clipboard().setText(text) + QApplication.clipboard().setText(self.get_selected_text(cursor)) def toPlainText(self): """ From 714c258cb23ab0b346ef84a85693ddfbf2558dfe Mon Sep 17 00:00:00 2001 From: Ludwig Neste Date: Tue, 10 Sep 2024 19:29:37 +0200 Subject: [PATCH 7/7] add tests --- .../codeeditor/tests/test_codeeditor.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/spyder/plugins/editor/widgets/codeeditor/tests/test_codeeditor.py b/spyder/plugins/editor/widgets/codeeditor/tests/test_codeeditor.py index 7233f2e58ea..497229c9abe 100644 --- a/spyder/plugins/editor/widgets/codeeditor/tests/test_codeeditor.py +++ b/spyder/plugins/editor/widgets/codeeditor/tests/test_codeeditor.py @@ -17,6 +17,7 @@ # Local imports from spyder.widgets.mixins import TIP_PARAMETER_HIGHLIGHT_COLOR +from spyder.py3compat import to_text_string HERE = osp.dirname(osp.abspath(__file__)) @@ -548,6 +549,71 @@ def test_delete(codeeditor): assert editor.get_text_line(0) == ' f1(a, b):' +def test_copy_entire_line(codeeditor): + """Test copying an entire line, if nothing is selected.""" + editor = codeeditor + text = "import this\nmsg='Hello World!'\nprint(msg)" + editor.set_text(text) + + # copy first line + cursor = editor.textCursor() + cursor.movePosition(QTextCursor.Start) + editor.setTextCursor(cursor) + editor.copy() + + cb = QApplication.clipboard() + assert cb.text() == "import this\n" + + # copy second line + cursor = editor.textCursor() + cursor.movePosition(QTextCursor.NextBlock) + editor.setTextCursor(cursor) + editor.copy() + cb = QApplication.clipboard() + assert cb.text() == "msg='Hello World!'\n" + + # copy third line + cursor = editor.textCursor() + cursor.movePosition(QTextCursor.NextBlock) + editor.setTextCursor(cursor) + editor.copy() + cb = QApplication.clipboard() + # since it is the last line, the newline should be + # at the start with the current implementation + assert cb.text() == "\nprint(msg)" + + +def test_cut_entire_line(codeeditor): + """Test cutting an entire line, if nothing is selected.""" + editor = codeeditor + text = "import this\nmsg='Hello World!'\nprint(msg)" + editor.set_text(text) + + # cut first line + cursor = editor.textCursor() + cursor.movePosition(QTextCursor.Start) + editor.setTextCursor(cursor) + editor.cut() + + cb = QApplication.clipboard() + assert cb.text() == "import this\n" + + # cut third line (tests last line case) + cb = QApplication.clipboard() + cursor = editor.textCursor() + cursor.movePosition(QTextCursor.NextBlock) + editor.setTextCursor(cursor) + editor.cut() + assert cb.text() == "\nprint(msg)" + + # cut third line + editor.cut() + cb = QApplication.clipboard() + # since it is the last line in the document, + # there is no newline to cut, thus this has no newline anymore + assert cb.text() == "msg='Hello World!'" + + def test_paste_files(codeeditor, copy_files_clipboard): """Test pasting files/folders into the editor.""" editor = codeeditor