Repository: sublimelsp/LSP

Issue:
Plugin/client sends duplicate didChange requests
**Describe the bug**

The plugin (acting as LSP client) can sometimes send duplicate `didChange` request.

See https://github.com/hashicorp/terraform-ls/issues/310 for more, including snippet of the log (which contains whole request).

**Expected behavior**

Avoid sending duplicate request.

**Environment (please complete the following information):**
- OS: macOS 10.15.7
- Sublime Text version: 4093
- LSP version: 1.1.0
- Language servers used: terraform

**Additional context**

I am aware that the log may not provide sufficient details to debug this and you might also say that server should just ignore it - which would be all fair responses. I just thought I'd raise this as an issue, so you are aware, despite being unable to provide much details. So feel free to close this issue if you find it hard to fix or don't want to fix it, I will have no hard feelings about it! 😄 



Discussion:
Just to clarify this is for the "full" sync variant, not the "incremental" sync variant.
> Just to clarify this is for the "full" sync variant, not the "incremental" sync variant.

The log does seems to suggest that this was a "full" sync because the client sent the whole document. I probably should've captured the whole log to see if there were any other incremental `didChange` requests sent before these and what response the client send to `initialize`.

You are right that the client _didn't_ send `Range` in `TextDocumentContentChangeEvent`, but I'm assuming it did sent `TextDocumentSyncKind: Incremental` - at least I didn't change any default settings on the client side around document synchronization, if there is one.

Is it valid to send range-less change event in incremental mode and does lack of `Range` make it "full" sync for that particular change? Should full sync still be allowed in "incremental mode"? The spec doesn't explicitly document this, but this seems to suggest it is not as "strict"?

```ts
/**
 * An event describing a change to a text document.
 * If range and rangeLength are omitted, the new text is considered to be
 * the full content of the document.
 */
export type TextDocumentContentChangeEvent = {
	/**
	 * The range of the document that changed.
	 */
	range: Range;

	/**
	 * The optional length of the range that got replaced.
	 *
	 * @deprecated use range instead.
	 */
	rangeLength?: number;

	/**
	 * The new text for the provided range.
	 */
	text: string;
} | {
	/**
	 * The new text of the whole document.
	 */
	text: string;
}
```
This client will send a "full" text sync in the following cases:

1. if you revert the file, using ST's built-in diff algorithm.
2. external FS changes happened, not in the editor.
3. if the server doesn't advertise `textDocumentSync.change == 2` in its static capabilities **and** it did not do a `client/registerCapability` for `textDocument/didChange` with `syncKind == 2` in its register options (whew)

Even if the server advertises `textDocumentSync.change == 2`, a client may still decide to send full text sync changes.

> Is it valid to send range-less change event in incremental mode and does lack of Range make it "full" sync for that particular change?

Yes.

> Should full sync still be allowed in "incremental mode"?

Yes.

It would be nice though if this client didn't send the same text twice. Could you explain in more detail in what context this would trigger? :)
> It would be nice though if this client didn't send the same text twice. Could you explain in more detail in what context this would trigger? :)

I wish I could. I should have stored the whole log and remembered what I did, but I didn't store and don't remember unfortunately.

With that in mind feel free to close this issue and I can open a new one with more context, if I run into the same problem again.
There is enough context here and I can reproduce this easily with the `File -> Revert File` command.

Resolving patch:
diff --git a/plugin/documents.py b/plugin/documents.py
--- a/plugin/documents.py
+++ b/plugin/documents.py
@@ -99,11 +99,11 @@ def notify() -> None:
 
     def on_reload_async(self) -> None:
         for listener in list(self.view_listeners):
-            listener.on_reload_async()
+            listener.reload_async()
 
     def on_revert_async(self) -> None:
         for listener in list(self.view_listeners):
-            listener.on_revert_async()
+            listener.revert_async()
 
     def __repr__(self) -> str:
         return "TextChangeListener({})".format(self.buffer.buffer_id)
@@ -226,16 +226,6 @@ def on_text_changed_async(self, change_count: int, changes: Iterable[sublime.Tex
         if "signatureHelp" not in userprefs().disabled_capabilities:
             self._do_signature_help(manual=False)
 
-    def on_revert_async(self) -> None:
-        if self.view.is_primary():
-            for sv in self.session_views_async():
-                sv.on_revert_async()
-
-    def on_reload_async(self) -> None:
-        if self.view.is_primary():
-            for sv in self.session_views_async():
-                sv.on_reload_async()
-
     def get_language_id(self) -> str:
         return self._language_id
 
@@ -570,6 +560,16 @@ def sum_total_errors_and_warnings_async(self) -> Tuple[int, int]:
             warnings += sb.total_warnings
         return errors, warnings
 
+    def revert_async(self) -> None:
+        if self.view.is_primary():
+            for sv in self.session_views_async():
+                sv.on_revert_async()
+
+    def reload_async(self) -> None:
+        if self.view.is_primary():
+            for sv in self.session_views_async():
+                sv.on_reload_async()
+
     # --- Private utility methods --------------------------------------------------------------------------------------
 
     def _when_selection_remains_stable_async(self, f: Callable[[], None], r: sublime.Region, after_ms: int) -> None:


Test patch: