From 4f30db8564fcfde22c87238c122b8b8bbe202b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ch=C5=82odnicki?= Date: Sat, 20 Apr 2024 19:51:58 +0200 Subject: [PATCH 1/2] fix: check semantic capability through session buffer (#2453) * fix: check semantic capability through session buffer * type * type --- plugin/core/sessions.py | 9 ++++++--- plugin/session_buffer.py | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 0f660af8c..8bb621295 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -1814,9 +1814,12 @@ def _set_focused_sheet(self, sheet: Optional[sublime.Sheet]) -> None: self.window.focus_sheet(sheet) def decode_semantic_token( - self, token_type_encoded: int, token_modifiers_encoded: int) -> Tuple[str, List[str], Optional[str]]: - types_legend = tuple(cast(List[str], self.get_capability('semanticTokensProvider.legend.tokenTypes'))) - modifiers_legend = tuple(cast(List[str], self.get_capability('semanticTokensProvider.legend.tokenModifiers'))) + self, + types_legend: Tuple[str, ...], + modifiers_legend: Tuple[str, ...], + token_type_encoded: int, + token_modifiers_encoded: int + ) -> Tuple[str, List[str], Optional[str]]: return decode_semantic_token( types_legend, modifiers_legend, self._semantic_tokens_map, token_type_encoded, token_modifiers_encoded) diff --git a/plugin/session_buffer.py b/plugin/session_buffer.py index 71febb56b..c8f56ab4d 100644 --- a/plugin/session_buffer.py +++ b/plugin/session_buffer.py @@ -630,6 +630,8 @@ def _draw_semantic_tokens_async(self) -> None: scope_regions = dict() # type: Dict[int, Tuple[str, List[sublime.Region]]] prev_line = 0 prev_col_utf16 = 0 + types_legend = tuple(cast(List[str], self.get_capability('semanticTokensProvider.legend.tokenTypes'))) + modifiers_legend = tuple(cast(List[str], self.get_capability('semanticTokensProvider.legend.tokenModifiers'))) for idx in range(0, len(self.semantic_tokens.data), 5): delta_line = self.semantic_tokens.data[idx] delta_start_utf16 = self.semantic_tokens.data[idx + 1] @@ -644,7 +646,7 @@ def _draw_semantic_tokens_async(self) -> None: prev_line = line prev_col_utf16 = col_utf16 token_type, token_modifiers, scope = self.session.decode_semantic_token( - token_type_encoded, token_modifiers_encoded) + types_legend, modifiers_legend, token_type_encoded, token_modifiers_encoded) if scope is None: # We can still use the meta scope and draw highlighting regions for custom token types if there is a # color scheme rule for this particular token type. From 4b02dbdfeea9988da0e3e0d8586b6e789c60812e Mon Sep 17 00:00:00 2001 From: jwortmann Date: Sat, 20 Apr 2024 19:52:15 +0200 Subject: [PATCH 2/2] Ensure didChange notification is never sent after didClose (#2438) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Ensure didChange is never sent after didClose This fixes for example the Pyright warning LSP-pyright: Received change text document command for closed file 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 4dd2e91ee9f9bc6bf8b93082b5be4df7243b0c75. * Fix failing test on Linux * actually this test passes locally with this line uncommented * Revert, apparently it fails on the CI... This reverts commit 43ede82dba41c96e26c035bd4809ef6714782304. * 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 11c5ecbc82423bbcd8f08dc7243b3c20141a5fc7. --------- Co-authored-by: Предраг Николић --- plugin/session_buffer.py | 46 ++++++++++++---------- tests/test_single_document.py | 73 +++++++++++++++++++++++------------ tests/testfile2.txt | 0 3 files changed, 74 insertions(+), 45 deletions(-) create mode 100644 tests/testfile2.txt diff --git a/plugin/session_buffer.py b/plugin/session_buffer.py index c8f56ab4d..c4a6a317f 100644 --- a/plugin/session_buffer.py +++ b/plugin/session_buffer.py @@ -166,8 +166,9 @@ def _check_did_open(self, view: sublime.View) -> None: self._do_document_link_async(view, version) self.session.notify_plugin_on_session_buffer_change(self) - def _check_did_close(self) -> None: + def _check_did_close(self, view: sublime.View) -> None: if self.opened and self.should_notify_did_close(): + self.purge_changes_async(view, suppress_requests=True) self.session.send_notification(did_close(uri=self._last_known_uri)) self.opened = False @@ -202,9 +203,9 @@ def remove_session_view(self, sv: SessionViewProtocol) -> None: self._clear_semantic_token_regions(sv.view) self.session_views.remove(sv) if len(self.session_views) == 0: - self._on_before_destroy() + self._on_before_destroy(sv.view) - def _on_before_destroy(self) -> None: + def _on_before_destroy(self, view: sublime.View) -> None: self.remove_all_inlay_hints() if self.has_capability("diagnosticProvider") and self.session.config.diagnostics_mode == "open_files": self.session.m_textDocument_publishDiagnostics({'uri': self._last_known_uri, 'diagnostics': []}) @@ -216,7 +217,7 @@ def _on_before_destroy(self) -> None: # in unregistering ourselves from the session. if not self.session.exiting: # Only send textDocument/didClose when we are the only view left (i.e. there are no other clones). - self._check_did_close() + self._check_did_close(view) self.session.unregister_session_buffer_async(self) def register_capability_async( @@ -308,7 +309,7 @@ def on_revert_async(self, view: sublime.View) -> None: on_reload_async = on_revert_async - def purge_changes_async(self, view: sublime.View) -> None: + def purge_changes_async(self, view: sublime.View, suppress_requests: bool = False) -> None: if self._pending_changes is None: return sync_kind = self.text_sync_kind() @@ -316,7 +317,7 @@ def purge_changes_async(self, view: sublime.View) -> None: return if sync_kind == TextDocumentSyncKind.Full: changes = None - version = view.change_count() + version = view.change_count() or self._pending_changes.version else: changes = self._pending_changes.changes version = self._pending_changes.version @@ -329,23 +330,28 @@ def purge_changes_async(self, view: sublime.View) -> None: finally: self._pending_changes = None self.session.notify_plugin_on_session_buffer_change(self) - sublime.set_timeout_async(lambda: self._on_after_change_async(view, version)) + sublime.set_timeout_async(lambda: self._on_after_change_async(view, version, suppress_requests)) - def _on_after_change_async(self, view: sublime.View, version: int) -> None: + def _on_after_change_async(self, view: sublime.View, version: int, suppress_requests: bool = False) -> None: if self._is_saving: self._has_changed_during_save = True return - self._do_color_boxes_async(view, version) - self.do_document_diagnostic_async(view, version) - if self.session.config.diagnostics_mode == "workspace" and \ - not self.session.workspace_diagnostics_pending_response and \ - self.session.has_capability('diagnosticProvider.workspaceDiagnostics'): - self._workspace_diagnostics_debouncer_async.debounce( - self.session.do_workspace_diagnostics_async, timeout_ms=WORKSPACE_DIAGNOSTICS_TIMEOUT) - self.do_semantic_tokens_async(view) - if userprefs().link_highlight_style in ("underline", "none"): - self._do_document_link_async(view, version) - self.do_inlay_hints_async(view) + if suppress_requests: + return + try: + self._do_color_boxes_async(view, version) + self.do_document_diagnostic_async(view, version) + if self.session.config.diagnostics_mode == "workspace" and \ + not self.session.workspace_diagnostics_pending_response and \ + self.session.has_capability('diagnosticProvider.workspaceDiagnostics'): + self._workspace_diagnostics_debouncer_async.debounce( + self.session.do_workspace_diagnostics_async, timeout_ms=WORKSPACE_DIAGNOSTICS_TIMEOUT) + self.do_semantic_tokens_async(view) + if userprefs().link_highlight_style in ("underline", "none"): + self._do_document_link_async(view, version) + self.do_inlay_hints_async(view) + except MissingUriError: + pass def on_pre_save_async(self, view: sublime.View) -> None: self._is_saving = True @@ -357,7 +363,7 @@ def on_pre_save_async(self, view: sublime.View) -> None: def on_post_save_async(self, view: sublime.View, new_uri: DocumentUri) -> None: self._is_saving = False if new_uri != self._last_known_uri: - self._check_did_close() + self._check_did_close(view) self._last_known_uri = new_uri self._check_did_open(view) else: diff --git a/tests/test_single_document.py b/tests/test_single_document.py index 4d1bb10d3..673f1842f 100644 --- a/tests/test_single_document.py +++ b/tests/test_single_document.py @@ -84,31 +84,6 @@ def test_did_close(self) -> 'Generator': self.view.close() yield from self.await_message("textDocument/didClose") - def test_did_change(self) -> 'Generator': - assert self.view - self.maxDiff = None - self.insert_characters("A") - yield from self.await_message("textDocument/didChange") - # multiple changes are batched into one didChange notification - self.insert_characters("B\n") - self.insert_characters("🙂\n") - self.insert_characters("D") - promise = YieldPromise() - yield from self.await_message("textDocument/didChange", promise) - self.assertEqual(promise.result(), { - 'contentChanges': [ - {'rangeLength': 0, 'range': {'start': {'line': 0, 'character': 1}, 'end': {'line': 0, 'character': 1}}, 'text': 'B'}, # noqa - {'rangeLength': 0, 'range': {'start': {'line': 0, 'character': 2}, 'end': {'line': 0, 'character': 2}}, 'text': '\n'}, # noqa - {'rangeLength': 0, 'range': {'start': {'line': 1, 'character': 0}, 'end': {'line': 1, 'character': 0}}, 'text': '🙂'}, # noqa - # Note that this is character offset (2) is correct (UTF-16). - {'rangeLength': 0, 'range': {'start': {'line': 1, 'character': 2}, 'end': {'line': 1, 'character': 2}}, 'text': '\n'}, # noqa - {'rangeLength': 0, 'range': {'start': {'line': 2, 'character': 0}, 'end': {'line': 2, 'character': 0}}, 'text': 'D'}], # noqa - 'textDocument': { - 'version': self.view.change_count(), - 'uri': filename_to_uri(TEST_FILE_PATH) - } - }) - def test_sends_save_with_purge(self) -> 'Generator': assert self.view self.view.settings().set("lsp_format_on_save", False) @@ -371,6 +346,54 @@ def test_progress(self) -> 'Generator': self.assertEqual(result, {"general": "kenobi"}) +class SingleDocumentTestCase2(TextDocumentTestCase): + + def test_did_change(self) -> 'Generator': + assert self.view + self.maxDiff = None + self.insert_characters("A") + yield from self.await_message("textDocument/didChange") + # multiple changes are batched into one didChange notification + self.insert_characters("B\n") + self.insert_characters("🙂\n") + self.insert_characters("D") + promise = YieldPromise() + yield from self.await_message("textDocument/didChange", promise) + self.assertEqual(promise.result(), { + 'contentChanges': [ + {'rangeLength': 0, 'range': {'start': {'line': 0, 'character': 1}, 'end': {'line': 0, 'character': 1}}, 'text': 'B'}, # noqa + {'rangeLength': 0, 'range': {'start': {'line': 0, 'character': 2}, 'end': {'line': 0, 'character': 2}}, 'text': '\n'}, # noqa + {'rangeLength': 0, 'range': {'start': {'line': 1, 'character': 0}, 'end': {'line': 1, 'character': 0}}, 'text': '🙂'}, # noqa + # Note that this is character offset (2) is correct (UTF-16). + {'rangeLength': 0, 'range': {'start': {'line': 1, 'character': 2}, 'end': {'line': 1, 'character': 2}}, 'text': '\n'}, # noqa + {'rangeLength': 0, 'range': {'start': {'line': 2, 'character': 0}, 'end': {'line': 2, 'character': 0}}, 'text': 'D'}], # noqa + 'textDocument': { + 'version': self.view.change_count(), + 'uri': filename_to_uri(TEST_FILE_PATH) + } + }) + + +class SingleDocumentTestCase3(TextDocumentTestCase): + + @classmethod + def get_test_name(cls) -> str: + return "testfile2" + + def test_did_change_before_did_close(self) -> 'Generator': + assert self.view + self.view.window().run_command("chain", { + "commands": [ + ["insert", {"characters": "TEST"}], + ["save", {"async": False}], + ["close", {}] + ] + }) + yield from self.await_message('textDocument/didChange') + # yield from self.await_message('textDocument/didSave') # TODO why is this not sent? + yield from self.await_message('textDocument/didClose') + + class WillSaveWaitUntilTestCase(TextDocumentTestCase): @classmethod diff --git a/tests/testfile2.txt b/tests/testfile2.txt new file mode 100644 index 000000000..e69de29bb