Repository: spyder-ide/spyder

Issue:
Lags appear when typing in large files in the editor
This was reported by @impact27 in PR #14471 and I noticed it too in macOS yesterday when typing text in `mainwindow.py`.

The problem seems related to the usage of `textdistance` for folding because when folding is deactivated, there's no delay at all (and we fixed lags with large files in previous versions).

@andfoy, please take a look at this one for 4.2.2.


Discussion:

Resolving patch:
diff --git a/spyder/plugins/editor/panels/codefolding.py b/spyder/plugins/editor/panels/codefolding.py
--- a/spyder/plugins/editor/panels/codefolding.py
+++ b/spyder/plugins/editor/panels/codefolding.py
@@ -192,17 +192,13 @@ def __compute_line_offsets(self, text, reverse=False):
             offset += len(line)
         return line_start_offset
 
-    def update_folding(self, ranges):
+    def update_folding(self, folding_info):
         """Update folding panel folding ranges."""
-        if ranges is None:
+        if folding_info is None:
             return
 
-        self.current_tree, self.root = merge_folding(
-            ranges, self.current_tree, self.root)
-
-        folding_info = collect_folding_regions(self.root)
-
-        (self.folding_regions, self.folding_nesting,
+        (self.current_tree, self.root,
+         self.folding_regions, self.folding_nesting,
          self.folding_levels, self.folding_status) = folding_info
         self.update()
 
diff --git a/spyder/plugins/editor/widgets/codeeditor.py b/spyder/plugins/editor/widgets/codeeditor.py
--- a/spyder/plugins/editor/widgets/codeeditor.py
+++ b/spyder/plugins/editor/widgets/codeeditor.py
@@ -22,6 +22,7 @@
 
 from unicodedata import category
 import logging
+import functools
 import os.path as osp
 import re
 import sre_constants
@@ -77,6 +78,8 @@
 # from spyder.plugins.editor.utils.folding import IndentFoldDetector, FoldScope
 from spyder.plugins.editor.utils.kill_ring import QtKillRing
 from spyder.plugins.editor.utils.languages import ALL_LANGUAGES, CELL_LANGUAGES
+from spyder.plugins.editor.panels.utils import (
+    merge_folding, collect_folding_regions)
 from spyder.plugins.completion.decorators import (
     request, handles, class_register)
 from spyder.plugins.editor.widgets.base import TextEditBaseWidget
@@ -575,6 +578,7 @@ def __init__(self, parent=None):
 
         # Code Folding
         self.code_folding = True
+        self.update_folding_thread = QThread()
 
         # Completions hint
         self.completions_hint = True
@@ -1774,15 +1778,31 @@ def request_folding(self):
     @handles(LSPRequestTypes.DOCUMENT_FOLDING_RANGE)
     def handle_folding_range(self, response):
         """Handle folding response."""
-        try:
-            ranges = response['params']
-            if ranges is None:
-                return
+        ranges = response['params']
+        if ranges is None:
+            return
 
-            folding_panel = self.panels.get(FoldingPanel)
+        folding_panel = self.panels.get(FoldingPanel)
+
+        # Update folding
+        if self.update_folding_thread.isRunning():
+            self.update_folding_thread.terminate()
+
+        self.update_folding_thread.run = functools.partial(
+            self.update_and_merge_folding, ranges)
+        self.update_folding_thread.finished.connect(
+            self.finish_code_folding)
+        self.update_folding_thread.start()
+
+        # Tests for the class function selector need this.
+        if running_under_pytest():
+            self.request_symbols()
 
-            # Update folding
+    def update_and_merge_folding(self, ranges):
+        try:
             text = self.toPlainText()
+            folding_panel = self.panels.get(FoldingPanel)
+
             self.text_diff = (self.differ.diff_main(self.previous_text, text),
                               self.previous_text)
             extended_ranges = []
@@ -1790,12 +1810,12 @@ def handle_folding_range(self, response):
                 text_region = self.get_text_region(start, end)
                 extended_ranges.append((start, end, text_region))
 
-            folding_panel.update_folding(extended_ranges)
+            current_tree, root = merge_folding(
+                extended_ranges, folding_panel.current_tree,
+                folding_panel.root)
 
-            # Update indent guides, which depend on folding
-            if self.indent_guides._enabled and len(self.patch) > 0:
-                line, column = self.get_cursor_line_column()
-                self.update_whitespace_count(line, column)
+            folding_info = collect_folding_regions(root)
+            self._folding_info = (current_tree, root, *folding_info)
         except RuntimeError:
             # This is triggered when a codeeditor instance was removed
             # before the response can be processed.
@@ -1803,9 +1823,14 @@ def handle_folding_range(self, response):
         except Exception:
             self.log_lsp_handle_errors("Error when processing folding")
 
-        # Tests for the class function selector need this.
-        if running_under_pytest():
-            self.request_symbols()
+    def finish_code_folding(self):
+        folding_panel = self.panels.get(FoldingPanel)
+        folding_panel.update_folding(self._folding_info)
+
+        # Update indent guides, which depend on folding
+        if self.indent_guides._enabled and len(self.patch) > 0:
+            line, column = self.get_cursor_line_column()
+            self.update_whitespace_count(line, column)
 
     # ------------- LSP: Save/close file -----------------------------------
     @request(method=LSPRequestTypes.DOCUMENT_DID_SAVE,


Test patch:
diff --git a/spyder/plugins/editor/widgets/tests/test_folding.py b/spyder/plugins/editor/widgets/tests/test_folding.py
--- a/spyder/plugins/editor/widgets/tests/test_folding.py
+++ b/spyder/plugins/editor/widgets/tests/test_folding.py
@@ -68,11 +68,11 @@ def test_folding(lsp_codeeditor, qtbot):
     with qtbot.waitSignal(code_editor.lsp_response_signal, timeout=30000):
         code_editor.document_did_change()
 
-    while folding_panel.folding_regions == {}:
-        # This fixes the race condition on the PyLS with the
-        # document_did_change() event
-        with qtbot.waitSignal(code_editor.lsp_response_signal, timeout=30000):
-            code_editor.request_folding()
+    with qtbot.waitSignal(code_editor.lsp_response_signal, timeout=30000):
+        code_editor.request_folding()
+
+    # Wait for the update thread to finish
+    qtbot.wait(3000)
 
     folding_regions = folding_panel.folding_regions
     folding_levels = folding_panel.folding_levels
@@ -100,11 +100,11 @@ def test_unfold_when_searching(search_codeeditor, qtbot):
     with qtbot.waitSignal(editor.lsp_response_signal, timeout=30000):
         editor.document_did_change()
 
-    while folding_panel.folding_regions == {}:
-        # This fixes the race condition on the PyLS with the
-        # document_did_change() event
-        with qtbot.waitSignal(editor.lsp_response_signal, timeout=30000):
-            editor.request_folding()
+    with qtbot.waitSignal(editor.lsp_response_signal, timeout=30000):
+        editor.request_folding()
+
+    # Wait for the update thread to finish
+    qtbot.wait(3000)
 
     line_search = editor.document().findBlockByLineNumber(3)
 
@@ -134,11 +134,11 @@ def test_unfold_goto(search_codeeditor, qtbot):
     with qtbot.waitSignal(editor.lsp_response_signal, timeout=30000):
         editor.document_did_change()
 
-    while folding_panel.folding_regions == {}:
-        # This fixes the race condition on the PyLS with the
-        # document_did_change() event
-        with qtbot.waitSignal(editor.lsp_response_signal, timeout=30000):
-            editor.request_folding()
+    with qtbot.waitSignal(editor.lsp_response_signal, timeout=30000):
+        editor.request_folding()
+
+    # Wait for the update thread to finish
+    qtbot.wait(3000)
 
     line_goto = editor.document().findBlockByLineNumber(5)