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 __init__(self, font=None, content=None, filename=None, parent=None): """ :param font: A reference to the font to be used by the editor. If not supplied use the system default :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) self.parent = parent # layout font = font if font is not None else QFont() self.editor = CodeEditor("AlternateCSPython", font, self) self.find_replace_dialog = None self.status = QStatusBar(self) self.layout = QVBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.editor) self.layout.addWidget(self.status) self.setLayout(self.layout) self._setup_editor(content, filename) self._presenter = PythonFileInterpreterPresenter(self, PythonCodeExecution(content)) self.code_commenter = CodeCommenter(self.editor) self.editor.modificationChanged.connect(self.sig_editor_modified) self.editor.fileNameChanged.connect(self.sig_filename_modified) self.setAttribute(Qt.WA_DeleteOnClose, True) # Connect the model signals to the view's signals so they can be accessed from outside the MVP self._presenter.model.sig_exec_progress.connect(self.sig_progress) self._presenter.model.sig_exec_error.connect(self.sig_exec_error) self._presenter.model.sig_exec_success.connect(self.sig_exec_success)
def __init__(self, font=None, content=None, filename=None, parent=None, completion_enabled=True): """ :param font: A reference to the font to be used by the editor. If not supplied use the system default :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 :param completion_enabled: Optional parameter to control code auto-completion suggestions """ super(PythonFileInterpreter, self).__init__(parent) self.parent = parent self.completion_enabled = completion_enabled # layout font = font if font is not None else QFont() self.editor = CodeEditor("AlternateCSPython", font, self) self.find_replace_dialog = None self.status = QStatusBar(self) self.layout = QVBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.editor) self.layout.addWidget(self.status) self.setLayout(self.layout) self._setup_editor(content, filename) self._presenter = PythonFileInterpreterPresenter( self, PythonCodeExecution(self.editor)) self.code_commenter = CodeCommenter(self.editor) if self.completion_enabled: self.code_completer = CodeCompleter( self.editor, self._presenter.model.globals_ns) self.editor.modificationChanged.connect(self.sig_editor_modified) self.editor.fileNameChanged.connect(self.sig_filename_modified) self.setAttribute(Qt.WA_DeleteOnClose, True) # Connect the model signals to the view's signals so they can be accessed from outside the MVP self._presenter.model.sig_exec_error.connect(self.sig_exec_error) self._presenter.model.sig_exec_success.connect(self.sig_exec_success) if self.completion_enabled: # Re-populate the completion API after execution success self._presenter.model.sig_exec_success.connect( self.code_completer.update_completion_api) # Only load the simpleapi completions if the code editor starts being modified. self.sig_editor_modified.connect( self.code_completer.add_simpleapi_to_completions_if_required)
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_construction_raises_error_for_unknown_language(self): # self.assertRaises causes a segfault here for some reason... try: CodeEditor("MyCoolLanguage") except ValueError: pass except Exception as exc: self.fail("Expected a Value error to be raised but found a " + exc.__name__)
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)
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) self.status = QStatusBar(self) layout = QVBoxLayout() layout.addWidget(self.editor) layout.addWidget(self.status) self.setLayout(layout) layout.setContentsMargins(0, 0, 0, 0) self._setup_editor(content, filename) self._presenter = PythonFileInterpreterPresenter( self, PythonCodeExecution(content)) self.editor.modificationChanged.connect(self.sig_editor_modified) self.editor.fileNameChanged.connect(self.sig_filename_modified)
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) self.status = QStatusBar(self) layout = QVBoxLayout() layout.addWidget(self.editor) layout.addWidget(self.status) self.setLayout(layout) layout.setContentsMargins(0, 0, 0, 0) self._setup_editor(content, filename) self._presenter = PythonFileInterpreterPresenter(self, PythonCodeExecution(content)) self.editor.modificationChanged.connect(self.sig_editor_modified) self.editor.fileNameChanged.connect(self.sig_filename_modified)
def test_default_construction_yields_empty_filename(self): widget = CodeEditor(TEST_LANG) self.assertEqual("", widget.fileName())
class PythonFileInterpreter(QWidget): sig_editor_modified = Signal(bool) sig_filename_modified = Signal(str) sig_progress = Signal(int) sig_exec_error = Signal(object) sig_exec_success = Signal(object) def __init__(self, font=None, content=None, filename=None, parent=None): """ :param font: A reference to the font to be used by the editor. If not supplied use the system default :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) self.parent = parent # layout font = font if font is not None else QFont() self.editor = CodeEditor("AlternateCSPython", font, self) self.find_replace_dialog = None self.status = QStatusBar(self) self.layout = QVBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.editor) self.layout.addWidget(self.status) self.setLayout(self.layout) self._setup_editor(content, filename) self._presenter = PythonFileInterpreterPresenter(self, PythonCodeExecution(self.editor)) self.code_commenter = CodeCommenter(self.editor) self.code_completer = CodeCompleter(self.editor, self._presenter.model.globals_ns) self.editor.modificationChanged.connect(self.sig_editor_modified) self.editor.fileNameChanged.connect(self.sig_filename_modified) self.setAttribute(Qt.WA_DeleteOnClose, True) # Connect the model signals to the view's signals so they can be accessed from outside the MVP self._presenter.model.sig_exec_error.connect(self.sig_exec_error) self._presenter.model.sig_exec_success.connect(self.sig_exec_success) # Re-populate the completion API after execution success self._presenter.model.sig_exec_success.connect(self.code_completer.update_completion_api) 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(prompt_for_confirmation=self.parent.confirm_on_save) def abort(self): self._presenter.req_abort() def execute_async(self, ignore_selection=False): return self._presenter.req_execute_async(ignore_selection) def execute_async_blocking(self): self._presenter.req_execute_async_blocking() def save(self, prompt_for_confirmation=False, force_save=False): if self.editor.isModified(): io = EditorIO(self.editor) return io.save_if_required(prompt_for_confirmation, force_save) else: return True def save_as(self): io = EditorIO(self.editor) new_filename = io.ask_for_filename() if new_filename: return io.write(save_as=new_filename), new_filename else: return False, None 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.setEolVisibility(True) self.editor.setWhitespaceVisibility(CodeEditor.WsVisible) def set_whitespace_invisible(self): self.editor.setEolVisibility(False) self.editor.setWhitespaceVisibility(CodeEditor.WsInvisible) def toggle_comment(self): self.code_commenter.toggle_comment() def _setup_editor(self, default_content, filename): editor = self.editor # Clear default QsciScintilla key bindings that we want to allow # to be users of this class self.clear_key_binding("Ctrl+/") # 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) def clear_key_binding(self, key_str): """Clear a keyboard shortcut bound to a Scintilla command""" self.editor.clearKeyBinding(key_str)
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()")
def test_get_selection_for_empty_selection(self): widget = CodeEditor(TEST_LANG) res = widget.getSelection() self.assertEqual((-1, -1, -1, -1), res)
def test_setReadOnly_to_true_sets_readonly_status(self): widget = CodeEditor(TEST_LANG) widget.setReadOnly(True) self.assertTrue(widget.isReadOnly())
def test_default_construction_yields_editable_widget(self): widget = CodeEditor(TEST_LANG) self.assertFalse(widget.isReadOnly())
def test_set_filename_returns_expected_string(self): widget = CodeEditor(TEST_LANG) test_filename = "myscript.py" widget.setFileName(test_filename) self.assertEqual(test_filename, widget.fileName())
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 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) self.status = QStatusBar(self) layout = QVBoxLayout() layout.addWidget(self.editor) layout.addWidget(self.status) self.setLayout(layout) layout.setContentsMargins(0, 0, 0, 0) self._setup_editor(content, filename) self._presenter = PythonFileInterpreterPresenter( self, PythonCodeExecution(content)) self.editor.modificationChanged.connect(self.sig_editor_modified) self.editor.fileNameChanged.connect(self.sig_filename_modified) @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 _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 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) self.status = QStatusBar(self) layout = QVBoxLayout() layout.addWidget(self.editor) layout.addWidget(self.status) self.setLayout(layout) layout.setContentsMargins(0, 0, 0, 0) self._setup_editor(content, filename) self._presenter = PythonFileInterpreterPresenter(self, PythonCodeExecution(content)) self.editor.modificationChanged.connect(self.sig_editor_modified) self.editor.fileNameChanged.connect(self.sig_filename_modified) @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 _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)
def test_set_text_can_be_read_again(self): widget = CodeEditor(TEST_LANG) code_str = 'print "Hello World!"' widget.setText(code_str) self.assertEqual(code_str, widget.text())
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))
def test_construction_accepts_Python_as_language(self): CodeEditor(TEST_LANG)
class PythonFileInterpreter(QWidget): sig_editor_modified = Signal(bool) sig_filename_modified = Signal(str) sig_progress = Signal(int) sig_exec_error = Signal(object) sig_exec_success = Signal(object) def __init__(self, font=None, content=None, filename=None, parent=None): """ :param font: A reference to the font to be used by the editor. If not supplied use the system default :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) self.parent = parent # layout font = font if font is not None else QFont() self.editor = CodeEditor("AlternateCSPython", font, self) self.find_replace_dialog = None self.status = QStatusBar(self) self.layout = QVBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.editor) self.layout.addWidget(self.status) self.setLayout(self.layout) self._setup_editor(content, filename) self._presenter = PythonFileInterpreterPresenter(self, PythonCodeExecution(content)) self.code_commenter = CodeCommenter(self.editor) self.editor.modificationChanged.connect(self.sig_editor_modified) self.editor.fileNameChanged.connect(self.sig_filename_modified) self.setAttribute(Qt.WA_DeleteOnClose, True) # Connect the model signals to the view's signals so they can be accessed from outside the MVP self._presenter.model.sig_exec_progress.connect(self.sig_progress) self._presenter.model.sig_exec_error.connect(self.sig_exec_error) self._presenter.model.sig_exec_success.connect(self.sig_exec_success) 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(prompt_for_confirmation=self.parent.confirm_on_save) def abort(self): self._presenter.req_abort() def execute_async(self, ignore_selection=False): return self._presenter.req_execute_async(ignore_selection) def execute_async_blocking(self): self._presenter.req_execute_async_blocking() def save(self, prompt_for_confirmation=False, force_save=False): if self.editor.isModified(): io = EditorIO(self.editor) return io.save_if_required(prompt_for_confirmation, force_save) else: return True def save_as(self): io = EditorIO(self.editor) new_filename = io.ask_for_filename() if new_filename: return io.write(save_as=new_filename), new_filename else: return False, None 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 toggle_comment(self): self.code_commenter.toggle_comment() def _setup_editor(self, default_content, filename): editor = self.editor # Clear default QsciScintilla key bindings that we want to allow # to be users of this class self.clear_key_binding("Ctrl+/") # 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) def clear_key_binding(self, key_str): """Clear a keyboard shortcut bound to a Scintilla command""" self.editor.clearKeyBinding(key_str)