Skip to content

Commit 4b02dbd

Browse files
Ensure didChange notification is never sent after didClose (#2438)
* Ensure didChange is never sent after didClose This fixes for example the Pyright warning LSP-pyright: Received change text document command for closed file <URI> when a file is saved and closed immediately after changes were applied. * Missed something * Add test * Maybe like this? * Try something else * Simplify expression to save one unnecessary API call view.change_count() returns 0 if the view isn't valid anymore (closed), so we can simply use short-circuit evaluation for this and don't need the is_valid() API call. * Exempt Linux * Small tweak to save an API call * Revert "Exempt Linux" This reverts commit 4dd2e91. * Fix failing test on Linux * actually this test passes locally with this line uncommented * Revert, apparently it fails on the CI... This reverts commit 43ede82. * try a slightly different approach just to see... test pass locally * Revert "try a slightly different approach just to see... test pass locally" the test still fail on the CI This reverts commit 11c5ecb. --------- Co-authored-by: Предраг Николић <[email protected]>
1 parent 4f30db8 commit 4b02dbd

File tree

3 files changed

+74
-45
lines changed

3 files changed

+74
-45
lines changed

plugin/session_buffer.py

+26-20
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,9 @@ def _check_did_open(self, view: sublime.View) -> None:
166166
self._do_document_link_async(view, version)
167167
self.session.notify_plugin_on_session_buffer_change(self)
168168

169-
def _check_did_close(self) -> None:
169+
def _check_did_close(self, view: sublime.View) -> None:
170170
if self.opened and self.should_notify_did_close():
171+
self.purge_changes_async(view, suppress_requests=True)
171172
self.session.send_notification(did_close(uri=self._last_known_uri))
172173
self.opened = False
173174

@@ -202,9 +203,9 @@ def remove_session_view(self, sv: SessionViewProtocol) -> None:
202203
self._clear_semantic_token_regions(sv.view)
203204
self.session_views.remove(sv)
204205
if len(self.session_views) == 0:
205-
self._on_before_destroy()
206+
self._on_before_destroy(sv.view)
206207

207-
def _on_before_destroy(self) -> None:
208+
def _on_before_destroy(self, view: sublime.View) -> None:
208209
self.remove_all_inlay_hints()
209210
if self.has_capability("diagnosticProvider") and self.session.config.diagnostics_mode == "open_files":
210211
self.session.m_textDocument_publishDiagnostics({'uri': self._last_known_uri, 'diagnostics': []})
@@ -216,7 +217,7 @@ def _on_before_destroy(self) -> None:
216217
# in unregistering ourselves from the session.
217218
if not self.session.exiting:
218219
# Only send textDocument/didClose when we are the only view left (i.e. there are no other clones).
219-
self._check_did_close()
220+
self._check_did_close(view)
220221
self.session.unregister_session_buffer_async(self)
221222

222223
def register_capability_async(
@@ -308,15 +309,15 @@ def on_revert_async(self, view: sublime.View) -> None:
308309

309310
on_reload_async = on_revert_async
310311

311-
def purge_changes_async(self, view: sublime.View) -> None:
312+
def purge_changes_async(self, view: sublime.View, suppress_requests: bool = False) -> None:
312313
if self._pending_changes is None:
313314
return
314315
sync_kind = self.text_sync_kind()
315316
if sync_kind == TextDocumentSyncKind.None_:
316317
return
317318
if sync_kind == TextDocumentSyncKind.Full:
318319
changes = None
319-
version = view.change_count()
320+
version = view.change_count() or self._pending_changes.version
320321
else:
321322
changes = self._pending_changes.changes
322323
version = self._pending_changes.version
@@ -329,23 +330,28 @@ def purge_changes_async(self, view: sublime.View) -> None:
329330
finally:
330331
self._pending_changes = None
331332
self.session.notify_plugin_on_session_buffer_change(self)
332-
sublime.set_timeout_async(lambda: self._on_after_change_async(view, version))
333+
sublime.set_timeout_async(lambda: self._on_after_change_async(view, version, suppress_requests))
333334

334-
def _on_after_change_async(self, view: sublime.View, version: int) -> None:
335+
def _on_after_change_async(self, view: sublime.View, version: int, suppress_requests: bool = False) -> None:
335336
if self._is_saving:
336337
self._has_changed_during_save = True
337338
return
338-
self._do_color_boxes_async(view, version)
339-
self.do_document_diagnostic_async(view, version)
340-
if self.session.config.diagnostics_mode == "workspace" and \
341-
not self.session.workspace_diagnostics_pending_response and \
342-
self.session.has_capability('diagnosticProvider.workspaceDiagnostics'):
343-
self._workspace_diagnostics_debouncer_async.debounce(
344-
self.session.do_workspace_diagnostics_async, timeout_ms=WORKSPACE_DIAGNOSTICS_TIMEOUT)
345-
self.do_semantic_tokens_async(view)
346-
if userprefs().link_highlight_style in ("underline", "none"):
347-
self._do_document_link_async(view, version)
348-
self.do_inlay_hints_async(view)
339+
if suppress_requests:
340+
return
341+
try:
342+
self._do_color_boxes_async(view, version)
343+
self.do_document_diagnostic_async(view, version)
344+
if self.session.config.diagnostics_mode == "workspace" and \
345+
not self.session.workspace_diagnostics_pending_response and \
346+
self.session.has_capability('diagnosticProvider.workspaceDiagnostics'):
347+
self._workspace_diagnostics_debouncer_async.debounce(
348+
self.session.do_workspace_diagnostics_async, timeout_ms=WORKSPACE_DIAGNOSTICS_TIMEOUT)
349+
self.do_semantic_tokens_async(view)
350+
if userprefs().link_highlight_style in ("underline", "none"):
351+
self._do_document_link_async(view, version)
352+
self.do_inlay_hints_async(view)
353+
except MissingUriError:
354+
pass
349355

350356
def on_pre_save_async(self, view: sublime.View) -> None:
351357
self._is_saving = True
@@ -357,7 +363,7 @@ def on_pre_save_async(self, view: sublime.View) -> None:
357363
def on_post_save_async(self, view: sublime.View, new_uri: DocumentUri) -> None:
358364
self._is_saving = False
359365
if new_uri != self._last_known_uri:
360-
self._check_did_close()
366+
self._check_did_close(view)
361367
self._last_known_uri = new_uri
362368
self._check_did_open(view)
363369
else:

tests/test_single_document.py

+48-25
Original file line numberDiff line numberDiff line change
@@ -84,31 +84,6 @@ def test_did_close(self) -> 'Generator':
8484
self.view.close()
8585
yield from self.await_message("textDocument/didClose")
8686

87-
def test_did_change(self) -> 'Generator':
88-
assert self.view
89-
self.maxDiff = None
90-
self.insert_characters("A")
91-
yield from self.await_message("textDocument/didChange")
92-
# multiple changes are batched into one didChange notification
93-
self.insert_characters("B\n")
94-
self.insert_characters("🙂\n")
95-
self.insert_characters("D")
96-
promise = YieldPromise()
97-
yield from self.await_message("textDocument/didChange", promise)
98-
self.assertEqual(promise.result(), {
99-
'contentChanges': [
100-
{'rangeLength': 0, 'range': {'start': {'line': 0, 'character': 1}, 'end': {'line': 0, 'character': 1}}, 'text': 'B'}, # noqa
101-
{'rangeLength': 0, 'range': {'start': {'line': 0, 'character': 2}, 'end': {'line': 0, 'character': 2}}, 'text': '\n'}, # noqa
102-
{'rangeLength': 0, 'range': {'start': {'line': 1, 'character': 0}, 'end': {'line': 1, 'character': 0}}, 'text': '🙂'}, # noqa
103-
# Note that this is character offset (2) is correct (UTF-16).
104-
{'rangeLength': 0, 'range': {'start': {'line': 1, 'character': 2}, 'end': {'line': 1, 'character': 2}}, 'text': '\n'}, # noqa
105-
{'rangeLength': 0, 'range': {'start': {'line': 2, 'character': 0}, 'end': {'line': 2, 'character': 0}}, 'text': 'D'}], # noqa
106-
'textDocument': {
107-
'version': self.view.change_count(),
108-
'uri': filename_to_uri(TEST_FILE_PATH)
109-
}
110-
})
111-
11287
def test_sends_save_with_purge(self) -> 'Generator':
11388
assert self.view
11489
self.view.settings().set("lsp_format_on_save", False)
@@ -371,6 +346,54 @@ def test_progress(self) -> 'Generator':
371346
self.assertEqual(result, {"general": "kenobi"})
372347

373348

349+
class SingleDocumentTestCase2(TextDocumentTestCase):
350+
351+
def test_did_change(self) -> 'Generator':
352+
assert self.view
353+
self.maxDiff = None
354+
self.insert_characters("A")
355+
yield from self.await_message("textDocument/didChange")
356+
# multiple changes are batched into one didChange notification
357+
self.insert_characters("B\n")
358+
self.insert_characters("🙂\n")
359+
self.insert_characters("D")
360+
promise = YieldPromise()
361+
yield from self.await_message("textDocument/didChange", promise)
362+
self.assertEqual(promise.result(), {
363+
'contentChanges': [
364+
{'rangeLength': 0, 'range': {'start': {'line': 0, 'character': 1}, 'end': {'line': 0, 'character': 1}}, 'text': 'B'}, # noqa
365+
{'rangeLength': 0, 'range': {'start': {'line': 0, 'character': 2}, 'end': {'line': 0, 'character': 2}}, 'text': '\n'}, # noqa
366+
{'rangeLength': 0, 'range': {'start': {'line': 1, 'character': 0}, 'end': {'line': 1, 'character': 0}}, 'text': '🙂'}, # noqa
367+
# Note that this is character offset (2) is correct (UTF-16).
368+
{'rangeLength': 0, 'range': {'start': {'line': 1, 'character': 2}, 'end': {'line': 1, 'character': 2}}, 'text': '\n'}, # noqa
369+
{'rangeLength': 0, 'range': {'start': {'line': 2, 'character': 0}, 'end': {'line': 2, 'character': 0}}, 'text': 'D'}], # noqa
370+
'textDocument': {
371+
'version': self.view.change_count(),
372+
'uri': filename_to_uri(TEST_FILE_PATH)
373+
}
374+
})
375+
376+
377+
class SingleDocumentTestCase3(TextDocumentTestCase):
378+
379+
@classmethod
380+
def get_test_name(cls) -> str:
381+
return "testfile2"
382+
383+
def test_did_change_before_did_close(self) -> 'Generator':
384+
assert self.view
385+
self.view.window().run_command("chain", {
386+
"commands": [
387+
["insert", {"characters": "TEST"}],
388+
["save", {"async": False}],
389+
["close", {}]
390+
]
391+
})
392+
yield from self.await_message('textDocument/didChange')
393+
# yield from self.await_message('textDocument/didSave') # TODO why is this not sent?
394+
yield from self.await_message('textDocument/didClose')
395+
396+
374397
class WillSaveWaitUntilTestCase(TextDocumentTestCase):
375398

376399
@classmethod

tests/testfile2.txt

Whitespace-only changes.

0 commit comments

Comments
 (0)