Repository: sublimelsp/LSP

Issue:
Code Lenses fail to update their position (and content) after Undo
**Describe the bug**

Code lenses are not getting re-requested and updated in some circumstances like for for example being scrolled out of the view and Undo being performed that restores removed line above the code lenses.

**To Reproduce**

Notice how code lenses are mispositioned after reverting removed line:

https://user-images.githubusercontent.com/153197/207286020-e3ad46e7-0442-4432-b522-063bd3a8c341.mov

**Environment (please complete the following information):**
- Sublime Text version: 4143
- LSP version: 1.20.0
- Language servers used: LSP-volar

**Additional information**

Checking the LSP logs I can see that on doing Undo we are sending `didChange` notification but not making the `textDocument/codeLens` request to update code lenses.


Discussion:
It also annoys me immensely that I have to scroll unresolved code lenses into the view and click within the document to resolve them. If I have unresolved code lenses on multiple pages then it becomes a chore to keep scrolling the document and triggering manual click. And then repeat the next time document is modified.

Resolving patch:
diff --git a/plugin/core/types.py b/plugin/core/types.py
--- a/plugin/core/types.py
+++ b/plugin/core/types.py
@@ -6,7 +6,6 @@
 from .typing import cast
 from .url import filename_to_uri
 from .url import parse_uri
-from threading import RLock
 from wcmatch.glob import BRACE
 from wcmatch.glob import globmatch
 from wcmatch.glob import GLOBSTAR
@@ -134,41 +133,46 @@ def __del__(self) -> None:
         self._settings.clear_on_change("LSP")
 
 
-class Debouncer:
+class DebouncerNonThreadSafe:
+    """
+    Debouncer for delaying execution of a function until specified timeout time.
+
+    When calling `debounce()` multiple times, if the time span between calls is shorter than the specified `timeout_ms`,
+    the callback function will only be called once, after `timeout_ms` since the last call.
+
+    This implementation is not thread safe. You must ensure that `debounce()` is called from the same thread as
+    was choosen during initialization through the `async_thread` argument.
+    """
 
-    def __init__(self) -> None:
+    def __init__(self, async_thread: bool) -> None:
+        self._async_thread = async_thread
         self._current_id = -1
         self._next_id = 0
-        self._current_id_lock = RLock()
 
-    def debounce(self, f: Callable[[], None], timeout_ms: int = 0, condition: Callable[[], bool] = lambda: True,
-                 async_thread: bool = False) -> None:
+    def debounce(
+        self, f: Callable[[], None], timeout_ms: int = 0, condition: Callable[[], bool] = lambda: True
+    ) -> None:
         """
-        Possibly run a function at a later point in time, either on the async thread or on the main thread.
+        Possibly run a function at a later point in time on the thread chosen during initialization.
 
         :param      f:             The function to possibly run
         :param      timeout_ms:    The time in milliseconds after which to possibly to run the function
         :param      condition:     The condition that must evaluate to True in order to run the funtion
-        :param      async_thread:  If true, run the function on the async worker thread, otherwise run
-                                   the function on the main thread
         """
 
         def run(debounce_id: int) -> None:
-            with self._current_id_lock:
-                if debounce_id != self._current_id:
-                    return
+            if debounce_id != self._current_id:
+                return
             if condition():
                 f()
 
-        runner = sublime.set_timeout_async if async_thread else sublime.set_timeout
-        with self._current_id_lock:
-            current_id = self._current_id = self._next_id
+        runner = sublime.set_timeout_async if self._async_thread else sublime.set_timeout
+        current_id = self._current_id = self._next_id
         self._next_id += 1
         runner(lambda: run(current_id), timeout_ms)
 
     def cancel_pending(self) -> None:
-        with self._current_id_lock:
-            self._current_id = -1
+        self._current_id = -1
 
 
 def read_dict_setting(settings_obj: sublime.Settings, key: str, default: dict) -> dict:
diff --git a/plugin/documents.py b/plugin/documents.py
--- a/plugin/documents.py
+++ b/plugin/documents.py
@@ -27,6 +27,7 @@
 from .core.signature_help import SigHelp
 from .core.types import basescope2languageid
 from .core.types import debounced
