Repository: sublimelsp/LSP

Issue:
Document state can get out of sync when server supports incremental sync
**Describe the bug**
In some situations, the document state can get out of sync with the server. One case where I can reproduce is when activating a view that doesn't have server started yet and immediately changing something.

**To Reproduce**
1. Activate a view that doesn't have the server started yet.
2. Immediately type something that would cause an error

**Expected behavior**
The server having a correct state of the document.

**Environment (please complete the following information):**
- Sublime Text version: 4087
- LSP version: 1.0.17
- Language servers used: LSP-json

**Additional context**
I believe what happening is:
1. the file is opened and immediately some text is inserted:
   - the `change_count()` (revision) is now 1
   - server receives the text content of the document as seen in revision 1
2. The `on_text_changed_async` is triggered
   - the reported change is the insert done in point 1.
   - the revision is still 1
3. Server receives a didChange notification with insert done in point 1
   - now the server has an out-of-sync state since it has applied the change to the initial state which already included that change.

Here are logs from when a couple of `,` are inserted quickly after activating the view. The logs are for when `didOpen` and then `didChange` notifications are sent:
```
DID_OPEN 1
ON_TEXT_CHANGED 1 [(5, 5, ',')]
ON_TEXT_CHANGED 1 [(6, 6, ',')]
ON_TEXT_CHANGED 2 [(7, 7, ',')]
```


Discussion:
Yeah, we need a check to see if the change_count is actually larger than what is already sent in didOpen. It'd be nice if on_text_changed_async would pass along the change_count from the point in time that the on_text_changed was recorded. I think we can solve it in the plugin, but the most tidy solution would probably be to have the ST api changed a bit.

Resolving patch:
diff --git a/plugin/documents.py b/plugin/documents.py
--- a/plugin/documents.py
+++ b/plugin/documents.py
@@ -136,9 +136,17 @@ def detach(self) -> None:
         self.ids_to_listeners.pop(self.buffer.buffer_id, None)
         super().detach()
 
-    def on_text_changed_async(self, changes: Iterable[sublime.TextChange]) -> None:
-        for listener in list(self.view_listeners):
-            listener.on_text_changed_async(changes)
+    def on_text_changed(self, changes: Iterable[sublime.TextChange]) -> None:
+        view = self.buffer.primary_view()
+        if not view:
+            return
+        change_count = view.change_count()
+
+        def notify() -> None:
+            for listener in list(self.view_listeners):
+                listener.on_text_changed_async(change_count, changes)
+
+        sublime.set_timeout_async(notify)
 
     def on_reload_async(self) -> None:
         for listener in list(self.view_listeners):
@@ -249,11 +257,11 @@ def session_buffers_async(self) -> Generator[SessionBuffer, None, None]:
         for sv in self.session_views_async():
             yield sv.session_buffer
 
-    def on_text_changed_async(self, changes: Iterable[sublime.TextChange]) -> None:
+    def on_text_changed_async(self, change_count: int, changes: Iterable[sublime.TextChange]) -> None:
         different, current_region = self._update_stored_region_async()
         if self.view.is_primary():
             for sv in self.session_views_async():
-                sv.on_text_changed_async(changes)
+                sv.on_text_changed_async(change_count, changes)
         if not different:
             return
         if "documentHighlight" not in userprefs().disabled_capabilities:
diff --git a/plugin/session_buffer.py b/plugin/session_buffer.py
--- a/plugin/session_buffer.py
+++ b/plugin/session_buffer.py
@@ -171,7 +171,8 @@ def should_notify_did_save(self) -> Tuple[bool, bool]:
     def should_notify_did_close(self) -> bool:
         return self.capabilities.should_notify_did_close() or self.session.should_notify_did_close()
 
-    def on_text_changed_async(self, view: sublime.View, changes: Iterable[sublime.TextChange]) -> None:
+    def on_text_changed_async(self, view: sublime.View, change_count: int,
+                              changes: Iterable[sublime.TextChange]) -> None:
         self.last_text_change_time = time.time()
         last_change = list(changes)[-1]
         if last_change.a.pt == 0 and last_change.b.pt == 0 and last_change.str == '' and view.size() != 0:
@@ -180,13 +181,16 @@ def on_text_changed_async(self, view: sublime.View, changes: Iterable[sublime.Te
             # one that removes all content and one that has 0,0,'' parameters.
             pass
         else:
-            change_count = view.change_count()
+            purge = False
             if self.pending_changes is None:
                 self.pending_changes = PendingChanges(change_count, changes)
-            else:
+                purge = True
+            elif self.pending_changes.version < change_count:
                 self.pending_changes.update(change_count, changes)
-            debounced(lambda: self.purge_changes_async(view), 500,
-                      lambda: view.is_valid() and change_count == view.change_count())
+                purge = True
+            if purge:
+                debounced(lambda: self.purge_changes_async(view), 500,
+                          lambda: view.is_valid() and change_count == view.change_count())
 
     def on_revert_async(self, view: sublime.View) -> None:
         self.pending_changes = None  # Don't bother with pending changes
diff --git a/plugin/session_view.py b/plugin/session_view.py
--- a/plugin/session_view.py
+++ b/plugin/session_view.py
@@ -143,8 +143,8 @@ def on_request_started_async(self, request_id: int, request: Request) -> None:
     def on_request_finished_async(self, request_id: int) -> None:
         self.active_requests.pop(request_id, None)
 
-    def on_text_changed_async(self, changes: Iterable[sublime.TextChange]) -> None:
-        self.session_buffer.on_text_changed_async(self.view, changes)
+    def on_text_changed_async(self, change_count: int, changes: Iterable[sublime.TextChange]) -> None:
+        self.session_buffer.on_text_changed_async(self.view, change_count, changes)
 
     def on_revert_async(self) -> None:
         self.session_buffer.on_revert_async(self.view)


Test patch: