From 0cfc2080af32ba39a71398308deea0a9bcadcd7b Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Sat, 17 Aug 2019 10:55:14 +0200 Subject: [PATCH 01/21] SpittableCodeEditTabWidget: also avoid ambiguous names when a name conflict arises with one of the parent splitters --- pyqode/core/widgets/splittable_tab_widget.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pyqode/core/widgets/splittable_tab_widget.py b/pyqode/core/widgets/splittable_tab_widget.py index e7218cfa..0f34d0d2 100644 --- a/pyqode/core/widgets/splittable_tab_widget.py +++ b/pyqode/core/widgets/splittable_tab_widget.py @@ -915,13 +915,20 @@ def current_widget(self): return self._current() return None - def widgets(self, include_clones=False): + def widgets(self, include_clones=False, from_root=False): """ Recursively gets the list of widgets. :param include_clones: True to retrieve all tabs, including clones, otherwise only original widgets are returned. - """ + :param from_root: True to get all widgets, rather than only the widgets + that are under the current splitter and its child splitters. + """ + if from_root and not self.root: + return self.parent().widgets( + include_clones=include_clones, + from_root=True + ) widgets = [] for i in range(self.main_tab_widget.count()): widget = self.main_tab_widget.widget(i) @@ -1438,7 +1445,7 @@ def open_document(self, path, encoding=None, replace_tabs_by_spaces=True, name = os.path.split(original_path)[1] use_parent_dir = False - for tab in self.widgets(): + for tab in self.widgets(from_root=True): title = QtCore.QFileInfo(tab.file.path).fileName() if title == name: tw = tab.parent_tab_widget From b07f0be24c1ea93f463fc196768e4cc508709058 Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Mon, 2 Sep 2019 15:47:57 +0200 Subject: [PATCH 02/21] Fix race condition where None is passed to TextHelper.line_text() - A dumb fix that probably doesn't address the underlying issue --- pyqode/core/api/utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyqode/core/api/utils.py b/pyqode/core/api/utils.py index b42f88dc..fad41889 100644 --- a/pyqode/core/api/utils.py +++ b/pyqode/core/api/utils.py @@ -293,6 +293,12 @@ def line_text(self, line_nbr): :return: Entire line's text :rtype: str """ + + # Under some (apparent) race conditions, this function can be called + # with a None line number. This should be fixed in a better way, but + # for now we return an empty string to avoid crashes. + if line_nbr is None: + return '' doc = self._editor.document() block = doc.findBlockByNumber(line_nbr) return block.text() From fd567695575dbb8c62158acf202979594e311c49 Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Sat, 7 Sep 2019 11:00:38 +0200 Subject: [PATCH 03/21] CodeEdit: remember if the code edit was already closed - Improves performance when closing multiple times (e.g. because of sloppy programming elsewhere) --- pyqode/core/api/code_edit.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyqode/core/api/code_edit.py b/pyqode/core/api/code_edit.py index 3cb8a794..57b8f1ed 100644 --- a/pyqode/core/api/code_edit.py +++ b/pyqode/core/api/code_edit.py @@ -436,6 +436,7 @@ def __init__(self, parent=None, create_default_actions=True): super(CodeEdit, self).__init__(parent) self.installEventFilter(self) self.clones = [] + self._closed = False self._show_ctx_mnu = True self._default_font_size = 10 self._backend = BackendManager(self) @@ -581,6 +582,9 @@ def close(self, clear=True): :param clear: True to clear the editor content before closing. """ + if self._closed: + return + self._closed = True if self._tooltips_runner: self._tooltips_runner.cancel_requests() self._tooltips_runner = None From cdd8b26a2f5759d787791b22ae60dd7fbd236d6f Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Sun, 8 Sep 2019 19:25:40 +0200 Subject: [PATCH 04/21] CodeEdit: don't unnecessarily call _reset_stylesheet() - Improves performance during initialization --- pyqode/core/api/code_edit.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/pyqode/core/api/code_edit.py b/pyqode/core/api/code_edit.py index 57b8f1ed..79575403 100644 --- a/pyqode/core/api/code_edit.py +++ b/pyqode/core/api/code_edit.py @@ -183,7 +183,8 @@ def font_name(self, value): if value == "": value = self._DEFAULT_FONT self._font_family = value - self._reset_stylesheet() + if self._auto_reset_stylesheet: + self._reset_stylesheet() for c in self.clones: c.font_name = value @@ -217,7 +218,8 @@ def font_size(self): @font_size.setter def font_size(self, value): self._font_size = value - self._reset_stylesheet() + if self._auto_reset_stylesheet: + self._reset_stylesheet() for c in self.clones: c.font_size = value @@ -231,7 +233,8 @@ def background(self): @background.setter def background(self, value): self._background = value - self._reset_stylesheet() + if self._auto_reset_stylesheet: + self._reset_stylesheet() for c in self.clones: c.background = value @@ -245,7 +248,8 @@ def foreground(self): @foreground.setter def foreground(self, value): self._foreground = value - self._reset_stylesheet() + if self._auto_reset_stylesheet: + self._reset_stylesheet() for c in self.clones: c.foreground = value @@ -275,7 +279,8 @@ def selection_background(self): @selection_background.setter def selection_background(self, value): self._sel_background = value - self._reset_stylesheet() + if self._auto_reset_stylesheet: + self._reset_stylesheet() for c in self.clones: c.selection_background = value @@ -434,6 +439,7 @@ def __init__(self, parent=None, create_default_actions=True): :attr:`show_menu_enabled` to False. """ super(CodeEdit, self).__init__(parent) + self._auto_reset_stylesheet = False self.installEventFilter(self) self.clones = [] self._closed = False @@ -503,6 +509,8 @@ def __init__(self, parent=None, create_default_actions=True): self.setCenterOnScroll(True) self.setLineWrapMode(self.NoWrap) self.setCursorWidth(2) + self._auto_reset_stylesheet = True + self._reset_stylesheet() def __repr__(self): return '%s(path=%r)' % (self.__class__.__name__, self.file.path) @@ -1337,8 +1345,11 @@ def _on_text_changed(self): ln = TextHelper(self).cursor_position()[0] self._modified_lines.add(ln) + def _reset_stylesheet(self): """ Resets stylesheet""" + # This function is called very often during initialization, which + # impacts performance. This is a hack to avoid this. self.setFont(QtGui.QFont(self._font_family, self._font_size + self._zoom_level)) flg_stylesheet = hasattr(self, '_flg_stylesheet') From e8dd60ff0e57d53f95216122b34ea89c9e1192d4 Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Thu, 12 Sep 2019 17:03:17 +0200 Subject: [PATCH 05/21] SplittableCodeEditTabWidget: set mime type on CodeEdit widgets that use PygmentsSH - Avoids always falling back to Python lexer, even if a different extension was specified --- pyqode/core/widgets/splittable_tab_widget.py | 21 ++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/pyqode/core/widgets/splittable_tab_widget.py b/pyqode/core/widgets/splittable_tab_widget.py index 0f34d0d2..adb593a3 100644 --- a/pyqode/core/widgets/splittable_tab_widget.py +++ b/pyqode/core/widgets/splittable_tab_widget.py @@ -1352,10 +1352,23 @@ def _create_code_edit(self, mimetype, *args, **kwargs): :return: Code editor widget instance. """ if mimetype in self.editors.keys(): - return self.editors[mimetype]( - *args, parent=self.main_tab_widget, **kwargs) - editor = self.fallback_editor(*args, parent=self.main_tab_widget, - **kwargs) + editor = self.editors[mimetype]( + *args, + parent=self.main_tab_widget, + **kwargs + ) + else: + editor = self.fallback_editor( + *args, + parent=self.main_tab_widget, + **kwargs + ) + try: + pygments = editor.modes.get('PygmentsSH') + except KeyError: + pass + else: + pygments.set_mime_type(mimetype) return editor def create_new_document(self, base_name='New Document', From a241bd897c68e018f46d4f49e3c9cc30bbd24c4b Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Thu, 12 Sep 2019 18:36:46 +0200 Subject: [PATCH 06/21] PygmentsSH: don't try to set unspecified mime type --- pyqode/core/modes/pygments_sh.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyqode/core/modes/pygments_sh.py b/pyqode/core/modes/pygments_sh.py index 2ddaf6ed..9e3f4f7a 100644 --- a/pyqode/core/modes/pygments_sh.py +++ b/pyqode/core/modes/pygments_sh.py @@ -188,6 +188,11 @@ def set_mime_type(self, mime_type): :param mime_type: mime type of the new lexer to setup. """ + + if not mime_type: + # Don't try to set unspecified mime type. Improves performance and + # avoids (harmless) error messages + return True try: self.set_lexer_from_mime_type(mime_type) except ClassNotFound: From ef193624da2a870c215cca53a95454d7e6729d3e Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Sat, 14 Sep 2019 11:37:13 +0200 Subject: [PATCH 07/21] PygmentsSH: fall back to TextLexer - Don't leave lexer unset even if no mimetype has been specified --- pyqode/core/modes/pygments_sh.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqode/core/modes/pygments_sh.py b/pyqode/core/modes/pygments_sh.py index 9e3f4f7a..1efa3cc8 100644 --- a/pyqode/core/modes/pygments_sh.py +++ b/pyqode/core/modes/pygments_sh.py @@ -190,9 +190,9 @@ def set_mime_type(self, mime_type): """ if not mime_type: - # Don't try to set unspecified mime type. Improves performance and - # avoids (harmless) error messages - return True + # Fall back to TextLexer + self._lexer = TextLexer() + return False try: self.set_lexer_from_mime_type(mime_type) except ClassNotFound: From df18fb891a23bf39429446bbc016bbcfad16f763 Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Mon, 16 Sep 2019 17:30:15 +0200 Subject: [PATCH 08/21] SplittableCodeEditTabWidget: only rename tabs when an actual save-as has occurred - avoids unsaved tabs from being renamed to blank --- pyqode/core/widgets/splittable_tab_widget.py | 22 +++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/pyqode/core/widgets/splittable_tab_widget.py b/pyqode/core/widgets/splittable_tab_widget.py index adb593a3..2f0075fe 100644 --- a/pyqode/core/widgets/splittable_tab_widget.py +++ b/pyqode/core/widgets/splittable_tab_widget.py @@ -1246,6 +1246,7 @@ def save_current_as(self): if widget.original: widget = widget.original mem = widget.file.path + old_path = widget.file.path widget.file._path = None widget.file._old_path = mem CodeEditTabWidget.default_directory = os.path.dirname(mem) @@ -1266,16 +1267,17 @@ def save_current_as(self): # Traverse through all splitters and all editors, and change the tab # text whenever the editor is a clone of the current widget or the # current widget itself. - current_document = widget.document() - for splitter in self.get_all_splitters(): - for editor in splitter._tabs: - if editor.document() != current_document: - continue - index = splitter.main_tab_widget.indexOf(editor) - splitter.main_tab_widget.setTabText( - index, - widget.file.name - ) + if old_path != widget.file.path: + current_document = widget.document() + for splitter in self.get_all_splitters(): + for editor in splitter._tabs: + if editor.document() != current_document: + continue + index = splitter.main_tab_widget.indexOf(editor) + splitter.main_tab_widget.setTabText( + index, + widget.file.name + ) return widget.file.path def get_root_splitter(self): From 6a8e031ca108bd5fcda696f577b3e5a190108392 Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Fri, 20 Sep 2019 08:50:12 +0200 Subject: [PATCH 09/21] SyntaxHighlighter: don't unnecessarily call _reset_stylesheet() - Improves performance --- pyqode/core/api/syntax_highlighter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqode/core/api/syntax_highlighter.py b/pyqode/core/api/syntax_highlighter.py index c6c162aa..a4c6a62d 100644 --- a/pyqode/core/api/syntax_highlighter.py +++ b/pyqode/core/api/syntax_highlighter.py @@ -254,6 +254,7 @@ def refresh_editor(self, color_scheme): :param color_scheme: new color scheme. """ + self.editor._auto_reset_stylesheet = False self.editor.background = color_scheme.background self.editor.foreground = color_scheme.formats[ 'normal'].foreground().color() @@ -272,6 +273,7 @@ def refresh_editor(self, color_scheme): pass else: mode.refresh_decorations(force=True) + self.editor._auto_reset_stylesheet = True self.editor._reset_stylesheet() def __init__(self, parent, color_scheme=None): From 804204a3fc2f6ca93b40381b3e091c78a380f3f8 Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Wed, 6 Nov 2019 09:52:06 +0100 Subject: [PATCH 10/21] Fall back to text/plain mimetype when lexer is missing --- pyqode/core/modes/pygments_sh.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pyqode/core/modes/pygments_sh.py b/pyqode/core/modes/pygments_sh.py index 1efa3cc8..7b472488 100644 --- a/pyqode/core/modes/pygments_sh.py +++ b/pyqode/core/modes/pygments_sh.py @@ -240,8 +240,14 @@ def set_lexer_from_mime_type(self, mime, **options): :param mime: mime type :param options: optional addtional options. """ - self._lexer = get_lexer_for_mimetype(mime, **options) - _logger().debug('lexer for mimetype (%s): %r', mime, self._lexer) + + try: + self._lexer = get_lexer_for_mimetype(mime, **options) + except (ClassNotFound, ImportError): + print('class not found for mime', mime) + self._lexer = get_lexer_for_mimetype('text/plain') + else: + _logger().debug('lexer for mimetype (%s): %r', mime, self._lexer) def highlight_block(self, text, block): """ From 506741baa3cc4db2c42a659ff33b415e7305e197 Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Fri, 8 Nov 2019 13:54:31 +0100 Subject: [PATCH 11/21] CodeEdit: home jumps to beginning of line (not block) for unindented lines - Matches expectations for working with text --- pyqode/core/api/code_edit.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/pyqode/core/api/code_edit.py b/pyqode/core/api/code_edit.py index 79575403..6e6144a3 100644 --- a/pyqode/core/api/code_edit.py +++ b/pyqode/core/api/code_edit.py @@ -1388,17 +1388,29 @@ def _reset_stylesheet(self): def _do_home_key(self, event=None, select=False): """ Performs home key action """ - # get nb char to first significative char - delta = (self.textCursor().positionInBlock() - - TextHelper(self).line_indent()) cursor = self.textCursor() move = QtGui.QTextCursor.MoveAnchor if select: move = QtGui.QTextCursor.KeepAnchor - if delta > 0: - cursor.movePosition(QtGui.QTextCursor.Left, move, delta) + indent = TextHelper(self).line_indent() + # Scenario 1: We're on an unindented block. In that case, we jump back + # to the start of the visible line, but not all the way to the back of + # the block. This is what you would expect when working with text and + # line wrapping is enabled. + if not indent: + cursor.movePosition(QtGui.QTextCursor.StartOfLine, move) else: - cursor.movePosition(QtGui.QTextCursor.StartOfBlock, move) + delta = self.textCursor().positionInBlock() - indent + # Scenario 2: We're on an indented block. In that case, we move + # back to the indented position. This is what you would expect when + # working with code. + if delta > 0: + cursor.movePosition(QtGui.QTextCursor.Left, move, delta) + # Scenario 3: We're on an indented block, but we're already at the + # start of the indentation. In that case, we jump back to the + # beginning of the block. + else: + cursor.movePosition(QtGui.QTextCursor.StartOfBlock, move) self.setTextCursor(cursor) if event: event.accept() From 139b4bc3bc7dc32edec9d03ae36a9cd744a2baa5 Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Fri, 8 Nov 2019 14:11:05 +0100 Subject: [PATCH 12/21] CodeEdit: correctly update visible blocks when first block is partly out of the viewport - Fixed line numbers when line wrapping is enabled --- pyqode/core/api/code_edit.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyqode/core/api/code_edit.py b/pyqode/core/api/code_edit.py index 6e6144a3..4fc48993 100644 --- a/pyqode/core/api/code_edit.py +++ b/pyqode/core/api/code_edit.py @@ -1328,11 +1328,13 @@ def _update_visible_blocks(self, *args): bottom = top + int(self.blockBoundingRect(block).height()) ebottom_top = 0 ebottom_bottom = self.height() + first_block = True while block.isValid(): visible = (top >= ebottom_top and bottom <= ebottom_bottom) - if not visible: + if not visible and not first_block: break - if block.isVisible(): + first_block = False + if visible and block.isVisible(): self._visible_blocks.append((top, block_nbr, block)) block = block.next() top = bottom From c9540dcc00026be78e711b1b1b3c119ff7bf6ad4 Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Fri, 15 Nov 2019 12:22:10 +0100 Subject: [PATCH 13/21] PanelsManager: fix references to modes --- pyqode/core/managers/panels.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqode/core/managers/panels.py b/pyqode/core/managers/panels.py index d09a816b..cdccae64 100644 --- a/pyqode/core/managers/panels.py +++ b/pyqode/core/managers/panels.py @@ -109,13 +109,13 @@ def keys(self): """ Returns the list of installed panel names. """ - return self._modes.keys() + return self._panels.keys() def values(self): """ Returns the list of installed panels. """ - return self._modes.values() + return self._panels.values() def __iter__(self): lst = [] From 7a57f08450d909aabe9708e4be9a3d0ef9374940 Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Thu, 21 Nov 2019 10:49:47 +0100 Subject: [PATCH 14/21] PygmentsSH: copy lexer when cloning - Avoids lexer from being reset when original is closed --- pyqode/core/modes/pygments_sh.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyqode/core/modes/pygments_sh.py b/pyqode/core/modes/pygments_sh.py index 7b472488..fd0b802e 100644 --- a/pyqode/core/modes/pygments_sh.py +++ b/pyqode/core/modes/pygments_sh.py @@ -174,6 +174,11 @@ def _init_style(self): """ Init pygments style """ self._update_style() + def clone_settings(self, original): + + # The lexer can be shared between clones. + self._lexer = original._lexer + def on_install(self, editor): """ :type editor: pyqode.code.api.CodeEdit From ee0803f6c65f65d61d58227c28d7771a36549ae7 Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Thu, 21 Nov 2019 10:49:01 +0100 Subject: [PATCH 15/21] CodeEdit: don't assume that clones have exactly the same modes and panels as originals - This doesn't need to be the case if they are programmatically altered --- pyqode/core/api/code_edit.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/pyqode/core/api/code_edit.py b/pyqode/core/api/code_edit.py index 4fc48993..1c582dc9 100644 --- a/pyqode/core/api/code_edit.py +++ b/pyqode/core/api/code_edit.py @@ -557,15 +557,24 @@ def link(self, clone): clone.file._encoding = self.file.encoding clone.file._mimetype = self.file.mimetype clone.setDocument(self.document()) - for original_mode, mode in zip(list(self.modes), list(clone.modes)): - mode.enabled = original_mode.enabled - mode.clone_settings(original_mode) - for original_panel, panel in zip( - list(self.panels), list(clone.panels)): - panel.enabled = original_panel.isEnabled() - panel.clone_settings(original_panel) + for original_mode in self.modes: + try: + clone_mode = clone.modes.get(original_mode.name) + except KeyError: + print('Not cloning', original_mode) + continue + clone_mode.enabled = original_mode.enabled + clone_mode.clone_settings(original_mode) + for original_panel in self.panels: + try: + clone_panel = clone.panels.get(original_panel.name) + except KeyError: + print('Not cloning', original_panel) + continue + clone_panel.enabled = original_panel.isEnabled() + clone_panel.clone_settings(original_panel) if not original_panel.isVisible(): - panel.setVisible(False) + clone_panel.setVisible(False) clone.use_spaces_instead_of_tabs = self.use_spaces_instead_of_tabs clone.tab_length = self.tab_length clone._save_on_focus_out = self._save_on_focus_out From 135da1cced6e500287ecd93f5eba67f2b67d1396 Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Sun, 24 Nov 2019 18:46:08 +0100 Subject: [PATCH 16/21] CodeEdit: better implementation of cut - Doesn't overwrite clipboard when a non-selected full line of only whitespace is cut --- pyqode/core/api/code_edit.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/pyqode/core/api/code_edit.py b/pyqode/core/api/code_edit.py index 1c582dc9..724d7912 100644 --- a/pyqode/core/api/code_edit.py +++ b/pyqode/core/api/code_edit.py @@ -896,25 +896,27 @@ def eventFilter(self, obj, event): def cut(self): """ - Cuts the selected text or the whole line if no text was selected. + Cuts the selected text or the whole line if no text was selected. When + cutting a full line that consists of only whitespace, the line is only + deleted, to avoid overwriting the clipboard with whitespace. """ + tc = self.textCursor() - helper = TextHelper(self) tc.beginEditBlock() - no_selection = False - sText = tc.selection().toPlainText() - # If only whitespace is selected, simply delete it - if not helper.current_line_text() and not sText.strip(): - tc.deleteChar() + if not tc.hasSelection(): + tc.movePosition(tc.StartOfLine) + tc.movePosition(tc.EndOfLine, tc.KeepAnchor) + from_selection = False + else: + from_selection = True + if from_selection or tc.selectedText().strip(): + need_cut = True else: - if not self.textCursor().hasSelection(): - no_selection = True - TextHelper(self).select_whole_line() - super(CodeEdit, self).cut() - if no_selection: - tc.deleteChar() + tc.removeSelectedText() + need_cut = False tc.endEditBlock() self.setTextCursor(tc) + super(CodeEdit, self).cut() def copy(self): """ From d58bc1f525591f7cc97d2f1653f44543c3bdf930 Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Sun, 24 Nov 2019 19:00:43 +0100 Subject: [PATCH 17/21] CodeEdit: fix swapping multiline blocks --- pyqode/core/api/code_edit.py | 8 +++++--- pyqode/core/api/utils.py | 27 ++++++++++++++++++++------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/pyqode/core/api/code_edit.py b/pyqode/core/api/code_edit.py index 724d7912..bd2a6d7b 100644 --- a/pyqode/core/api/code_edit.py +++ b/pyqode/core/api/code_edit.py @@ -952,7 +952,7 @@ def __swapLine(self, up): return # Select the current lines and the line that will be swapped, turn # them into a list, and then perform the swap on this list - helper.select_lines(start_index, end_index) + helper.select_lines(start_index, end_index, select_blocks=True) lines = helper.selected_text().replace(u'\u2029', u'\n').split(u'\n') if up: lines = lines[1:] + [lines[0]] @@ -967,9 +967,11 @@ def __swapLine(self, up): if has_selection: # If text was originally selected, select the range again if up: - helper.select_lines(start_index, end_index - 1) + helper.select_lines(start_index, end_index - 1, + select_blocks=True) else: - helper.select_lines(start_index + 1, end_index) + helper.select_lines(start_index + 1, end_index, + select_blocks=True) else: # Else restore cursor position, while moving with the swap helper.goto_line(line - 1 if up else line + 1, column) diff --git a/pyqode/core/api/utils.py b/pyqode/core/api/utils.py index fad41889..0ee2fa1e 100644 --- a/pyqode/core/api/utils.py +++ b/pyqode/core/api/utils.py @@ -438,7 +438,8 @@ def move_cursor_to(self, line): cursor.setPosition(block.position()) return cursor - def select_lines(self, start=0, end=-1, apply_selection=True): + def select_lines(self, start=0, end=-1, apply_selection=True, + select_blocks=False): """ Selects entire lines between start and end line numbers. @@ -453,6 +454,8 @@ def select_lines(self, start=0, end=-1, apply_selection=True): end of the document :param apply_selection: True to apply the selection before returning the QTextCursor. + :param select_blocks: True to operate on blocks rather than visual + lines. :returns: A QTextCursor that holds the requested selection """ editor = self._editor @@ -461,21 +464,31 @@ def select_lines(self, start=0, end=-1, apply_selection=True): if start < 0: start = 0 text_cursor = self.move_cursor_to(start) + if select_blocks: + move_start = text_cursor.StartOfBlock + move_end = text_cursor.EndOfBlock + move_up = text_cursor.PreviousBlock + move_down = text_cursor.NextBlock + else: + move_start = text_cursor.StartOfLine + move_end = text_cursor.EndOfLine + move_up = text_cursor.Up + move_down = text_cursor.Down if end > start: # Going down - text_cursor.movePosition(text_cursor.Down, + text_cursor.movePosition(move_down, text_cursor.KeepAnchor, end - start) - text_cursor.movePosition(text_cursor.EndOfLine, + text_cursor.movePosition(move_end, text_cursor.KeepAnchor) elif end < start: # going up # don't miss end of line ! - text_cursor.movePosition(text_cursor.EndOfLine, + text_cursor.movePosition(move_end, text_cursor.MoveAnchor) - text_cursor.movePosition(text_cursor.Up, + text_cursor.movePosition(move_up, text_cursor.KeepAnchor, start - end) - text_cursor.movePosition(text_cursor.StartOfLine, + text_cursor.movePosition(move_start, text_cursor.KeepAnchor) else: - text_cursor.movePosition(text_cursor.EndOfLine, + text_cursor.movePosition(move_end, text_cursor.KeepAnchor) if apply_selection: editor.setTextCursor(text_cursor) From 39984743d98b92223ef75703e533905a70554ba7 Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Sun, 24 Nov 2019 20:04:24 +0100 Subject: [PATCH 18/21] AutoCompleteMode: make list of characters that shoudn't be duplicated configurable --- pyqode/core/modes/autocomplete.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pyqode/core/modes/autocomplete.py b/pyqode/core/modes/autocomplete.py index 3e67094a..fa072d80 100644 --- a/pyqode/core/modes/autocomplete.py +++ b/pyqode/core/modes/autocomplete.py @@ -22,6 +22,7 @@ def __init__(self): super(AutoCompleteMode, self).__init__() #: Auto complete mapping, maps input key with completion text. self.MAPPING = {'"': '"', "'": "'", "(": ")", "{": "}", "[": "]"} + self.AVOID_DUPLICATES = ')', ']', '}' #: The format to use for each symbol in mapping when there is a selection self.SELECTED_QUOTES_FORMATS = {key: '%s%s%s' for key in self.MAPPING.keys()} #: The format to use for each symbol in mapping when there is no selection @@ -92,15 +93,13 @@ def _on_key_pressed(self, event): tc.endEditBlock() self.editor.setTextCursor(tc) ignore = True - elif txt and next_char == txt and next_char in self.MAPPING: + elif ( + txt and next_char == txt and ( + next_char in self.MAPPING or + txt in self.AVOID_DUPLICATES + ) + ): ignore = True - elif event.text() == ')' or event.text() == ']' or event.text() == '}': - # if typing the same symbol twice, the symbol should not be written - # and the cursor moved just after the char - # e.g. if you type ) just before ), the cursor will just move after - # the existing ) - if next_char == event.text(): - ignore = True if ignore: event.accept() TextHelper(self.editor).clear_selection() From 90f124c7801f3674da32151a5b45b44b2f306969 Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Fri, 29 Nov 2019 19:13:07 +0100 Subject: [PATCH 19/21] SearchAndReplacePanel: postpone selecting until searching is done - Avoids non-responsiveness when pressing enter too quickly --- pyqode/core/panels/search_and_replace.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pyqode/core/panels/search_and_replace.py b/pyqode/core/panels/search_and_replace.py index 8b995307..ab78eea9 100644 --- a/pyqode/core/panels/search_and_replace.py +++ b/pyqode/core/panels/search_and_replace.py @@ -6,7 +6,6 @@ import sre_constants from pyqode.qt import QtCore, QtGui, QtWidgets - from pyqode.core import icons from pyqode.core._forms.search_panel_ui import Ui_SearchPanel from pyqode.core.api.decoration import TextDecoration @@ -144,6 +143,7 @@ def __init__(self): self._current_occurrence_index = 0 self._bg = None self._fg = None + self._working = False self._update_buttons(txt="") self.lineEditSearch.installEventFilter(self) self.lineEditReplace.installEventFilter(self) @@ -344,6 +344,7 @@ def request_search(self, txt=None): if txt is None or isinstance(txt, int): txt = self.lineEditSearch.text() if txt: + self._working = True self.job_runner.request_job( self._exec_search, txt, self._search_flags()) else: @@ -390,6 +391,10 @@ def select_next(self): :return: True in case of success, false if no occurrence could be selected. """ + + if self._working: + QtCore.QTimer.singleShot(100, self.select_next) + return current_occurence = self._current_occurrence() occurrences = self.get_occurences() if not occurrences: @@ -428,6 +433,10 @@ def select_previous(self): :return: True in case of success, false if no occurrence could be selected. """ + + if self._working: + QtCore.QTimer.singleShot(100, self.select_previous) + return current_occurence = self._current_occurrence() occurrences = self.get_occurences() if not occurrences: @@ -592,6 +601,7 @@ def _update_label_matches(self): self.labelMatches.clear() def _on_search_finished(self): + self._working = False self._clear_decorations() all_occurences = self.get_occurences() occurrences = all_occurences[:self.MAX_HIGHLIGHTED_OCCURENCES] From 64eaf1744f1e2d739268afd7f1fa0c9fe01270b8 Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Fri, 13 Dec 2019 19:43:03 +0100 Subject: [PATCH 20/21] Remove line (don't just blank) when cutting unselected text --- pyqode/core/api/code_edit.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqode/core/api/code_edit.py b/pyqode/core/api/code_edit.py index bd2a6d7b..cd8dd171 100644 --- a/pyqode/core/api/code_edit.py +++ b/pyqode/core/api/code_edit.py @@ -905,6 +905,8 @@ def cut(self): tc.beginEditBlock() if not tc.hasSelection(): tc.movePosition(tc.StartOfLine) + tc.movePosition(tc.Left) + tc.movePosition(tc.Right, tc.KeepAnchor) tc.movePosition(tc.EndOfLine, tc.KeepAnchor) from_selection = False else: From cfa583ee2875e30f99ec0fbcfcc54622ad58292e Mon Sep 17 00:00:00 2001 From: Sebastiaan Mathot Date: Sat, 14 Dec 2019 11:09:04 +0100 Subject: [PATCH 21/21] Remove extraneous print statements --- pyqode/core/api/code_edit.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyqode/core/api/code_edit.py b/pyqode/core/api/code_edit.py index cd8dd171..1034155a 100644 --- a/pyqode/core/api/code_edit.py +++ b/pyqode/core/api/code_edit.py @@ -561,7 +561,6 @@ def link(self, clone): try: clone_mode = clone.modes.get(original_mode.name) except KeyError: - print('Not cloning', original_mode) continue clone_mode.enabled = original_mode.enabled clone_mode.clone_settings(original_mode) @@ -569,7 +568,6 @@ def link(self, clone): try: clone_panel = clone.panels.get(original_panel.name) except KeyError: - print('Not cloning', original_panel) continue clone_panel.enabled = original_panel.isEnabled() clone_panel.clone_settings(original_panel)