def test_get_selection_for_non_empty_selection(self): widget = CodeEditor(TEST_LANG) widget.setText("""first line second line third line fourth line """) selected = (0, 2, 3, 4) widget.setSelection(*selected) res = widget.getSelection() self.assertEqual(selected, res)
class CodeCommenterTest(unittest.TestCase): def setUp(self): self.lines = [ "# Mantid Repository : https://github.com/mantidproject/mantid", "# ", "# Copyright © 2019 ISIS Rutherford Appleton Laboratory UKRI,", "# NScD Oak Ridge National Laboratory, European Spallation Source", "# & Institut Laue - Langevin", "# SPDX - License - Identifier: GPL - 3.0 +", "# This file is part of the mantidqt package", "", "import numpy", "# import mantid", "do_something()" ] self.editor = CodeEditor("AlternateCSPython", QFont()) self.editor.setText('\n'.join(self.lines)) self.commenter = CodeCommenter(self.editor) def test_comment_single_line_no_selection(self): self.editor.setCursorPosition(8, 1) self.commenter.toggle_comment() expected_lines = copy(self.lines) expected_lines[8] = '# ' + expected_lines[8] self.assertEqual(self.editor.text(), '\n'.join(expected_lines)) def test_multiline_uncomment(self): start_line, end_line = 0, 3 self.editor.setSelection(0, 2, 3, 2) self.commenter.toggle_comment() expected_lines = copy(self.lines) expected_lines[start_line:end_line + 1] = [ line.replace('# ', '') for line in expected_lines[0:end_line + 1] ] self.assertEqual(self.editor.text(), '\n'.join(expected_lines)) def test_multiline_comment(self): start_line, end_line = 5, 8 self.editor.setSelection(start_line, 5, end_line, 5) self.commenter.toggle_comment() expected_lines = copy(self.lines) expected_lines[start_line:end_line + 1] = [ '# ' + line for line in expected_lines[start_line:end_line + 1] ] self.assertEqual(self.editor.text(), '\n'.join(expected_lines))
class PythonFileInterpreter(QWidget): sig_editor_modified = Signal(bool) sig_filename_modified = Signal(str) def __init__(self, content=None, filename=None, parent=None): """ :param content: An optional string of content to pass to the editor :param filename: The file path where the content was read. :param parent: An optional parent QWidget """ super(PythonFileInterpreter, self).__init__(parent) # layout self.editor = CodeEditor("AlternateCSPythonLexer", self) # Clear QsciScintilla key bindings that may override PyQt's bindings self.clear_key_binding("Ctrl+/") self.status = QStatusBar(self) self.layout = QVBoxLayout() self.layout.addWidget(self.editor) self.layout.addWidget(self.status) self.setLayout(self.layout) self.layout.setContentsMargins(0, 0, 0, 0) self._setup_editor(content, filename) self.setAttribute(Qt.WA_DeleteOnClose, True) self._presenter = PythonFileInterpreterPresenter( self, PythonCodeExecution(content)) self.editor.modificationChanged.connect(self.sig_editor_modified) self.editor.fileNameChanged.connect(self.sig_filename_modified) self.find_replace_dialog = None self.find_replace_dialog_shown = False def closeEvent(self, event): self.deleteLater() if self.find_replace_dialog: self.find_replace_dialog.close() super(PythonFileInterpreter, self).closeEvent(event) def show_find_replace_dialog(self): if self.find_replace_dialog is None: self.find_replace_dialog = EmbeddedFindReplaceDialog( self, self.editor) self.layout.insertWidget(0, self.find_replace_dialog.view) self.find_replace_dialog.show() def hide_find_replace_dialog(self): if self.find_replace_dialog is not None: self.find_replace_dialog.hide() @property def filename(self): return self.editor.fileName() def confirm_close(self): """Confirm the widget can be closed. If the editor contents are modified then a user can interject and cancel closing. :return: True if closing was considered successful, false otherwise """ return self.save(confirm=True) def abort(self): self._presenter.req_abort() def execute_async(self): self._presenter.req_execute_async() def save(self, confirm=False): if self.editor.isModified(): io = EditorIO(self.editor) return io.save_if_required(confirm) else: return True def set_editor_readonly(self, ro): self.editor.setReadOnly(ro) def set_status_message(self, msg): self.status.showMessage(msg) def replace_tabs_with_spaces(self): self.replace_text(TAB_CHAR, SPACE_CHAR * TAB_WIDTH) def replace_text(self, match_text, replace_text): if self.editor.selectedText() == '': self.editor.selectAll() new_text = self.editor.selectedText().replace(match_text, replace_text) self.editor.replaceSelectedText(new_text) def replace_spaces_with_tabs(self): self.replace_text(SPACE_CHAR * TAB_WIDTH, TAB_CHAR) def set_whitespace_visible(self): self.editor.setWhitespaceVisibility(CodeEditor.WsVisible) def set_whitespace_invisible(self): self.editor.setWhitespaceVisibility(CodeEditor.WsInvisible) def clear_key_binding(self, key_str): """Clear a keyboard shortcut bound to a Scintilla command""" self.editor.clearKeyBinding(key_str) def toggle_comment(self): if self.editor.selectedText() == '': # If nothing selected, do nothing return # Note selection indices to restore highlighting later selection_idxs = list(self.editor.getSelection()) # Expand selection from first character on start line to end char on last line line_end_pos = len( self.editor.text().split('\n')[selection_idxs[2]].rstrip()) line_selection_idxs = [ selection_idxs[0], 0, selection_idxs[2], line_end_pos ] self.editor.setSelection(*line_selection_idxs) selected_lines = self.editor.selectedText().split('\n') if self._are_comments(selected_lines) is True: toggled_lines = self._uncomment_lines(selected_lines) # Track deleted characters to keep highlighting consistent selection_idxs[1] -= 2 selection_idxs[-1] -= 2 else: toggled_lines = self._comment_lines(selected_lines) selection_idxs[1] += 2 selection_idxs[-1] += 2 # Replace lines with commented/uncommented lines self.editor.replaceSelectedText('\n'.join(toggled_lines)) # Restore highlighting self.editor.setSelection(*selection_idxs) def _comment_lines(self, lines): for i in range(len(lines)): lines[i] = '# ' + lines[i] return lines def _uncomment_lines(self, lines): for i in range(len(lines)): uncommented_line = lines[i].replace('# ', '', 1) if uncommented_line == lines[i]: uncommented_line = lines[i].replace('#', '', 1) lines[i] = uncommented_line return lines def _are_comments(self, code_lines): for line in code_lines: if line.strip(): if not line.strip().startswith('#'): return False return True def _setup_editor(self, default_content, filename): editor = self.editor # use tabs not spaces for indentation editor.setIndentationsUseTabs(False) editor.setTabWidth(TAB_WIDTH) # show current editing line but in a softer color editor.setCaretLineBackgroundColor(CURRENTLINE_BKGD_COLOR) editor.setCaretLineVisible(True) # set a margin large enough for sensible file sizes < 1000 lines # and the progress marker font_metrics = QFontMetrics(self.font()) editor.setMarginWidth(1, font_metrics.averageCharWidth() * 3 + 20) # fill with content if supplied and set source filename if default_content is not None: editor.setText(default_content) if filename is not None: editor.setFileName(filename) # Default content does not count as a modification editor.setModified(False) editor.enableAutoCompletion(CodeEditor.AcsAll)
class CodeCommenterTest(unittest.TestCase): def setUp(self): self.lines = [ "# Mantid Repository : https://github.com/mantidproject/mantid", "# ", "# Copyright © 2019 ISIS Rutherford Appleton Laboratory UKRI,", "# NScD Oak Ridge National Laboratory, European Spallation Source", "# & Institut Laue - Langevin", "# SPDX - License - Identifier: GPL - 3.0 +", "# This file is part of the mantidqt package", "", "import numpy", "# import mantid", "for ii in range(2):", " do_something()", "do_something_else()" ] self.editor = CodeEditor("AlternateCSPython", QFont()) self.editor.setText('\n'.join(self.lines)) self.commenter = CodeCommenter(self.editor) def test_comment_single_line_no_selection(self): self.editor.setCursorPosition(8, 1) self.commenter.toggle_comment() expected_lines = copy(self.lines) expected_lines[8] = '# ' + expected_lines[8] self.assertEqual(self.editor.text(), '\n'.join(expected_lines)) def test_uncomment_with_inline_comment(self): """ Check that uncommenting works correctly when there is an inline comment on the line of code that is to be uncommented. """ commented_lines = [ '#do_something() # inline comment', '#do_something() #inline comment', '# do_something() # inline comment', '# do_something() #inline comment', ' #do_something() # inline comment', ' #do_something() #inline comment', ' # do_something() # inline comment', ' # do_something() #inline comment' ] expected_uncommented_lines = [ 'do_something() # inline comment', 'do_something() #inline comment', 'do_something() # inline comment', 'do_something() #inline comment', ' do_something() # inline comment', ' do_something() #inline comment', ' do_something() # inline comment', ' do_something() #inline comment' ] uncommented_lines = self.commenter._uncomment_lines(commented_lines) self.assertEqual(expected_uncommented_lines, uncommented_lines) def test_multiline_uncomment(self): start_line, end_line = 0, 3 self.editor.setSelection(0, 2, 3, 2) self.commenter.toggle_comment() expected_lines = copy(self.lines) expected_lines[start_line:end_line + 1] = [ line.replace('# ', '') for line in expected_lines[0:end_line + 1] ] self.assertEqual(self.editor.text(), '\n'.join(expected_lines)) def test_multiline_comment_for_mix_of_commented_and_uncommented_lines( self): start_line, end_line = 5, 8 self.editor.setSelection(start_line, 5, end_line, 5) self.commenter.toggle_comment() expected_lines = copy(self.lines) expected_lines[start_line:end_line + 1] = [ '# ' + line for line in expected_lines[start_line:end_line + 1] ] self.assertEqual(self.editor.text(), '\n'.join(expected_lines)) def test_multiline_comment_uses_top_level_indentation(self): start_line, end_line = 10, 13 self.editor.setSelection(start_line, 5, end_line, 5) self.commenter.toggle_comment() expected_lines = [ "# for ii in range(2):", "# do_something()", "# do_something_else()" ] self.assertEqual(self.editor.text().split('\n')[start_line:end_line], expected_lines) def test_comment_preserves_indenting_on_single_line(self): iline = 11 self.editor.setSelection(iline, 5, iline, 6) self.commenter.toggle_comment() # check commented at indented position self.assertEqual(self.editor.text().split('\n')[iline], " # do_something()")