예제 #1
0
    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
예제 #2
0
    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)
예제 #3
0
    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)
예제 #4
0
    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)
예제 #5
0
 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__)
예제 #6
0
 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)
예제 #7
0
    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)
예제 #8
0
 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)
예제 #9
0
    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)
예제 #10
0
 def test_default_construction_yields_empty_filename(self):
     widget = CodeEditor(TEST_LANG)
     self.assertEqual("", widget.fileName())
예제 #11
0
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)
예제 #12
0
class CodeCommenterTest(unittest.TestCase):
    def setUp(self):
        self.lines = [
            "# Mantid Repository : https://github.com/mantidproject/mantid",
            "# ",
            "# Copyright &copy; 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()")
예제 #13
0
 def test_get_selection_for_empty_selection(self):
     widget = CodeEditor(TEST_LANG)
     res = widget.getSelection()
     self.assertEqual((-1, -1, -1, -1), res)
예제 #14
0
 def test_setReadOnly_to_true_sets_readonly_status(self):
     widget = CodeEditor(TEST_LANG)
     widget.setReadOnly(True)
     self.assertTrue(widget.isReadOnly())
예제 #15
0
 def test_default_construction_yields_editable_widget(self):
     widget = CodeEditor(TEST_LANG)
     self.assertFalse(widget.isReadOnly())
예제 #16
0
 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())
예제 #17
0
 def test_get_selection_for_empty_selection(self):
     widget = CodeEditor(TEST_LANG)
     res = widget.getSelection()
     self.assertEqual((-1, -1, -1, -1), res)
예제 #18
0
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)
예제 #19
0
 def test_default_construction_yields_editable_widget(self):
     widget = CodeEditor(TEST_LANG)
     self.assertFalse(widget.isReadOnly())
예제 #20
0
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)
예제 #21
0
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)
예제 #22
0
 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())
예제 #23
0
 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())
예제 #24
0
 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())
예제 #25
0
class CodeCommenterTest(unittest.TestCase):
    def setUp(self):
        self.lines = [
            "# Mantid Repository : https://github.com/mantidproject/mantid",
            "# ",
            "# Copyright &copy; 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))
예제 #26
0
 def test_setReadOnly_to_true_sets_readonly_status(self):
     widget = CodeEditor(TEST_LANG)
     widget.setReadOnly(True)
     self.assertTrue(widget.isReadOnly())
예제 #27
0
 def test_construction_accepts_Python_as_language(self):
     CodeEditor(TEST_LANG)
예제 #28
0
 def test_default_construction_yields_empty_filename(self):
     widget = CodeEditor(TEST_LANG)
     self.assertEqual("", widget.fileName())
예제 #29
0
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)