def construct_editor(qtbot, *args, **kwargs): os.environ['SPY_TEST_USE_INTROSPECTION'] = 'True' app = qapplication() lsp_manager = LSPManager(parent=None) editor = CodeEditor(parent=None) kwargs['language'] = 'Python' editor.setup_editor(*args, **kwargs) wrapper = LSPEditorWrapper(None, editor, lsp_manager) lsp_manager.register_plugin_type( LSPEventTypes.DOCUMENT, wrapper.sig_initialize) with qtbot.waitSignal(wrapper.sig_initialize, timeout=30000): editor.filename = 'test.py' editor.language = 'Python' lsp_manager.start_lsp_client('python') text = ("def some_function():\n" # D100, D103: Missing docstring " \n" # W293 trailing spaces " a = 1 # a comment\n" # E261 two spaces before inline comment "\n" " a += s\n" # Undefined variable s " return a\n" ) editor.set_text(text) with qtbot.waitSignal(editor.lsp_response_signal, timeout=30000): editor.document_did_open() yield editor, lsp_manager os.environ['SPY_TEST_USE_INTROSPECTION'] = 'False' lsp_manager.closing_plugin()
def lsp_codeeditor(lsp_manager, qtbot_module, request): """CodeEditor instance with LSP services activated.""" # Create a CodeEditor instance editor = CodeEditor(parent=None) editor.setup_editor(language='Python', tab_mode=False, markers=True, close_quotes=True, close_parentheses=True, color_scheme='spyder/dark', font=QFont("Monospace", 10)) editor.resize(640, 480) qtbot_module.addWidget(editor) editor.show() # Redirect editor LSP requests to lsp_manager editor.sig_perform_lsp_request.connect(lsp_manager.send_request) editor.filename = 'test.py' editor.language = 'Python' lsp_manager.register_file('python', 'test.py', editor) server_settings = lsp_manager.main.editor.lsp_editor_settings['python'] editor.start_lsp_services(server_settings) with qtbot_module.waitSignal(editor.lsp_response_signal, timeout=30000): editor.document_did_open() def teardown(): editor.hide() editor.completion_widget.hide() request.addfinalizer(teardown) return editor, lsp_manager
def lsp_codeeditor(lsp_manager, qtbot_module, request): """CodeEditor instance with LSP services activated.""" # Create a CodeEditor instance editor = CodeEditor(parent=None) editor.setup_editor(language='Python', tab_mode=False, markers=True, color_scheme='spyder/dark', font=QFont("Monospace", 10)) editor.resize(640, 480) qtbot_module.addWidget(editor) editor.show() # Redirect editor LSP requests to lsp_manager editor.sig_perform_lsp_request.connect(lsp_manager.send_request) editor.filename = 'test.py' editor.language = 'Python' lsp_manager.register_file('python', 'test.py', editor) server_settings = lsp_manager.main.editor.lsp_editor_settings['python'] editor.start_lsp_services(server_settings) with qtbot_module.waitSignal(editor.lsp_response_signal, timeout=30000): editor.document_did_open() def teardown(): editor.hide() editor.completion_widget.hide() request.addfinalizer(teardown) return editor, lsp_manager
def editor_close_quotes(): """Set up Editor with close quotes activated.""" app = qapplication() editor = CodeEditor(parent=None) kwargs = {} kwargs['language'] = 'Python' kwargs['close_quotes'] = True editor.setup_editor(**kwargs) return editor
def findreplace_editor(qtbot, request): """Set up PathManager.""" editor = CodeEditor() editor.setup_editor() widget = FindReplace(None) widget.set_editor(editor) qtbot.addWidget(widget) qtbot.addWidget(editor) return widget, editor
def construct_editor(text): app = qapplication() editor = CodeEditor(parent=None) editor.setup_editor(language='Python') editor.set_text(text) cursor = editor.textCursor() cursor.movePosition(QTextCursor.End) editor.setTextCursor(cursor) return editor
def editor_close_brackets(): """Set up Editor with close brackets activated.""" app = qapplication() editor = CodeEditor(parent=None) kwargs = {} kwargs['language'] = 'Python' kwargs['close_parentheses'] = True editor.setup_editor(**kwargs) return editor
def editor_auto_docstring(): """Set up Editor with auto docstring activated.""" app = qapplication() editor = CodeEditor(parent=None) kwargs = {} kwargs['language'] = 'Python' kwargs['close_quotes'] = True kwargs['close_parentheses'] = True editor.setup_editor(**kwargs) return editor
def editor_bot(qtbot): widget = CodeEditor(None) widget.setup_editor(linenumbers=True, markers=True, show_blanks=True, scrollflagarea=True, font=QFont("Courier New", 10), color_scheme='Zenburn', language='Python') qtbot.addWidget(widget) return widget
def codeeditor_factory(): editor = CodeEditor(parent=None) editor.setup_editor(language='Python', tab_mode=False, markers=True, close_quotes=True, close_parentheses=True, color_scheme='spyder/dark', font=QFont("Monospace", 10)) editor.resize(640, 480) return editor
def code_editor_indent_bot(qtbot): """ Setup CodeEditor with some text useful for folding related tests. """ editor = CodeEditor(parent=None) indent_chars = " " * 2 tab_stop_width_spaces = 4 language = "Python" editor.setup_editor(language=language, indent_chars=indent_chars, tab_stop_width_spaces=tab_stop_width_spaces) return editor, qtbot
def code_editor_bot(qtbot): """ Setup CodeEditor with some text useful for folding related tests. """ editor = CodeEditor(parent=None) indent_chars = " " * 4 tab_stop_width_spaces = 4 language = "Python" editor.setup_editor(language=language, indent_chars=indent_chars, tab_stop_width_spaces=tab_stop_width_spaces) return editor, qtbot
def construct_editor(qtbot): """Construct editor for testing decorations.""" editor = CodeEditor(parent=None) editor.setup_editor( language='Python', color_scheme='spyder/dark', font=QFont("Monospace", 10), ) editor.resize(640, 480) editor.show() qtbot.addWidget(editor) return editor
def codeeditor_factory(): editor = CodeEditor(parent=None) editor.setup_editor(language='Python', tab_mode=False, markers=True, close_quotes=True, close_parentheses=True, color_scheme='spyder/dark', font=QFont("Monospace", 10), automatic_completions=True, automatic_completions_after_chars=1, automatic_completions_after_ms=200) editor.resize(640, 480) return editor
def construct_editor(*args, **kwargs): """Construct editor with some text for testing extra selections.""" app = qapplication() editor = CodeEditor(parent=None) kwargs['language'] = 'Python' editor.setup_editor(*args, **kwargs) text = ("def some_function():\n" " some_variable = 1\n" " some_variable += 2\n" " return some_variable\n" "# %%") editor.set_text(text) return editor
def codeeditor(qtbot): widget = CodeEditor(None) widget.setup_editor(linenumbers=True, markers=True, tab_mode=False, font=QFont("Courier New", 10), show_blanks=True, color_scheme='spyder/dark', scroll_past_end=True) widget.setup_editor(language='Python') widget.resize(640, 480) widget.show() yield widget widget.close()
def lsp_codeeditor(qtbot): """CodeEditor instance with LSP services activated.""" # Activate pycodestyle and pydocstyle CONF.set('lsp-server', 'pycodestyle', True) CONF.set('lsp-server', 'pydocstyle', True) # Tell CodeEditor to use introspection os.environ['SPY_TEST_USE_INTROSPECTION'] = 'True' # Create an LSPManager instance to be able to start an LSP client lsp_manager = LSPManager(parent=None) # Create a CodeEditor instance editor = CodeEditor(parent=None) editor.setup_editor(language='Python', tab_mode=False, markers=True, color_scheme='spyder/dark', font=QFont("Monospace", 10)) editor.resize(640, 480) qtbot.addWidget(editor) editor.show() # Redirect editor LSP requests to lsp_manager editor.sig_perform_lsp_request.connect(lsp_manager.send_request) # Create wrapper lsp_wrapper = LSPWrapper(editor, lsp_manager) # Start LSP Python client with qtbot.waitSignal(lsp_wrapper.sig_lsp_services_started, timeout=30000): editor.filename = 'test.py' editor.language = 'Python' lsp_manager.start_client('python') python_client = lsp_manager.clients['python']['instance'] python_client.sig_initialize.connect(lsp_wrapper.start_lsp_services) # Send a textDocument/didOpen request to the server with qtbot.waitSignal(editor.lsp_response_signal, timeout=30000): editor.document_did_open() yield editor # Tear down operations os.environ['SPY_TEST_USE_INTROSPECTION'] = 'False' CONF.set('lsp-server', 'pycodestyle', False) CONF.set('lsp-server', 'pydocstyle', False) lsp_manager.shutdown()
def editor_outline_explorer_bot(): """setup editor and outline_explorer.""" app = qapplication() editor = CodeEditor(parent=None) editor.setup_editor(language='Python') outlineexplorer = OutlineExplorerWidget(editor) editor.set_text(text) editor.oe_proxy = OutlineExplorerProxyEditor(editor, "test.py") outlineexplorer.set_current_editor(editor.oe_proxy, update=False, clear=False) outlineexplorer.setEnabled(True) return editor, outlineexplorer, editor.oe_proxy
class TestPropertiesWidget(QWidget): def __init__(self, parent): QWidget.__init__(self, parent) font = QFont(get_family(MONOSPACE), 10, QFont.Normal) info_icon = QLabel() icon = get_std_icon("MessageBoxInformation").pixmap(24, 24) info_icon.setPixmap(icon) info_icon.setFixedWidth(32) info_icon.setAlignment(Qt.AlignTop) self.desc_label = QLabel() self.desc_label.setWordWrap(True) self.desc_label.setAlignment(Qt.AlignTop) self.desc_label.setFont(font) group_desc = QGroupBox(_("Description"), self) layout = QHBoxLayout() layout.addWidget(info_icon) layout.addWidget(self.desc_label) group_desc.setLayout(layout) self.editor = CodeEditor(self) self.editor.setup_editor(linenumbers=True, font=font) self.editor.setReadOnly(True) group_code = QGroupBox(_("Source code"), self) layout = QVBoxLayout() layout.addWidget(self.editor) group_code.setLayout(layout) self.run_button = QPushButton(get_icon("apply.png"), _("Run this script"), self) self.quit_button = QPushButton(get_icon("exit.png"), _("Quit"), self) hlayout = QHBoxLayout() hlayout.addWidget(self.run_button) hlayout.addStretch() hlayout.addWidget(self.quit_button) vlayout = QVBoxLayout() vlayout.addWidget(group_desc) vlayout.addWidget(group_code) vlayout.addLayout(hlayout) self.setLayout(vlayout) def set_item(self, test): self.desc_label.setText(test.get_description()) self.editor.set_text_from_file(test.filename)
def get_indent_fix(text, indent_chars=" " * 4, tab_stop_width_spaces=4, sol=False, forward=True, language='Python'): """Return text with last line's indentation fixed.""" app = qapplication() editor = CodeEditor(parent=None) editor.setup_editor(language=language, indent_chars=indent_chars, tab_stop_width_spaces=tab_stop_width_spaces) editor.set_text(text) cursor = editor.textCursor() cursor.movePosition(QTextCursor.End) if sol: lines = text.splitlines(True) repeat = len(lines[-1].lstrip()) cursor.movePosition(QTextCursor.Left, n=repeat) editor.setTextCursor(cursor) editor.fix_indent(forward=forward) return to_text_string(editor.toPlainText())
def get_fold_levels(): """setup editor and return fold levels.""" app = qapplication() editor = CodeEditor(parent=None) editor.setup_editor(language='Python') text = ( '# dummy test file\n' 'class a():\n' # fold-block level-0 ' self.b = 1\n' ' print(self.b)\n' ' \n' ' def some_method(self):\n' # fold-block level-1 ' self.b = 3\n' '\n' ' def other_method(self):\n' # fold-block level-1 '\n' # a blank line (should be ignored) ' # a comment with arbitrary indentation\n' # should be ignored ' a = (1,\n' # fold-block level-2 ' 2,\n' ' 3)\n') editor.set_text(text) return print_tree(editor, return_list=True)
def get_fold_levels(): """setup editor and return fold levels.""" app = qapplication() editor = CodeEditor(parent=None) editor.setup_editor(language='Python') text = ('# dummy test file\n' 'class a():\n' # fold-block level-0 ' self.b = 1\n' ' print(self.b)\n' ' \n' ' def some_method(self):\n' # fold-block level-1 ' self.b = 3\n' '\n' ' def other_method(self):\n' # fold-block level-1 '\n' # a blank line (should be ignored) ' # a comment with arbitrary indentation\n' # should be ignored ' a = (1,\n' # fold-block level-2 ' 2,\n' ' 3)\n' ) editor.set_text(text) return print_tree(editor, return_list=True)
def construct_editor(*args, **kwargs): app = qapplication() editor = CodeEditor(parent=None) kwargs['language'] = 'Python' editor.setup_editor(*args, **kwargs) return editor
def test_update_decorations_when_scrolling(qtbot): """ Test how many calls we're doing to update decorations when scrolling. """ # NOTE: Here we need to use `patch` from unittest.mock, instead of the # mocker fixture, to have the same results when running the test # alone and with the other tests in this file. patched_object = ('spyder.plugins.editor.utils.decoration.' 'TextDecorationsManager._update') with patch(patched_object) as _update: # NOTE: We can't use a fixture to build a CodeEditor instance here # because the testing results are not consistent. editor = CodeEditor(parent=None) editor.setup_editor( language='Python', color_scheme='spyder/dark', font=QFont("Monospace", 10), ) editor.resize(640, 480) editor.show() qtbot.addWidget(editor) # If there's no waiting after CodeEditor is created, there shouldn't # be a call to _update. assert _update.call_count == 0 with open(osp.join(PARENT, 'codeeditor.py'), 'r') as f: text = f.read() editor.set_text(text) # If there's no waiting after setting text, there shouldn't be a # call to _update either. assert _update.call_count == 0 # Simulate scrolling scrollbar = editor.verticalScrollBar() for i in range(6): scrollbar.setValue(i * 70) qtbot.wait(100) # A new call is done here due to __cursor_position_changed being # called, which in turn calls highlight_current_cell and # highlight_current_line assert _update.call_count == 1 # Wait for decorations to update qtbot.wait(editor.UPDATE_DECORATIONS_TIMEOUT + 100) # Assert a new call to _update was done assert _update.call_count == 2 # Simulate grabbing and moving the scrollbar with the mouse scrollbar = editor.verticalScrollBar() value = scrollbar.value() for __ in range(400): scrollbar.setValue(value + 1) value = scrollbar.value() # No calls should be done after this. assert _update.call_count == 2 # Wait for decorations to update qtbot.wait(editor.UPDATE_DECORATIONS_TIMEOUT + 100) # Assert a new call to _update was done assert _update.call_count == 3 # Move to the last visible line _, last = editor.get_visible_block_numbers() editor.go_to_line(last) # Simulate continuously pressing the down arrow key. for __ in range(200): qtbot.keyPress(editor, Qt.Key_Down) if sys.platform.startswith('linux'): qtbot.wait(5) # Only one call to _update should be done, after releasing the key. qtbot.wait(editor.UPDATE_DECORATIONS_TIMEOUT + 100) assert _update.call_count == 4 # Simulate continuously pressing the up arrow key. for __ in range(200): qtbot.keyPress(editor, Qt.Key_Up) if sys.platform.startswith('linux'): qtbot.wait(5) # Only one call to _update should be done, after releasing the key. qtbot.wait(editor.UPDATE_DECORATIONS_TIMEOUT + 100) assert _update.call_count == 5
class AppearanceConfigPage(GeneralConfigPage): CONF_SECTION = "appearance" NAME = _("Appearance") def setup_page(self): self.ICON = ima.icon('eyedropper') names = self.get_option("names") try: names.pop(names.index(u'Custom')) except ValueError: pass custom_names = self.get_option("custom_names", []) # Interface options theme_group = QGroupBox(_("Main interface")) # Interface Widgets ui_themes = ['Automatic', 'Light', 'Dark'] ui_theme_choices = list(zip(ui_themes, [ui_theme.lower() for ui_theme in ui_themes])) ui_theme_combo = self.create_combobox(_('Interface theme'), ui_theme_choices, 'ui_theme', restart=True) styles = [str(txt) for txt in list(QStyleFactory.keys())] # Don't offer users the possibility to change to a different # style in Gtk-based desktops # Fixes Issue 2036 if is_gtk_desktop() and ('GTK+' in styles): styles = ['GTK+'] choices = list(zip(styles, [style.lower() for style in styles])) style_combo = self.create_combobox(_('Qt windows style'), choices, 'windows_style', default=self.main.default_style) self.style_combobox = style_combo.combobox themes = ['Spyder 2', 'Spyder 3'] icon_choices = list(zip(themes, [theme.lower() for theme in themes])) icons_combo = self.create_combobox(_('Icon theme'), icon_choices, 'icon_theme', restart=True) theme_comboboxes_layout = QGridLayout() theme_comboboxes_layout.addWidget(ui_theme_combo.label, 0, 0) theme_comboboxes_layout.addWidget(ui_theme_combo.combobox, 0, 1) theme_comboboxes_layout.addWidget(style_combo.label, 1, 0) theme_comboboxes_layout.addWidget(self.style_combobox, 1, 1) theme_comboboxes_layout.addWidget(icons_combo.label, 2, 0) theme_comboboxes_layout.addWidget(icons_combo.combobox, 2, 1) theme_layout = QVBoxLayout() theme_layout.addLayout(theme_comboboxes_layout) theme_group.setLayout(theme_layout) # Syntax coloring options syntax_group = QGroupBox(_("Syntax highlighting theme")) # Syntax Widgets edit_button = QPushButton(_("Edit selected scheme")) create_button = QPushButton(_("Create new scheme")) self.delete_button = QPushButton(_("Delete scheme")) self.reset_button = QPushButton(_("Reset to defaults")) self.preview_editor = CodeEditor(self) self.stacked_widget = QStackedWidget(self) self.scheme_editor_dialog = SchemeEditor(parent=self, stack=self.stacked_widget) self.scheme_choices_dict = {} schemes_combobox_widget = self.create_combobox('', [('', '')], 'selected') self.schemes_combobox = schemes_combobox_widget.combobox # Syntax layout syntax_layout = QGridLayout(syntax_group) btns = [self.schemes_combobox, edit_button, self.reset_button, create_button, self.delete_button] for i, btn in enumerate(btns): syntax_layout.addWidget(btn, i, 1) syntax_layout.setColumnStretch(0, 1) syntax_layout.setColumnStretch(1, 2) syntax_layout.setColumnStretch(2, 1) syntax_layout.setContentsMargins(0, 12, 0, 12) # Fonts options fonts_group = QGroupBox(_("Fonts")) # Fonts widgets plain_text_font = self.create_fontgroup( option='font', title=_("Plain text"), fontfilters=QFontComboBox.MonospacedFonts, without_group=True) rich_text_font = self.create_fontgroup( option='rich_font', title=_("Rich text"), without_group=True) # Fonts layouts fonts_layout = QGridLayout() fonts_layout.addWidget(plain_text_font.fontlabel, 0, 0) fonts_layout.addWidget(plain_text_font.fontbox, 0, 1) fonts_layout.addWidget(plain_text_font.sizelabel, 0, 2) fonts_layout.addWidget(plain_text_font.sizebox, 0, 3) fonts_layout.addWidget(rich_text_font.fontlabel, 1, 0) fonts_layout.addWidget(rich_text_font.fontbox, 1, 1) fonts_layout.addWidget(rich_text_font.sizelabel, 1, 2) fonts_layout.addWidget(rich_text_font.sizebox, 1, 3) fonts_group.setLayout(fonts_layout) # Left options layout options_layout = QVBoxLayout() options_layout.addWidget(theme_group) options_layout.addWidget(syntax_group) options_layout.addWidget(fonts_group) # Right preview layout preview_group = QGroupBox(_("Preview")) preview_layout = QVBoxLayout() preview_layout.addWidget(self.preview_editor) preview_group.setLayout(preview_layout) # Combined layout combined_layout = QGridLayout() combined_layout.setRowStretch(0, 1) combined_layout.setColumnStretch(1, 100) combined_layout.addLayout(options_layout, 0, 0) combined_layout.addWidget(preview_group, 0, 1) self.setLayout(combined_layout) # Signals and slots create_button.clicked.connect(self.create_new_scheme) edit_button.clicked.connect(self.edit_scheme) self.reset_button.clicked.connect(self.reset_to_default) self.delete_button.clicked.connect(self.delete_scheme) self.schemes_combobox.currentIndexChanged.connect(self.update_preview) self.schemes_combobox.currentIndexChanged.connect(self.update_buttons) # Setup for name in names: self.scheme_editor_dialog.add_color_scheme_stack(name) for name in custom_names: self.scheme_editor_dialog.add_color_scheme_stack(name, custom=True) self.update_combobox() self.update_preview() self.update_qt_style_combobox() def get_font(self, option): """Return global font used in Spyder.""" return get_font(option=option) def set_font(self, font, option): """Set global font used in Spyder.""" # Update fonts in all plugins set_font(font, option=option) plugins = self.main.widgetlist + self.main.thirdparty_plugins for plugin in plugins: plugin.update_font() def apply_settings(self, options): self.set_option('selected', self.current_scheme) color_scheme = self.get_option('selected') ui_theme = self.get_option('ui_theme') style_sheet = self.main.styleSheet() if ui_theme == 'automatic': if ((not is_dark_font_color(color_scheme) and not style_sheet) or (is_dark_font_color(color_scheme) and style_sheet)): self.changed_options.add('ui_theme') elif 'ui_theme' in self.changed_options: self.changed_options.remove('ui_theme') if 'ui_theme' not in self.changed_options: self.main.editor.apply_plugin_settings(['color_scheme_name']) if self.main.ipyconsole is not None: self.main.ipyconsole.apply_plugin_settings( ['color_scheme_name']) if self.main.historylog is not None: self.main.historylog.apply_plugin_settings( ['color_scheme_name']) if self.main.help is not None: self.main.help.apply_plugin_settings(['color_scheme_name']) self.update_combobox() self.update_preview() else: if 'ui_theme' in self.changed_options: if (style_sheet and ui_theme == 'dark' or not style_sheet and ui_theme == 'light'): self.changed_options.remove('ui_theme') if 'ui_theme' not in self.changed_options: self.main.editor.apply_plugin_settings(['color_scheme_name']) if self.main.ipyconsole is not None: self.main.ipyconsole.apply_plugin_settings( ['color_scheme_name']) if self.main.historylog is not None: self.main.historylog.apply_plugin_settings( ['color_scheme_name']) if self.main.help is not None: self.main.help.apply_plugin_settings(['color_scheme_name']) self.update_combobox() self.update_preview() self.main.apply_settings() # Helpers # ------------------------------------------------------------------------- @property def current_scheme_name(self): return self.schemes_combobox.currentText() @property def current_scheme(self): return self.scheme_choices_dict[self.current_scheme_name] @property def current_scheme_index(self): return self.schemes_combobox.currentIndex() def update_qt_style_combobox(self): """Enable/disable the Qt style combobox.""" if is_dark_interface(): self.style_combobox.setEnabled(False) else: self.style_combobox.setEnabled(True) def update_combobox(self): """Recreates the combobox contents.""" index = self.current_scheme_index self.schemes_combobox.blockSignals(True) names = self.get_option("names") try: names.pop(names.index(u'Custom')) except ValueError: pass custom_names = self.get_option("custom_names", []) # Useful for retrieving the actual data for n in names + custom_names: self.scheme_choices_dict[self.get_option('{0}/name'.format(n))] = n if custom_names: choices = names + [None] + custom_names else: choices = names combobox = self.schemes_combobox combobox.clear() for name in choices: if name is None: continue combobox.addItem(self.get_option('{0}/name'.format(name)), name) if custom_names: combobox.insertSeparator(len(names)) self.schemes_combobox.blockSignals(False) self.schemes_combobox.setCurrentIndex(index) def update_buttons(self): """Updates the enable status of delete and reset buttons.""" current_scheme = self.current_scheme names = self.get_option("names") try: names.pop(names.index(u'Custom')) except ValueError: pass delete_enabled = current_scheme not in names self.delete_button.setEnabled(delete_enabled) self.reset_button.setEnabled(not delete_enabled) def update_preview(self, index=None, scheme_name=None): """ Update the color scheme of the preview editor and adds text. Note ---- 'index' is needed, because this is triggered by a signal that sends the selected index. """ text = ('"""A string"""\n\n' '# A comment\n\n' '# %% A cell\n\n' 'class Foo(object):\n' ' def __init__(self):\n' ' bar = 42\n' ' print(bar)\n' ) show_blanks = CONF.get('editor', 'blank_spaces') update_scrollbar = CONF.get('editor', 'scroll_past_end') if scheme_name is None: scheme_name = self.current_scheme self.preview_editor.setup_editor(linenumbers=True, markers=True, tab_mode=False, font=get_font(), show_blanks=show_blanks, color_scheme=scheme_name, scroll_past_end=update_scrollbar) self.preview_editor.set_text(text) self.preview_editor.set_language('Python') # Actions # ------------------------------------------------------------------------- def create_new_scheme(self): """Creates a new color scheme with a custom name.""" names = self.get_option('names') custom_names = self.get_option('custom_names', []) # Get the available number this new color scheme counter = len(custom_names) - 1 custom_index = [int(n.split('-')[-1]) for n in custom_names] for i in range(len(custom_names)): if custom_index[i] != i: counter = i - 1 break custom_name = "custom-{0}".format(counter+1) # Add the config settings, based on the current one. custom_names.append(custom_name) self.set_option('custom_names', custom_names) for key in syntaxhighlighters.COLOR_SCHEME_KEYS: name = "{0}/{1}".format(custom_name, key) default_name = "{0}/{1}".format(self.current_scheme, key) option = self.get_option(default_name) self.set_option(name, option) self.set_option('{0}/name'.format(custom_name), custom_name) # Now they need to be loaded! how to make a partial load_from_conf? dlg = self.scheme_editor_dialog dlg.add_color_scheme_stack(custom_name, custom=True) dlg.set_scheme(custom_name) self.load_from_conf() if dlg.exec_(): # This is needed to have the custom name updated on the combobox name = dlg.get_scheme_name() self.set_option('{0}/name'.format(custom_name), name) # The +1 is needed because of the separator in the combobox index = (names + custom_names).index(custom_name) + 1 self.update_combobox() self.schemes_combobox.setCurrentIndex(index) else: # Delete the config .... custom_names.remove(custom_name) self.set_option('custom_names', custom_names) dlg.delete_color_scheme_stack(custom_name) def edit_scheme(self): """Edit current scheme.""" dlg = self.scheme_editor_dialog dlg.set_scheme(self.current_scheme) if dlg.exec_(): # Update temp scheme to reflect instant edits on the preview temporal_color_scheme = dlg.get_edited_color_scheme() for key in temporal_color_scheme: option = "temp/{0}".format(key) value = temporal_color_scheme[key] self.set_option(option, value) self.update_preview(scheme_name='temp') def delete_scheme(self): """Deletes the currently selected custom color scheme.""" scheme_name = self.current_scheme answer = QMessageBox.warning(self, _("Warning"), _("Are you sure you want to delete " "this scheme?"), QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.Yes: # Put the combobox in Spyder by default, when deleting a scheme names = self.get_option('names') self.set_scheme('spyder') self.schemes_combobox.setCurrentIndex(names.index('spyder')) self.set_option('selected', 'spyder') # Delete from custom_names custom_names = self.get_option('custom_names', []) if scheme_name in custom_names: custom_names.remove(scheme_name) self.set_option('custom_names', custom_names) # Delete config options for key in syntaxhighlighters.COLOR_SCHEME_KEYS: option = "{0}/{1}".format(scheme_name, key) CONF.remove_option(self.CONF_SECTION, option) CONF.remove_option(self.CONF_SECTION, "{0}/name".format(scheme_name)) self.update_combobox() self.update_preview() def set_scheme(self, scheme_name): """ Set the current stack in the dialog to the scheme with 'scheme_name'. """ dlg = self.scheme_editor_dialog dlg.set_scheme(scheme_name) @Slot() def reset_to_default(self): """Restore initial values for default color schemes.""" # Checks that this is indeed a default scheme scheme = self.current_scheme names = self.get_option('names') if scheme in names: for key in syntaxhighlighters.COLOR_SCHEME_KEYS: option = "{0}/{1}".format(scheme, key) value = CONF.get_default(self.CONF_SECTION, option) self.set_option(option, value) self.load_from_conf()
class AppearanceConfigPage(GeneralConfigPage): CONF_SECTION = "appearance" NAME = _("Appearance") def setup_page(self): self.ICON = ima.icon('eyedropper') names = self.get_option("names") try: names.pop(names.index(u'Custom')) except ValueError: pass custom_names = self.get_option("custom_names", []) # Interface options theme_group = QGroupBox(_("Main interface")) # Interface Widgets ui_themes = ['Automatic', 'Light', 'Dark'] ui_theme_choices = list( zip(ui_themes, [ui_theme.lower() for ui_theme in ui_themes])) ui_theme_combo = self.create_combobox(_('Interface theme'), ui_theme_choices, 'ui_theme', restart=True) styles = [str(txt) for txt in list(QStyleFactory.keys())] # Don't offer users the possibility to change to a different # style in Gtk-based desktops # See spyder-ide/spyder#2036. if is_gtk_desktop() and ('GTK+' in styles): styles = ['GTK+'] choices = list(zip(styles, [style.lower() for style in styles])) style_combo = self.create_combobox(_('Qt windows style'), choices, 'windows_style', default=self.main.default_style) self.style_combobox = style_combo.combobox themes = ['Spyder 2', 'Spyder 3'] icon_choices = list(zip(themes, [theme.lower() for theme in themes])) icons_combo = self.create_combobox(_('Icon theme'), icon_choices, 'icon_theme', restart=True) theme_comboboxes_layout = QGridLayout() theme_comboboxes_layout.addWidget(ui_theme_combo.label, 0, 0) theme_comboboxes_layout.addWidget(ui_theme_combo.combobox, 0, 1) theme_comboboxes_layout.addWidget(style_combo.label, 1, 0) theme_comboboxes_layout.addWidget(self.style_combobox, 1, 1) theme_comboboxes_layout.addWidget(icons_combo.label, 2, 0) theme_comboboxes_layout.addWidget(icons_combo.combobox, 2, 1) theme_layout = QVBoxLayout() theme_layout.addLayout(theme_comboboxes_layout) theme_group.setLayout(theme_layout) # Syntax coloring options syntax_group = QGroupBox(_("Syntax highlighting theme")) # Syntax Widgets edit_button = QPushButton(_("Edit selected scheme")) create_button = QPushButton(_("Create new scheme")) self.delete_button = QPushButton(_("Delete scheme")) self.reset_button = QPushButton(_("Reset to defaults")) self.preview_editor = CodeEditor(self) self.stacked_widget = QStackedWidget(self) self.scheme_editor_dialog = SchemeEditor(parent=self, stack=self.stacked_widget) self.scheme_choices_dict = {} schemes_combobox_widget = self.create_combobox('', [('', '')], 'selected') self.schemes_combobox = schemes_combobox_widget.combobox # Syntax layout syntax_layout = QGridLayout(syntax_group) btns = [ self.schemes_combobox, edit_button, self.reset_button, create_button, self.delete_button ] for i, btn in enumerate(btns): syntax_layout.addWidget(btn, i, 1) syntax_layout.setColumnStretch(0, 1) syntax_layout.setColumnStretch(1, 2) syntax_layout.setColumnStretch(2, 1) syntax_layout.setContentsMargins(0, 12, 0, 12) # Fonts options fonts_group = QGroupBox(_("Fonts")) # Fonts widgets plain_text_font = self.create_fontgroup( option='font', title=_("Plain text"), fontfilters=QFontComboBox.MonospacedFonts, without_group=True) rich_text_font = self.create_fontgroup(option='rich_font', title=_("Rich text"), without_group=True) # Fonts layouts fonts_layout = QGridLayout(fonts_group) fonts_layout.addWidget(plain_text_font.fontlabel, 0, 0) fonts_layout.addWidget(plain_text_font.fontbox, 0, 1) fonts_layout.addWidget(plain_text_font.sizelabel, 0, 2) fonts_layout.addWidget(plain_text_font.sizebox, 0, 3) fonts_layout.addWidget(rich_text_font.fontlabel, 1, 0) fonts_layout.addWidget(rich_text_font.fontbox, 1, 1) fonts_layout.addWidget(rich_text_font.sizelabel, 1, 2) fonts_layout.addWidget(rich_text_font.sizebox, 1, 3) fonts_layout.setRowStretch(fonts_layout.rowCount(), 1) # Left options layout options_layout = QVBoxLayout() options_layout.addWidget(theme_group) options_layout.addWidget(syntax_group) options_layout.addWidget(fonts_group) # Right preview layout preview_group = QGroupBox(_("Preview")) preview_layout = QVBoxLayout() preview_layout.addWidget(self.preview_editor) preview_group.setLayout(preview_layout) # Combined layout combined_layout = QGridLayout() combined_layout.setRowStretch(0, 1) combined_layout.setColumnStretch(1, 100) combined_layout.addLayout(options_layout, 0, 0) combined_layout.addWidget(preview_group, 0, 1) self.setLayout(combined_layout) # Signals and slots create_button.clicked.connect(self.create_new_scheme) edit_button.clicked.connect(self.edit_scheme) self.reset_button.clicked.connect(self.reset_to_default) self.delete_button.clicked.connect(self.delete_scheme) self.schemes_combobox.currentIndexChanged.connect(self.update_preview) self.schemes_combobox.currentIndexChanged.connect(self.update_buttons) # Setup for name in names: self.scheme_editor_dialog.add_color_scheme_stack(name) for name in custom_names: self.scheme_editor_dialog.add_color_scheme_stack(name, custom=True) self.update_combobox() self.update_preview() self.update_qt_style_combobox() def get_font(self, option): """Return global font used in Spyder.""" return get_font(option=option) def set_font(self, font, option): """Set global font used in Spyder.""" # Update fonts in all plugins set_font(font, option=option) plugins = self.main.widgetlist + self.main.thirdparty_plugins for plugin in plugins: plugin.update_font() def apply_settings(self, options): self.set_option('selected', self.current_scheme) color_scheme = self.get_option('selected') ui_theme = self.get_option('ui_theme') style_sheet = self.main.styleSheet() if ui_theme == 'automatic': if ((not is_dark_font_color(color_scheme) and not style_sheet) or (is_dark_font_color(color_scheme) and style_sheet)): self.changed_options.add('ui_theme') elif 'ui_theme' in self.changed_options: self.changed_options.remove('ui_theme') if 'ui_theme' not in self.changed_options: self.main.editor.apply_plugin_settings(['color_scheme_name']) if self.main.ipyconsole is not None: self.main.ipyconsole.apply_plugin_settings( ['color_scheme_name']) if self.main.historylog is not None: self.main.historylog.apply_plugin_settings( ['color_scheme_name']) if self.main.help is not None: self.main.help.apply_plugin_settings(['color_scheme_name']) self.update_combobox() self.update_preview() else: if 'ui_theme' in self.changed_options: if (style_sheet and ui_theme == 'dark' or not style_sheet and ui_theme == 'light'): self.changed_options.remove('ui_theme') if 'ui_theme' not in self.changed_options: self.main.editor.apply_plugin_settings(['color_scheme_name']) if self.main.ipyconsole is not None: self.main.ipyconsole.apply_plugin_settings( ['color_scheme_name']) if self.main.historylog is not None: self.main.historylog.apply_plugin_settings( ['color_scheme_name']) if self.main.help is not None: self.main.help.apply_plugin_settings(['color_scheme_name']) self.update_combobox() self.update_preview() self.main.apply_settings() # Helpers # ------------------------------------------------------------------------- @property def current_scheme_name(self): return self.schemes_combobox.currentText() @property def current_scheme(self): return self.scheme_choices_dict[self.current_scheme_name] @property def current_scheme_index(self): return self.schemes_combobox.currentIndex() def update_qt_style_combobox(self): """Enable/disable the Qt style combobox.""" if is_dark_interface(): self.style_combobox.setEnabled(False) else: self.style_combobox.setEnabled(True) def update_combobox(self): """Recreates the combobox contents.""" index = self.current_scheme_index self.schemes_combobox.blockSignals(True) names = self.get_option("names") try: names.pop(names.index(u'Custom')) except ValueError: pass custom_names = self.get_option("custom_names", []) # Useful for retrieving the actual data for n in names + custom_names: self.scheme_choices_dict[self.get_option('{0}/name'.format(n))] = n if custom_names: choices = names + [None] + custom_names else: choices = names combobox = self.schemes_combobox combobox.clear() for name in choices: if name is None: continue combobox.addItem(self.get_option('{0}/name'.format(name)), name) if custom_names: combobox.insertSeparator(len(names)) self.schemes_combobox.blockSignals(False) self.schemes_combobox.setCurrentIndex(index) def update_buttons(self): """Updates the enable status of delete and reset buttons.""" current_scheme = self.current_scheme names = self.get_option("names") try: names.pop(names.index(u'Custom')) except ValueError: pass delete_enabled = current_scheme not in names self.delete_button.setEnabled(delete_enabled) self.reset_button.setEnabled(not delete_enabled) def update_preview(self, index=None, scheme_name=None): """ Update the color scheme of the preview editor and adds text. Note ---- 'index' is needed, because this is triggered by a signal that sends the selected index. """ text = ('"""A string"""\n\n' '# A comment\n\n' '# %% A cell\n\n' 'class Foo(object):\n' ' def __init__(self):\n' ' bar = 42\n' ' print(bar)\n') show_blanks = CONF.get('editor', 'blank_spaces') update_scrollbar = CONF.get('editor', 'scroll_past_end') underline_errors = CONF.get('editor', 'underline_errors') if scheme_name is None: scheme_name = self.current_scheme self.preview_editor.setup_editor(linenumbers=True, markers=True, tab_mode=False, font=get_font(), show_blanks=show_blanks, underline_errors=underline_errors, color_scheme=scheme_name, scroll_past_end=update_scrollbar) self.preview_editor.set_text(text) self.preview_editor.set_language('Python') # Actions # ------------------------------------------------------------------------- def create_new_scheme(self): """Creates a new color scheme with a custom name.""" names = self.get_option('names') custom_names = self.get_option('custom_names', []) # Get the available number this new color scheme counter = len(custom_names) - 1 custom_index = [int(n.split('-')[-1]) for n in custom_names] for i in range(len(custom_names)): if custom_index[i] != i: counter = i - 1 break custom_name = "custom-{0}".format(counter + 1) # Add the config settings, based on the current one. custom_names.append(custom_name) self.set_option('custom_names', custom_names) for key in syntaxhighlighters.COLOR_SCHEME_KEYS: name = "{0}/{1}".format(custom_name, key) default_name = "{0}/{1}".format(self.current_scheme, key) option = self.get_option(default_name) self.set_option(name, option) self.set_option('{0}/name'.format(custom_name), custom_name) # Now they need to be loaded! how to make a partial load_from_conf? dlg = self.scheme_editor_dialog dlg.add_color_scheme_stack(custom_name, custom=True) dlg.set_scheme(custom_name) self.load_from_conf() if dlg.exec_(): # This is needed to have the custom name updated on the combobox name = dlg.get_scheme_name() self.set_option('{0}/name'.format(custom_name), name) # The +1 is needed because of the separator in the combobox index = (names + custom_names).index(custom_name) + 1 self.update_combobox() self.schemes_combobox.setCurrentIndex(index) else: # Delete the config .... custom_names.remove(custom_name) self.set_option('custom_names', custom_names) dlg.delete_color_scheme_stack(custom_name) def edit_scheme(self): """Edit current scheme.""" dlg = self.scheme_editor_dialog dlg.set_scheme(self.current_scheme) if dlg.exec_(): # Update temp scheme to reflect instant edits on the preview temporal_color_scheme = dlg.get_edited_color_scheme() for key in temporal_color_scheme: option = "temp/{0}".format(key) value = temporal_color_scheme[key] self.set_option(option, value) self.update_preview(scheme_name='temp') def delete_scheme(self): """Deletes the currently selected custom color scheme.""" scheme_name = self.current_scheme answer = QMessageBox.warning( self, _("Warning"), _("Are you sure you want to delete " "this scheme?"), QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.Yes: # Put the combobox in Spyder by default, when deleting a scheme names = self.get_option('names') self.set_scheme('spyder') self.schemes_combobox.setCurrentIndex(names.index('spyder')) self.set_option('selected', 'spyder') # Delete from custom_names custom_names = self.get_option('custom_names', []) if scheme_name in custom_names: custom_names.remove(scheme_name) self.set_option('custom_names', custom_names) # Delete config options for key in syntaxhighlighters.COLOR_SCHEME_KEYS: option = "{0}/{1}".format(scheme_name, key) CONF.remove_option(self.CONF_SECTION, option) CONF.remove_option(self.CONF_SECTION, "{0}/name".format(scheme_name)) self.update_combobox() self.update_preview() def set_scheme(self, scheme_name): """ Set the current stack in the dialog to the scheme with 'scheme_name'. """ dlg = self.scheme_editor_dialog dlg.set_scheme(scheme_name) @Slot() def reset_to_default(self): """Restore initial values for default color schemes.""" # Checks that this is indeed a default scheme scheme = self.current_scheme names = self.get_option('names') if scheme in names: for key in syntaxhighlighters.COLOR_SCHEME_KEYS: option = "{0}/{1}".format(scheme, key) value = CONF.get_default(self.CONF_SECTION, option) self.set_option(option, value) self.load_from_conf()
prev_text = '' text = block.text().strip() if text in self.open_chars: return TextBlockHelper.get_fold_lvl(prev_block) + 1 if prev_text.endswith(self.open_chars) and prev_text not in \ self.open_chars: return TextBlockHelper.get_fold_lvl(prev_block) + 1 if self.close_chars in prev_text: return TextBlockHelper.get_fold_lvl(prev_block) - 1 return TextBlockHelper.get_fold_lvl(prev_block) if __name__ == '__main__': """Print folding blocks of this file for debugging""" from spyder.plugins.editor.api.folding import print_tree from spyder.utils.qthelpers import qapplication from spyder.plugins.editor.widgets.codeeditor import CodeEditor if len(sys.argv) > 1: fname = sys.argv[1] else: fname = __file__ app = qapplication() editor = CodeEditor(parent=None) editor.setup_editor(language='Python') editor.set_text_from_file(fname) print_tree(editor)
class LSPServerEditor(QDialog): DEFAULT_HOST = '127.0.0.1' DEFAULT_PORT = 2084 DEFAULT_CMD = '' DEFAULT_ARGS = '' DEFAULT_CONFIGURATION = '{}' DEFAULT_EXTERNAL = False HOST_REGEX = re.compile(r'^\w+([.]\w+)*$') NON_EMPTY_REGEX = re.compile(r'^\S+$') JSON_VALID = _('JSON valid') JSON_INVALID = _('JSON invalid') def __init__(self, parent, language=None, cmd='', host='127.0.0.1', port=2084, args='', external=False, configurations={}, **kwargs): super(LSPServerEditor, self).__init__(parent) self.parent = parent self.external = external bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.button_ok = bbox.button(QDialogButtonBox.Ok) self.button_cancel = bbox.button(QDialogButtonBox.Cancel) self.button_ok.setEnabled(False) description = _('To create a new configuration, ' 'you need to select a programming ' 'language, along with a executable ' 'name for the server to execute ' '(If the instance is local), ' 'and the host and port. Finally, ' 'you need to provide the ' 'arguments that the server accepts. ' 'The placeholders <tt>%(host)s</tt> and ' '<tt>%(port)s</tt> refer to the host ' 'and the port, respectively.') server_settings_description = QLabel(description) server_settings_description.setWordWrap(True) lang_label = QLabel(_('Language:')) self.lang_cb = QComboBox(self) self.lang_cb.setToolTip(_('Programming language provided ' 'by the LSP server')) self.lang_cb.addItem(_('Select a language')) self.lang_cb.addItems(LSP_LANGUAGES) if language is not None: idx = LSP_LANGUAGES.index(language) self.lang_cb.setCurrentIndex(idx + 1) self.button_ok.setEnabled(True) host_label = QLabel(_('Host:')) self.host_input = QLineEdit(self) self.host_input.setToolTip(_('Name of the host that will provide ' 'access to the server')) self.host_input.setText(host) self.host_input.textChanged.connect(lambda x: self.validate()) port_label = QLabel(_('Port:')) self.port_spinner = QSpinBox(self) self.port_spinner.setToolTip(_('TCP port number of the server')) self.port_spinner.setMinimum(1) self.port_spinner.setMaximum(60000) self.port_spinner.setValue(port) cmd_label = QLabel(_('Command to execute:')) self.cmd_input = QLineEdit(self) self.cmd_input.setToolTip(_('Command used to start the ' 'LSP server locally')) self.cmd_input.setText(cmd) if not external: self.cmd_input.textChanged.connect(lambda x: self.validate()) args_label = QLabel(_('Server arguments:')) self.args_input = QLineEdit(self) self.args_input.setToolTip(_('Additional arguments required to ' 'start the server')) self.args_input.setText(args) conf_label = QLabel(_('LSP Server Configurations:')) self.conf_input = CodeEditor(None) self.conf_input.textChanged.connect(self.validate) color_scheme = CONF.get('color_schemes', 'selected') self.conf_input.setup_editor( language='JSON', color_scheme=color_scheme, wrap=False, edge_line=True, highlight_current_line=True, highlight_current_cell=True, occurrence_highlighting=True, auto_unindent=True, font=get_font(), filename='config.json') self.conf_input.setToolTip(_('Additional LSP server configurations ' 'set at runtime. JSON required')) conf_text = '{}' try: conf_text = json.dumps(configurations, indent=4, sort_keys=True) except Exception: pass self.conf_input.set_text(conf_text) self.json_label = QLabel(self.JSON_VALID, self) self.external_cb = QCheckBox(_('External server'), self) self.external_cb.setToolTip(_('Check if the server runs ' 'on a remote location')) self.external_cb.setChecked(external) self.external_cb.stateChanged.connect(self.set_local_options) hlayout = QHBoxLayout() general_vlayout = QVBoxLayout() general_vlayout.addWidget(server_settings_description) vlayout = QVBoxLayout() lang_layout = QVBoxLayout() lang_layout.addWidget(lang_label) lang_layout.addWidget(self.lang_cb) # layout2 = QHBoxLayout() # layout2.addLayout(lang_layout) lang_layout.addWidget(self.external_cb) vlayout.addLayout(lang_layout) host_layout = QVBoxLayout() host_layout.addWidget(host_label) host_layout.addWidget(self.host_input) port_layout = QVBoxLayout() port_layout.addWidget(port_label) port_layout.addWidget(self.port_spinner) conn_info_layout = QHBoxLayout() conn_info_layout.addLayout(host_layout) conn_info_layout.addLayout(port_layout) vlayout.addLayout(conn_info_layout) cmd_layout = QVBoxLayout() cmd_layout.addWidget(cmd_label) cmd_layout.addWidget(self.cmd_input) vlayout.addLayout(cmd_layout) args_layout = QVBoxLayout() args_layout.addWidget(args_label) args_layout.addWidget(self.args_input) vlayout.addLayout(args_layout) conf_layout = QVBoxLayout() conf_layout.addWidget(conf_label) conf_layout.addWidget(self.conf_input) conf_layout.addWidget(self.json_label) hlayout.addLayout(vlayout) hlayout.addLayout(conf_layout) general_vlayout.addLayout(hlayout) general_vlayout.addWidget(bbox) self.setLayout(general_vlayout) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) self.lang_cb.currentIndexChanged.connect( self.lang_selection_changed) self.form_status(False) if language is not None: self.form_status(True) self.validate() @Slot() def validate(self): host_text = self.host_input.text() cmd_text = self.cmd_input.text() if not self.HOST_REGEX.match(host_text): self.button_ok.setEnabled(False) self.host_input.setStyleSheet("QLineEdit{border: 1px solid red;}") self.host_input.setToolTip('Hostname must be valid') return else: self.host_input.setStyleSheet( "QLineEdit{border: 1px solid green;}") self.host_input.setToolTip('Hostname is valid') self.button_ok.setEnabled(True) if not self.external: if not self.NON_EMPTY_REGEX.match(cmd_text): self.button_ok.setEnabled(False) self.cmd_input.setStyleSheet( "QLineEdit{border: 1px solid red;}") self.cmd_input.setToolTip('Command must be non empty') return if find_program(cmd_text) is None: self.button_ok.setEnabled(False) self.cmd_input.setStyleSheet( "QLineEdit{border: 1px solid red;}") self.cmd_input.setToolTip('Program was not found ' 'on your system') return else: self.cmd_input.setStyleSheet( "QLineEdit{border: 1px solid green;}") self.cmd_input.setToolTip('Program was found on your system') self.button_ok.setEnabled(True) try: json.loads(self.conf_input.toPlainText()) try: self.json_label.setText(self.JSON_VALID) except: pass except (ValueError, json.decoder.JSONDecodeError): try: self.json_label.setText(self.JSON_INVALID) self.button_ok.setEnabled(False) except: pass def form_status(self, status): self.host_input.setEnabled(status) self.port_spinner.setEnabled(status) self.external_cb.setEnabled(status) self.cmd_input.setEnabled(status) self.args_input.setEnabled(status) self.conf_input.setEnabled(status) self.json_label.setVisible(status) @Slot() def lang_selection_changed(self): idx = self.lang_cb.currentIndex() if idx == 0: self.set_defaults() self.form_status(False) self.button_ok.setEnabled(False) else: server = self.parent.get_server_by_lang(LSP_LANGUAGES[idx - 1]) self.form_status(True) if server is not None: self.host_input.setText(server.host) self.port_spinner.setValue(server.port) self.external_cb.setChecked(server.external) self.cmd_input.setText(server.cmd) self.args_input.setText(server.args) self.conf_input.set_text(json.dumps(server.configurations)) self.json_label.setText(self.JSON_VALID) self.button_ok.setEnabled(True) else: self.set_defaults() def set_defaults(self): self.cmd_input.setStyleSheet('') self.host_input.setStyleSheet('') self.host_input.setText(self.DEFAULT_HOST) self.port_spinner.setValue(self.DEFAULT_PORT) self.external_cb.setChecked(self.DEFAULT_EXTERNAL) self.cmd_input.setText(self.DEFAULT_CMD) self.args_input.setText(self.DEFAULT_ARGS) self.conf_input.set_text(self.DEFAULT_CONFIGURATION) self.json_label.setText(self.JSON_VALID) @Slot(bool) @Slot(int) def set_local_options(self, enabled): self.external = enabled self.cmd_input.setEnabled(True) self.args_input.setEnabled(True) if enabled: self.cmd_input.setEnabled(False) self.cmd_input.setStyleSheet('') self.args_input.setEnabled(False) try: self.validate() except: pass def get_options(self): language_idx = self.lang_cb.currentIndex() language = LSP_LANGUAGES[language_idx - 1] host = self.host_input.text() port = int(self.port_spinner.value()) external = self.external_cb.isChecked() args = self.args_input.text() cmd = self.cmd_input.text() configurations = json.loads(self.conf_input.toPlainText()) server = LSPServer(language=language.lower(), cmd=cmd, args=args, host=host, port=port, external=external, configurations=configurations) return server
class LSPServerEditor(QDialog): DEFAULT_HOST = '127.0.0.1' DEFAULT_PORT = 2084 DEFAULT_CMD = '' DEFAULT_ARGS = '' DEFAULT_CONFIGURATION = '{}' DEFAULT_EXTERNAL = False HOST_REGEX = re.compile(r'^\w+([.]\w+)*$') NON_EMPTY_REGEX = re.compile(r'^\S+$') JSON_VALID = _('JSON valid') JSON_INVALID = _('JSON invalid') def __init__(self, parent, language=None, cmd='', host='127.0.0.1', port=2084, args='', external=False, configurations={}, **kwargs): super(LSPServerEditor, self).__init__(parent) self.parent = parent self.external = external bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.button_ok = bbox.button(QDialogButtonBox.Ok) self.button_cancel = bbox.button(QDialogButtonBox.Cancel) self.button_ok.setEnabled(False) description = _('To create a new configuration, ' 'you need to select a programming ' 'language, along with a executable ' 'name for the server to execute ' '(If the instance is local), ' 'and the host and port. Finally, ' 'you need to provide the ' 'arguments that the server accepts. ' 'The placeholders <tt>%(host)s</tt> and ' '<tt>%(port)s</tt> refer to the host ' 'and the port, respectively.') server_settings_description = QLabel(description) server_settings_description.setWordWrap(True) lang_label = QLabel(_('Language:')) self.lang_cb = QComboBox(self) self.lang_cb.setToolTip(_('Programming language provided ' 'by the LSP server')) self.lang_cb.addItem(_('Select a language')) self.lang_cb.addItems(LSP_LANGUAGES) if language is not None: idx = LSP_LANGUAGES.index(language) self.lang_cb.setCurrentIndex(idx + 1) self.button_ok.setEnabled(True) host_label = QLabel(_('Host:')) self.host_input = QLineEdit(self) self.host_input.setToolTip(_('Name of the host that will provide ' 'access to the server')) self.host_input.setText(host) self.host_input.textChanged.connect(lambda x: self.validate()) port_label = QLabel(_('Port:')) self.port_spinner = QSpinBox(self) self.port_spinner.setToolTip(_('TCP port number of the server')) self.port_spinner.setMinimum(1) self.port_spinner.setMaximum(60000) self.port_spinner.setValue(port) cmd_label = QLabel(_('Command to execute:')) self.cmd_input = QLineEdit(self) self.cmd_input.setToolTip(_('Command used to start the ' 'LSP server locally')) self.cmd_input.setText(cmd) if not external: self.cmd_input.textChanged.connect(lambda x: self.validate()) args_label = QLabel(_('Server arguments:')) self.args_input = QLineEdit(self) self.args_input.setToolTip(_('Additional arguments required to ' 'start the server')) self.args_input.setText(args) conf_label = QLabel(_('LSP Server Configurations:')) self.conf_input = CodeEditor(None) self.conf_input.textChanged.connect(self.validate) color_scheme = CONF.get('appearance', 'selected') self.conf_input.setup_editor( language='JSON', color_scheme=color_scheme, wrap=False, edge_line=True, highlight_current_line=True, highlight_current_cell=True, occurrence_highlighting=True, auto_unindent=True, font=get_font(), filename='config.json') self.conf_input.setToolTip(_('Additional LSP server configurations ' 'set at runtime. JSON required')) conf_text = '{}' try: conf_text = json.dumps(configurations, indent=4, sort_keys=True) except Exception: pass self.conf_input.set_text(conf_text) self.json_label = QLabel(self.JSON_VALID, self) self.external_cb = QCheckBox(_('External server'), self) self.external_cb.setToolTip(_('Check if the server runs ' 'on a remote location')) self.external_cb.setChecked(external) self.external_cb.stateChanged.connect(self.set_local_options) hlayout = QHBoxLayout() general_vlayout = QVBoxLayout() general_vlayout.addWidget(server_settings_description) vlayout = QVBoxLayout() lang_layout = QVBoxLayout() lang_layout.addWidget(lang_label) lang_layout.addWidget(self.lang_cb) # layout2 = QHBoxLayout() # layout2.addLayout(lang_layout) lang_layout.addWidget(self.external_cb) vlayout.addLayout(lang_layout) host_layout = QVBoxLayout() host_layout.addWidget(host_label) host_layout.addWidget(self.host_input) port_layout = QVBoxLayout() port_layout.addWidget(port_label) port_layout.addWidget(self.port_spinner) conn_info_layout = QHBoxLayout() conn_info_layout.addLayout(host_layout) conn_info_layout.addLayout(port_layout) vlayout.addLayout(conn_info_layout) cmd_layout = QVBoxLayout() cmd_layout.addWidget(cmd_label) cmd_layout.addWidget(self.cmd_input) vlayout.addLayout(cmd_layout) args_layout = QVBoxLayout() args_layout.addWidget(args_label) args_layout.addWidget(self.args_input) vlayout.addLayout(args_layout) conf_layout = QVBoxLayout() conf_layout.addWidget(conf_label) conf_layout.addWidget(self.conf_input) conf_layout.addWidget(self.json_label) hlayout.addLayout(vlayout) hlayout.addLayout(conf_layout) general_vlayout.addLayout(hlayout) general_vlayout.addWidget(bbox) self.setLayout(general_vlayout) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) self.lang_cb.currentIndexChanged.connect( self.lang_selection_changed) self.form_status(False) if language is not None: self.form_status(True) self.validate() @Slot() def validate(self): host_text = self.host_input.text() cmd_text = self.cmd_input.text() if not self.HOST_REGEX.match(host_text): self.button_ok.setEnabled(False) self.host_input.setStyleSheet("QLineEdit{border: 1px solid red;}") self.host_input.setToolTip('Hostname must be valid') return else: self.host_input.setStyleSheet( "QLineEdit{border: 1px solid green;}") self.host_input.setToolTip('Hostname is valid') self.button_ok.setEnabled(True) if not self.external: if not self.NON_EMPTY_REGEX.match(cmd_text): self.button_ok.setEnabled(False) self.cmd_input.setStyleSheet( "QLineEdit{border: 1px solid red;}") self.cmd_input.setToolTip('Command must be non empty') return if find_program(cmd_text) is None: self.button_ok.setEnabled(False) self.cmd_input.setStyleSheet( "QLineEdit{border: 1px solid red;}") self.cmd_input.setToolTip('Program was not found ' 'on your system') return else: self.cmd_input.setStyleSheet( "QLineEdit{border: 1px solid green;}") self.cmd_input.setToolTip('Program was found on your system') self.button_ok.setEnabled(True) try: json.loads(self.conf_input.toPlainText()) try: self.json_label.setText(self.JSON_VALID) except: pass except (ValueError, json.decoder.JSONDecodeError): try: self.json_label.setText(self.JSON_INVALID) self.button_ok.setEnabled(False) except: pass def form_status(self, status): self.host_input.setEnabled(status) self.port_spinner.setEnabled(status) self.external_cb.setEnabled(status) self.cmd_input.setEnabled(status) self.args_input.setEnabled(status) self.conf_input.setEnabled(status) self.json_label.setVisible(status) @Slot() def lang_selection_changed(self): idx = self.lang_cb.currentIndex() if idx == 0: self.set_defaults() self.form_status(False) self.button_ok.setEnabled(False) else: server = self.parent.get_server_by_lang(LSP_LANGUAGES[idx - 1]) self.form_status(True) if server is not None: self.host_input.setText(server.host) self.port_spinner.setValue(server.port) self.external_cb.setChecked(server.external) self.cmd_input.setText(server.cmd) self.args_input.setText(server.args) self.conf_input.set_text(json.dumps(server.configurations)) self.json_label.setText(self.JSON_VALID) self.button_ok.setEnabled(True) else: self.set_defaults() def set_defaults(self): self.cmd_input.setStyleSheet('') self.host_input.setStyleSheet('') self.host_input.setText(self.DEFAULT_HOST) self.port_spinner.setValue(self.DEFAULT_PORT) self.external_cb.setChecked(self.DEFAULT_EXTERNAL) self.cmd_input.setText(self.DEFAULT_CMD) self.args_input.setText(self.DEFAULT_ARGS) self.conf_input.set_text(self.DEFAULT_CONFIGURATION) self.json_label.setText(self.JSON_VALID) @Slot(bool) @Slot(int) def set_local_options(self, enabled): self.external = enabled self.cmd_input.setEnabled(True) self.args_input.setEnabled(True) if enabled: self.cmd_input.setEnabled(False) self.cmd_input.setStyleSheet('') self.args_input.setEnabled(False) try: self.validate() except: pass def get_options(self): language_idx = self.lang_cb.currentIndex() language = LSP_LANGUAGES[language_idx - 1] host = self.host_input.text() port = int(self.port_spinner.value()) external = self.external_cb.isChecked() args = self.args_input.text() cmd = self.cmd_input.text() configurations = json.loads(self.conf_input.toPlainText()) server = LSPServer(language=language.lower(), cmd=cmd, args=args, host=host, port=port, external=external, configurations=configurations) return server
class ObjectExplorer(QDialog): """Object explorer main widget window.""" # TODO: Use signal to trigger update of configs sig_option_changed = Signal(str, object) def __init__(self, obj, name='', expanded=False, resize_to_contents=True, parent=None, attribute_columns=DEFAULT_ATTR_COLS, attribute_details=DEFAULT_ATTR_DETAILS, show_callable_attributes=False, show_special_attributes=False, dataframe_format=None, readonly=None, reset=False): """ Constructor :param name: name of the object as it will appear in the root node :param expanded: show the first visible root element expanded :param resize_to_contents: resize columns to contents ignoring width of the attributes :param obj: any Python object or variable :param attribute_columns: list of AttributeColumn objects that define which columns are present in the table and their defaults :param attribute_details: list of AttributeDetails objects that define which attributes can be selected in the details pane. :param show_callable_attributes: if True rows where the 'is attribute' and 'is callable' columns are both True, are displayed. Otherwise they are hidden. :param show_special_attributes: if True rows where the 'is attribute' is True and the object name starts and ends with two underscores, are displayed. Otherwise they are hidden. :param dataframe_format: Format for the values in the Dataframe Editor. :param reset: If true the persistent settings, such as column widths, are reset. """ QDialog.__init__(self, parent=parent) self.setAttribute(Qt.WA_DeleteOnClose) # Model self._attr_cols = attribute_columns self._attr_details = attribute_details self._dataframe_format = dataframe_format self.readonly = readonly self.btn_save_and_close = None self.btn_close = None self._tree_model = TreeModel(obj, obj_name=name, attr_cols=self._attr_cols) self._proxy_tree_model = TreeProxyModel( show_callable_attributes=show_callable_attributes, show_special_attributes=show_special_attributes, dataframe_format=dataframe_format) self._proxy_tree_model.setSourceModel(self._tree_model) # self._proxy_tree_model.setSortRole(RegistryTableModel.SORT_ROLE) self._proxy_tree_model.setDynamicSortFilter(True) # self._proxy_tree_model.setSortCaseSensitivity(Qt.CaseInsensitive) # Views self._setup_actions() self._setup_menu(show_callable_attributes=show_callable_attributes, show_special_attributes=show_special_attributes) self._setup_views() if name: name = "{} -".format(name) self.setWindowTitle("{} {}".format(name, EDITOR_NAME)) self.setWindowFlags(Qt.Window) self._resize_to_contents = resize_to_contents self._readViewSettings(reset=reset) # Update views with model self.toggle_show_special_attribute_action.setChecked( show_special_attributes) self.toggle_show_callable_action.setChecked(show_callable_attributes) # Select first row so that a hidden root node will not be selected. first_row_index = self._proxy_tree_model.firstItemIndex() self.obj_tree.setCurrentIndex(first_row_index) if self._tree_model.inspectedNodeIsVisible or expanded: self.obj_tree.expand(first_row_index) def get_value(self): """Get editor current object state.""" return self._tree_model.inspectedItem.obj def _make_show_column_function(self, column_idx): """Creates a function that shows or hides a column.""" show_column = lambda checked: self.obj_tree.setColumnHidden( column_idx, not checked) return show_column def _setup_actions(self): """Creates the main window actions.""" # Show/hide callable objects self.toggle_show_callable_action = \ QAction(_("Show callable attributes"), self, checkable=True, shortcut=QKeySequence("Alt+C"), statusTip=_("Shows/hides attributes " "that are callable (functions, methods, etc)")) self.toggle_show_callable_action.toggled.connect( self._proxy_tree_model.setShowCallables) # Show/hide special attributes self.toggle_show_special_attribute_action = \ QAction(_("Show __special__ attributes"), self, checkable=True, shortcut=QKeySequence("Alt+S"), statusTip=_("Shows or hides __special__ attributes")) self.toggle_show_special_attribute_action.toggled.connect( self._proxy_tree_model.setShowSpecialAttributes) def _setup_menu(self, show_callable_attributes=False, show_special_attributes=False): """Sets up the main menu.""" self.tools_layout = QHBoxLayout() callable_attributes = create_toolbutton( self, text=_("Show callable attributes"), icon=ima.icon("class"), toggled=self._toggle_show_callable_attributes_action) callable_attributes.setCheckable(True) callable_attributes.setChecked(show_callable_attributes) self.tools_layout.addWidget(callable_attributes) special_attributes = create_toolbutton( self, text=_("Show __special__ attributes"), icon=ima.icon("private2"), toggled=self._toggle_show_special_attributes_action) special_attributes.setCheckable(True) special_attributes.setChecked(show_special_attributes) self.tools_layout.addWidget(special_attributes) self.tools_layout.addStretch() self.options_button = create_toolbutton(self, text=_('Options'), icon=ima.icon('tooloptions')) self.options_button.setPopupMode(QToolButton.InstantPopup) self.show_cols_submenu = QMenu(self) self.options_button.setMenu(self.show_cols_submenu) # Don't show menu arrow and remove padding if is_dark_interface(): self.options_button.setStyleSheet( ("QToolButton::menu-indicator{image: none;}\n" "QToolButton{padding: 3px;}")) else: self.options_button.setStyleSheet( "QToolButton::menu-indicator{image: none;}") self.tools_layout.addWidget(self.options_button) @Slot() def _toggle_show_callable_attributes_action(self): """Toggle show callable atributes action.""" action_checked = not self.toggle_show_callable_action.isChecked() self.toggle_show_callable_action.setChecked(action_checked) self.sig_option_changed.emit('show_callable_attributes', action_checked) @Slot() def _toggle_show_special_attributes_action(self): """Toggle show special attributes action.""" action_checked = ( not self.toggle_show_special_attribute_action.isChecked()) self.toggle_show_special_attribute_action.setChecked(action_checked) self.sig_option_changed.emit('show_special_attributes', action_checked) @Slot(str) def _set_dataframe_format(self, new_format): """ Set format to use in DataframeEditor. Args: new_format (string): e.g. "%.3f" """ self.sig_option_changed.emit('dataframe_format', new_format) self._tree_model.dataframe_format = new_format def _setup_views(self): """Creates the UI widgets.""" self.central_splitter = QSplitter(self, orientation=Qt.Vertical) layout = create_plugin_layout(self.tools_layout, self.central_splitter) self.setLayout(layout) # Tree widget self.obj_tree = ToggleColumnTreeView( dataframe_format=self._dataframe_format) self.obj_tree.setAlternatingRowColors(True) self.obj_tree.setModel(self._proxy_tree_model) self.obj_tree.setSelectionBehavior(QAbstractItemView.SelectRows) self.obj_tree.setUniformRowHeights(True) self.obj_tree.setAnimated(True) self.obj_tree.add_header_context_menu() self.obj_tree.sig_option_changed.connect(self.sig_option_changed.emit) # Stretch last column? # It doesn't play nice when columns are hidden and then shown again. obj_tree_header = self.obj_tree.header() obj_tree_header.setSectionsMovable(True) obj_tree_header.setStretchLastSection(False) add_actions(self.show_cols_submenu, self.obj_tree.toggle_column_actions_group.actions()) self.central_splitter.addWidget(self.obj_tree) # Bottom pane bottom_pane_widget = QWidget() bottom_layout = QHBoxLayout() bottom_layout.setSpacing(0) bottom_layout.setContentsMargins(5, 5, 5, 5) # left top right bottom bottom_pane_widget.setLayout(bottom_layout) self.central_splitter.addWidget(bottom_pane_widget) group_box = QGroupBox(_("Details")) bottom_layout.addWidget(group_box) v_group_layout = QVBoxLayout() h_group_layout = QHBoxLayout() h_group_layout.setContentsMargins(2, 2, 2, 2) # left top right bottom group_box.setLayout(v_group_layout) v_group_layout.addLayout(h_group_layout) # Radio buttons radio_widget = QWidget() radio_layout = QVBoxLayout() radio_layout.setContentsMargins(0, 0, 0, 0) # left top right bottom radio_widget.setLayout(radio_layout) self.button_group = QButtonGroup(self) for button_id, attr_detail in enumerate(self._attr_details): radio_button = QRadioButton(attr_detail.name) radio_layout.addWidget(radio_button) self.button_group.addButton(radio_button, button_id) self.button_group.buttonClicked[int].connect( self._change_details_field) self.button_group.button(0).setChecked(True) radio_layout.addStretch(1) h_group_layout.addWidget(radio_widget) # Editor widget self.editor = CodeEditor(self) self.editor.setReadOnly(True) h_group_layout.addWidget(self.editor) # Warining label about repr repr_label = QLabel( _("(*) Some objects have very large repr's, " "which can freeze Spyder. Please use this " "with care.")) v_group_layout.addWidget(repr_label) # Save and close buttons btn_layout = QHBoxLayout() btn_layout.addStretch() if not self.readonly: self.btn_save_and_close = QPushButton(_('Save and Close')) self.btn_save_and_close.setDisabled(True) self.btn_save_and_close.clicked.connect(self.accept) btn_layout.addWidget(self.btn_save_and_close) self.btn_close = QPushButton(_('Close')) self.btn_close.setAutoDefault(True) self.btn_close.setDefault(True) self.btn_close.clicked.connect(self.reject) btn_layout.addWidget(self.btn_close) v_group_layout.addLayout(btn_layout) # Splitter parameters self.central_splitter.setCollapsible(0, False) self.central_splitter.setCollapsible(1, True) self.central_splitter.setSizes([500, 320]) self.central_splitter.setStretchFactor(0, 10) self.central_splitter.setStretchFactor(1, 0) # Connect signals # Keep a temporary reference of the selection_model to prevent # segfault in PySide. # See http://permalink.gmane.org/gmane.comp.lib.qt.pyside.devel/222 selection_model = self.obj_tree.selectionModel() selection_model.currentChanged.connect(self._update_details) # Check if the values of the model have been changed self._proxy_tree_model.sig_setting_data.connect( self.save_and_close_enable) self._proxy_tree_model.sig_update_details.connect( self._update_details_for_item) # End of setup_methods def _readViewSettings(self, reset=False): """ Reads the persistent program settings. :param reset: If True, the program resets to its default settings. """ pos = QPoint(20, 20) window_size = QSize(825, 650) details_button_idx = 0 header = self.obj_tree.header() header_restored = False if reset: logger.debug("Resetting persistent view settings") else: pos = pos window_size = window_size details_button_idx = details_button_idx # splitter_state = settings.value("central_splitter/state") splitter_state = None if splitter_state: self.central_splitter.restoreState(splitter_state) # header_restored = self.obj_tree.read_view_settings( # 'table/header_state', # settings, reset) header_restored = False if not header_restored: column_sizes = [col.width for col in self._attr_cols] column_visible = [col.col_visible for col in self._attr_cols] for idx, size in enumerate(column_sizes): if not self._resize_to_contents and size > 0: # Just in case header.resizeSection(idx, size) else: header.setSectionResizeMode(QHeaderView.ResizeToContents) for idx, visible in enumerate(column_visible): elem = self.obj_tree.toggle_column_actions_group.actions()[idx] elem.setChecked(visible) self.resize(window_size) button = self.button_group.button(details_button_idx) if button is not None: button.setChecked(True) @Slot() def save_and_close_enable(self): """Handle the data change event to enable the save and close button.""" if self.btn_save_and_close: self.btn_save_and_close.setEnabled(True) self.btn_save_and_close.setAutoDefault(True) self.btn_save_and_close.setDefault(True) @Slot(QModelIndex, QModelIndex) def _update_details(self, current_index, _previous_index): """Shows the object details in the editor given an index.""" tree_item = self._proxy_tree_model.treeItem(current_index) self._update_details_for_item(tree_item) def _change_details_field(self, _button_id=None): """Changes the field that is displayed in the details pane.""" # logger.debug("_change_details_field: {}".format(_button_id)) current_index = self.obj_tree.selectionModel().currentIndex() tree_item = self._proxy_tree_model.treeItem(current_index) self._update_details_for_item(tree_item) @Slot(TreeItem) def _update_details_for_item(self, tree_item): """Shows the object details in the editor given an tree_item.""" try: # obj = tree_item.obj button_id = self.button_group.checkedId() assert button_id >= 0, ("No radio button selected. " "Please report this bug.") attr_details = self._attr_details[button_id] data = attr_details.data_fn(tree_item) self.editor.setPlainText(data) self.editor.setWordWrapMode(attr_details.line_wrap) show_blanks = CONF.get('editor', 'blank_spaces') update_scrollbar = CONF.get('editor', 'scroll_past_end') scheme_name = CONF.get('appearance', 'selected') self.editor.setup_editor(tab_mode=False, font=get_font(), show_blanks=show_blanks, color_scheme=scheme_name, scroll_past_end=update_scrollbar) self.editor.set_text(data) if attr_details.name == 'Source code': self.editor.set_language('Python') else: self.editor.set_language('Rst') except Exception as ex: self.editor.setStyleSheet("color: red;") stack_trace = traceback.format_exc() self.editor.setPlainText("{}\n\n{}".format(ex, stack_trace)) self.editor.setWordWrapMode( QTextOption.WrapAtWordBoundaryOrAnywhere) @classmethod def create_explorer(cls, *args, **kwargs): """ Creates and shows and ObjectExplorer window. The *args and **kwargs will be passed to the ObjectExplorer constructor A (class attribute) reference to the browser window is kept to prevent it from being garbage-collected. """ object_explorer = cls(*args, **kwargs) object_explorer.exec_() return object_explorer
class LSPServerEditor(QDialog): DEFAULT_HOST = '127.0.0.1' DEFAULT_PORT = 2084 DEFAULT_CMD = '' DEFAULT_ARGS = '' DEFAULT_CONFIGURATION = '{}' DEFAULT_EXTERNAL = False DEFAULT_STDIO = False HOST_REGEX = re.compile(r'^\w+([.]\w+)*$') NON_EMPTY_REGEX = re.compile(r'^\S+$') JSON_VALID = _('Valid JSON') JSON_INVALID = _('Invalid JSON') MIN_SIZE = QSize(850, 600) INVALID_CSS = "QLineEdit {border: 1px solid red;}" VALID_CSS = "QLineEdit {border: 1px solid green;}" def __init__(self, parent, language=None, cmd='', host='127.0.0.1', port=2084, args='', external=False, stdio=False, configurations={}, **kwargs): super(LSPServerEditor, self).__init__(parent) description = _( "To create a new server configuration, you need to select a " "programming language, set the command to start its associated " "server and enter any arguments that should be passed to it on " "startup. Additionally, you can set the server's hostname and " "port if connecting to an external server, " "or to a local one using TCP instead of stdio pipes." "<br><br>" "<i>Note</i>: You can use the placeholders <tt>{host}</tt> and " "<tt>{port}</tt> in the server arguments field to automatically " "fill in the respective values.<br>" ) self.parent = parent self.external = external # Widgets self.server_settings_description = QLabel(description) self.lang_cb = QComboBox(self) self.external_cb = QCheckBox(_('External server'), self) self.host_label = QLabel(_('Host:')) self.host_input = QLineEdit(self) self.port_label = QLabel(_('Port:')) self.port_spinner = QSpinBox(self) self.cmd_label = QLabel(_('Command:')) self.cmd_input = QLineEdit(self) self.args_label = QLabel(_('Arguments:')) self.args_input = QLineEdit(self) self.json_label = QLabel(self.JSON_VALID, self) self.conf_label = QLabel(_('<b>Server Configuration:</b>')) self.conf_input = CodeEditor(None) self.bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.button_ok = self.bbox.button(QDialogButtonBox.Ok) self.button_cancel = self.bbox.button(QDialogButtonBox.Cancel) # Widget setup self.setMinimumSize(self.MIN_SIZE) self.setWindowTitle(_('LSP server editor')) self.server_settings_description.setWordWrap(True) self.lang_cb.setToolTip( _('Programming language provided by the LSP server')) self.lang_cb.addItem(_('Select a language')) self.lang_cb.addItems(LSP_LANGUAGES) self.button_ok.setEnabled(False) if language is not None: idx = LSP_LANGUAGES.index(language) self.lang_cb.setCurrentIndex(idx + 1) self.button_ok.setEnabled(True) self.host_input.setPlaceholderText('127.0.0.1') self.host_input.setText(host) self.host_input.textChanged.connect(lambda x: self.validate()) self.port_spinner.setToolTip(_('TCP port number of the server')) self.port_spinner.setMinimum(1) self.port_spinner.setMaximum(60000) self.port_spinner.setValue(port) self.cmd_input.setText(cmd) self.cmd_input.setPlaceholderText('/absolute/path/to/command') self.args_input.setToolTip( _('Additional arguments required to start the server')) self.args_input.setText(args) self.args_input.setPlaceholderText(r'--host {host} --port {port}') self.conf_input.setup_editor( language='json', color_scheme=CONF.get('appearance', 'selected'), wrap=False, edge_line=True, highlight_current_line=True, highlight_current_cell=True, occurrence_highlighting=True, auto_unindent=True, font=get_font(), filename='config.json', folding=False ) self.conf_input.set_language('json', 'config.json') self.conf_input.setToolTip(_('Additional LSP server configuration ' 'set at runtime. JSON required')) try: conf_text = json.dumps(configurations, indent=4, sort_keys=True) except Exception: conf_text = '{}' self.conf_input.set_text(conf_text) self.external_cb.setToolTip( _('Check if the server runs on a remote location')) self.external_cb.setChecked(external) self.stdio_cb = QCheckBox(_('Use stdio pipes for communication'), self) self.stdio_cb.setToolTip(_('Check if the server communicates ' 'using stdin/out pipes')) self.stdio_cb.setChecked(stdio) # Layout setup hlayout = QHBoxLayout() general_vlayout = QVBoxLayout() general_vlayout.addWidget(self.server_settings_description) vlayout = QVBoxLayout() lang_group = QGroupBox(_('Language')) lang_layout = QVBoxLayout() lang_layout.addWidget(self.lang_cb) lang_group.setLayout(lang_layout) vlayout.addWidget(lang_group) server_group = QGroupBox(_('Language server')) server_layout = QGridLayout() server_layout.addWidget(self.cmd_label, 0, 0) server_layout.addWidget(self.cmd_input, 0, 1) server_layout.addWidget(self.args_label, 1, 0) server_layout.addWidget(self.args_input, 1, 1) server_group.setLayout(server_layout) vlayout.addWidget(server_group) address_group = QGroupBox(_('Server address')) host_layout = QVBoxLayout() host_layout.addWidget(self.host_label) host_layout.addWidget(self.host_input) port_layout = QVBoxLayout() port_layout.addWidget(self.port_label) port_layout.addWidget(self.port_spinner) conn_info_layout = QHBoxLayout() conn_info_layout.addLayout(host_layout) conn_info_layout.addLayout(port_layout) address_group.setLayout(conn_info_layout) vlayout.addWidget(address_group) advanced_group = QGroupBox(_('Advanced')) advanced_layout = QVBoxLayout() advanced_layout.addWidget(self.external_cb) advanced_layout.addWidget(self.stdio_cb) advanced_group.setLayout(advanced_layout) vlayout.addWidget(advanced_group) conf_layout = QVBoxLayout() conf_layout.addWidget(self.conf_label) conf_layout.addWidget(self.conf_input) conf_layout.addWidget(self.json_label) vlayout.addStretch() hlayout.addLayout(vlayout, 2) hlayout.addLayout(conf_layout, 3) general_vlayout.addLayout(hlayout) general_vlayout.addWidget(self.bbox) self.setLayout(general_vlayout) self.form_status(False) # Signals if not external: self.cmd_input.textChanged.connect(lambda x: self.validate()) self.external_cb.stateChanged.connect(self.set_local_options) self.stdio_cb.stateChanged.connect(self.set_stdio_options) self.lang_cb.currentIndexChanged.connect(self.lang_selection_changed) self.conf_input.textChanged.connect(self.validate) self.bbox.accepted.connect(self.accept) self.bbox.rejected.connect(self.reject) # Final setup if language is not None: self.form_status(True) self.validate() if stdio: self.set_stdio_options(True) if external: self.set_local_options(True) @Slot() def validate(self): host_text = self.host_input.text() cmd_text = self.cmd_input.text() if not self.HOST_REGEX.match(host_text): self.button_ok.setEnabled(False) self.host_input.setStyleSheet(self.INVALID_CSS) if bool(host_text): self.host_input.setToolTip(_('Hostname must be valid')) else: self.host_input.setToolTip( _('Hostname or IP address of the host on which the server ' 'is running. Must be non empty.')) else: self.host_input.setStyleSheet(self.VALID_CSS) self.host_input.setToolTip(_('Hostname is valid')) self.button_ok.setEnabled(True) if not self.external: if not self.NON_EMPTY_REGEX.match(cmd_text): self.button_ok.setEnabled(False) self.cmd_input.setStyleSheet(self.INVALID_CSS) self.cmd_input.setToolTip( _('Command used to start the LSP server locally. Must be ' 'non empty')) return if find_program(cmd_text) is None: self.button_ok.setEnabled(False) self.cmd_input.setStyleSheet(self.INVALID_CSS) self.cmd_input.setToolTip(_('Program was not found ' 'on your system')) else: self.cmd_input.setStyleSheet(self.VALID_CSS) self.cmd_input.setToolTip(_('Program was found on your ' 'system')) self.button_ok.setEnabled(True) try: json.loads(self.conf_input.toPlainText()) try: self.json_label.setText(self.JSON_VALID) except Exception: pass except ValueError: try: self.json_label.setText(self.JSON_INVALID) self.button_ok.setEnabled(False) except Exception: pass def form_status(self, status): self.host_input.setEnabled(status) self.port_spinner.setEnabled(status) self.external_cb.setEnabled(status) self.stdio_cb.setEnabled(status) self.cmd_input.setEnabled(status) self.args_input.setEnabled(status) self.conf_input.setEnabled(status) self.json_label.setVisible(status) @Slot() def lang_selection_changed(self): idx = self.lang_cb.currentIndex() if idx == 0: self.set_defaults() self.form_status(False) self.button_ok.setEnabled(False) else: server = self.parent.get_server_by_lang(LSP_LANGUAGES[idx - 1]) self.form_status(True) if server is not None: self.host_input.setText(server.host) self.port_spinner.setValue(server.port) self.external_cb.setChecked(server.external) self.stdio_cb.setChecked(server.stdio) self.cmd_input.setText(server.cmd) self.args_input.setText(server.args) self.conf_input.set_text(json.dumps(server.configurations)) self.json_label.setText(self.JSON_VALID) self.button_ok.setEnabled(True) else: self.set_defaults() def set_defaults(self): self.cmd_input.setStyleSheet('') self.host_input.setStyleSheet('') self.host_input.setText(self.DEFAULT_HOST) self.port_spinner.setValue(self.DEFAULT_PORT) self.external_cb.setChecked(self.DEFAULT_EXTERNAL) self.stdio_cb.setChecked(self.DEFAULT_STDIO) self.cmd_input.setText(self.DEFAULT_CMD) self.args_input.setText(self.DEFAULT_ARGS) self.conf_input.set_text(self.DEFAULT_CONFIGURATION) self.json_label.setText(self.JSON_VALID) @Slot(bool) @Slot(int) def set_local_options(self, enabled): self.external = enabled self.cmd_input.setEnabled(True) self.args_input.setEnabled(True) if enabled: self.cmd_input.setEnabled(False) self.cmd_input.setStyleSheet('') self.args_input.setEnabled(False) self.stdio_cb.stateChanged.disconnect() self.stdio_cb.setChecked(False) self.stdio_cb.setEnabled(False) else: self.cmd_input.setEnabled(True) self.args_input.setEnabled(True) self.stdio_cb.setEnabled(True) self.stdio_cb.setChecked(False) self.stdio_cb.stateChanged.connect(self.set_stdio_options) try: self.validate() except Exception: pass @Slot(bool) @Slot(int) def set_stdio_options(self, enabled): self.stdio = enabled if enabled: self.cmd_input.setEnabled(True) self.args_input.setEnabled(True) self.external_cb.stateChanged.disconnect() self.external_cb.setChecked(False) self.external_cb.setEnabled(False) self.host_input.setStyleSheet('') self.host_input.setEnabled(False) self.port_spinner.setEnabled(False) else: self.cmd_input.setEnabled(True) self.args_input.setEnabled(True) self.external_cb.setChecked(False) self.external_cb.setEnabled(True) self.external_cb.stateChanged.connect(self.set_local_options) self.host_input.setEnabled(True) self.port_spinner.setEnabled(True) try: self.validate() except Exception: pass def get_options(self): language_idx = self.lang_cb.currentIndex() language = LSP_LANGUAGES[language_idx - 1] host = self.host_input.text() port = int(self.port_spinner.value()) external = self.external_cb.isChecked() stdio = self.stdio_cb.isChecked() args = self.args_input.text() cmd = self.cmd_input.text() configurations = json.loads(self.conf_input.toPlainText()) server = LSPServer(language=language.lower(), cmd=cmd, args=args, host=host, port=port, external=external, stdio=stdio, configurations=configurations) return server
def code_editor(): """setup editor and return fold levels.""" editor = CodeEditor(parent=None) editor.setup_editor(language='Python') editor.set_text(text) return editor