+from .core.types import DebouncerNonThreadSafe
 from .core.types import FEATURES_TIMEOUT
 from .core.types import SettingsRegistration
 from .core.typing import Any, Callable, Optional, Dict, Generator, Iterable, List, Tuple, Union
@@ -158,6 +159,7 @@ def on_change() -> None:
             self.set_uri(view_to_uri(view))
         self._auto_complete_triggered_manually = False
         self._change_count_on_last_save = -1
+        self._code_lenses_debouncer_async = DebouncerNonThreadSafe(async_thread=True)
         self._registration = SettingsRegistration(view.settings(), on_change=on_change)
         self._setup()
 
@@ -317,6 +319,8 @@ def on_text_changed_async(self, change_count: int, changes: Iterable[sublime.Tex
         if self.view.is_primary():
             for sv in self.session_views_async():
                 sv.on_text_changed_async(change_count, changes)
+        self._code_lenses_debouncer_async.debounce(
+            self._do_code_lenses_async, timeout_ms=self.code_lenses_debounce_time)
         if not different:
             return
         self._clear_highlight_regions()
@@ -324,8 +328,6 @@ def on_text_changed_async(self, change_count: int, changes: Iterable[sublime.Tex
             self._when_selection_remains_stable_async(self._do_highlights_async, current_region,
                                                       after_ms=self.highlights_debounce_time)
         self.do_signature_help_async(manual=False)
-        self._when_selection_remains_stable_async(self._do_code_lenses_async, current_region,
-                                                  after_ms=self.code_lenses_debounce_time)
 
     def get_uri(self) -> str:
         return self._uri
diff --git a/plugin/session_buffer.py b/plugin/session_buffer.py
--- a/plugin/session_buffer.py
+++ b/plugin/session_buffer.py
@@ -12,7 +12,7 @@
 from .core.settings import userprefs
 from .core.types import Capabilities
 from .core.types import debounced
-from .core.types import Debouncer
+from .core.types import DebouncerNonThreadSafe
 from .core.types import FEATURES_TIMEOUT
 from .core.typing import Any, Callable, Iterable, Optional, List, Set, Dict, Tuple, Union
 from .core.views import DIAGNOSTIC_SEVERITY
@@ -110,7 +110,7 @@ def __init__(self, session_view: SessionViewProtocol, buffer_id: int, uri: Docum
         self.diagnostics_flags = 0
         self.diagnostics_are_visible = False
         self.last_text_change_time = 0.0
-        self.diagnostics_debouncer = Debouncer()
+        self.diagnostics_debouncer_async = DebouncerNonThreadSafe(async_thread=True)
         self.color_phantoms = sublime.PhantomSet(view, "lsp_color")
         self.document_links = []  # type: List[DocumentLink]
         self.semantic_tokens = SemanticTokensData()
@@ -423,10 +423,10 @@ def on_diagnostics_async(
                 else:
                     data.regions.append(region)
                 diagnostics.append((diagnostic, region))
-            self._publish_diagnostics_to_session_views(
+            self._publish_diagnostics_to_session_views_async(
                 diagnostics_version, diagnostics, data_per_severity, visible_session_views)
 
-    def _publish_diagnostics_to_session_views(
+    def _publish_diagnostics_to_session_views_async(
         self,
         diagnostics_version: int,
         diagnostics: List[Tuple[Diagnostic, sublime.Region]],
@@ -442,7 +442,7 @@ def present() -> None:
             for sv in self.session_views:
                 sv.present_diagnostics_async(sv in visible_session_views)
 
-        self.diagnostics_debouncer.cancel_pending()
+        self.diagnostics_debouncer_async.cancel_pending()
 
         if self.diagnostics_are_visible:
             # Old diagnostics are visible. Update immediately.
@@ -458,11 +458,10 @@ def present() -> None:
             if delay_in_seconds <= 0.0:
                 present()
             else:
-                self.diagnostics_debouncer.debounce(
+                self.diagnostics_debouncer_async.debounce(
                     present,
                     timeout_ms=int(1000.0 * delay_in_seconds),
                     condition=lambda: bool(view and view.is_valid() and view.change_count() == diagnostics_version),
-                    async_thread=True
                 )
 
     # --- textDocument/semanticTokens ----------------------------------------------------------------------------------


Test patch: