From 9c9493a7acc2428614f51c5c34a7168f8781902f Mon Sep 17 00:00:00 2001 From: Janos Wortmann Date: Mon, 25 Mar 2024 22:29:28 +0100 Subject: [PATCH 01/20] 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. --- plugin/session_buffer.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/plugin/session_buffer.py b/plugin/session_buffer.py index 71febb56b..56bc403a9 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) 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() if view.is_valid() else self._pending_changes.version else: changes = self._pending_changes.changes version = self._pending_changes.version @@ -329,12 +330,14 @@ 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 + if suppress_requests or not view.is_valid(): + return self._do_color_boxes_async(view, version) self.do_document_diagnostic_async(view, version) if self.session.config.diagnostics_mode == "workspace" and \ @@ -357,7 +360,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: From ba364a78d6ef2f086d1d4c7caea77889550307ab Mon Sep 17 00:00:00 2001 From: Janos Wortmann Date: Tue, 26 Mar 2024 17:19:26 +0100 Subject: [PATCH 02/20] Missed something --- plugin/session_buffer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/session_buffer.py b/plugin/session_buffer.py index 56bc403a9..deb5f85c5 100644 --- a/plugin/session_buffer.py +++ b/plugin/session_buffer.py @@ -168,7 +168,7 @@ def _check_did_open(self, view: sublime.View) -> None: def _check_did_close(self, view: sublime.View) -> None: if self.opened and self.should_notify_did_close(): - self.purge_changes_async(view) + self.purge_changes_async(view, suppress_requests=True) self.session.send_notification(did_close(uri=self._last_known_uri)) self.opened = False From c211f9962ff432e45caab4846488d42fb5a288f1 Mon Sep 17 00:00:00 2001 From: Janos Wortmann Date: Sat, 30 Mar 2024 22:50:11 +0100 Subject: [PATCH 03/20] Add test --- tests/test_single_document.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_single_document.py b/tests/test_single_document.py index 4d1bb10d3..d48083157 100644 --- a/tests/test_single_document.py +++ b/tests/test_single_document.py @@ -109,6 +109,19 @@ def test_did_change(self) -> 'Generator': } }) + 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') + def test_sends_save_with_purge(self) -> 'Generator': assert self.view self.view.settings().set("lsp_format_on_save", False) From b8b0d9b1c47c50ab9fe66eb39f0d3a36fb9b761c Mon Sep 17 00:00:00 2001 From: Janos Wortmann Date: Sat, 30 Mar 2024 22:57:58 +0100 Subject: [PATCH 04/20] Maybe like this? --- tests/test_single_document.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/tests/test_single_document.py b/tests/test_single_document.py index d48083157..6f80dd644 100644 --- a/tests/test_single_document.py +++ b/tests/test_single_document.py @@ -109,19 +109,6 @@ def test_did_change(self) -> 'Generator': } }) - 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') - def test_sends_save_with_purge(self) -> 'Generator': assert self.view self.view.settings().set("lsp_format_on_save", False) @@ -411,3 +398,19 @@ def test_will_save_wait_until(self) -> 'Generator': text = self.view.substr(sublime.Region(0, self.view.size())) self.assertEquals("BBB", text) yield from self.await_clear_view_and_save() + + +class AnotherDocumentTestCase(TextDocumentTestCase): + + 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') From d430b9f943885a7e19c2b30a90c6f458cba6fa13 Mon Sep 17 00:00:00 2001 From: Janos Wortmann Date: Sat, 30 Mar 2024 23:09:23 +0100 Subject: [PATCH 05/20] Try something else --- tests/test_single_document.py | 4 ++++ tests/testfile2.txt | 0 2 files changed, 4 insertions(+) create mode 100644 tests/testfile2.txt diff --git a/tests/test_single_document.py b/tests/test_single_document.py index 6f80dd644..4ac3b4255 100644 --- a/tests/test_single_document.py +++ b/tests/test_single_document.py @@ -402,6 +402,10 @@ def test_will_save_wait_until(self) -> 'Generator': class AnotherDocumentTestCase(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", { diff --git a/tests/testfile2.txt b/tests/testfile2.txt new file mode 100644 index 000000000..e69de29bb From 0fc0e1e67817ea366acaac89942ccdc39c2a0a7e Mon Sep 17 00:00:00 2001 From: Janos Wortmann Date: Sun, 31 Mar 2024 20:40:07 +0200 Subject: [PATCH 06/20] 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. --- plugin/session_buffer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/session_buffer.py b/plugin/session_buffer.py index deb5f85c5..68f1828e8 100644 --- a/plugin/session_buffer.py +++ b/plugin/session_buffer.py @@ -317,7 +317,7 @@ def purge_changes_async(self, view: sublime.View, suppress_requests: bool = Fals return if sync_kind == TextDocumentSyncKind.Full: changes = None - version = view.change_count() if view.is_valid() else self._pending_changes.version + version = view.change_count() or self._pending_changes.version else: changes = self._pending_changes.changes version = self._pending_changes.version From 9af21f48e1d862e332d94c7308bffde0080c7196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Sun, 7 Apr 2024 13:35:12 +0200 Subject: [PATCH 07/20] only ubuntu --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4226f7909..07a2d9641 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,7 +20,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macOS-latest, windows-latest] + os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 From f9bab9fa29bc294849bafe7f3961bdf660e13420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Sun, 7 Apr 2024 13:40:44 +0200 Subject: [PATCH 08/20] sometimes things dont need to make sense --- tests/test_configurations.py | 51 ++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/tests/test_configurations.py b/tests/test_configurations.py index 93af0a0ef..533c12229 100644 --- a/tests/test_configurations.py +++ b/tests/test_configurations.py @@ -1,12 +1,13 @@ from LSP.plugin.core.configurations import WindowConfigManager from test_mocks import DISABLED_CONFIG from test_mocks import TEST_CONFIG +from unittest import TestCase from unittest.mock import MagicMock +from unittesting import ViewTestCase import sublime -import unittest -class GlobalConfigManagerTests(unittest.TestCase): +class GlobalConfigManagerTests(TestCase): def test_empty_configs(self): window_mgr = WindowConfigManager(sublime.active_window(), {}) @@ -24,35 +25,29 @@ def test_override_config(self): self.assertFalse(list(window_mgr.all.values())[0].enabled) -class WindowConfigManagerTests(unittest.TestCase): +class WindowConfigManagerTests(ViewTestCase): def test_no_configs(self): - view = sublime.active_window().active_view() - self.assertIsNotNone(view) - assert view - manager = WindowConfigManager(sublime.active_window(), {}) - self.assertEqual(list(manager.match_view(view)), []) + self.assertIsNotNone(self.view) + self.assertIsNotNone(self.window) + manager = WindowConfigManager(self.window, {}) + self.assertEqual(list(manager.match_view(self.view)), []) def test_with_single_config(self): - window = sublime.active_window() - view = window.active_view() - self.assertIsNotNone(view) - assert view - manager = WindowConfigManager(window, {TEST_CONFIG.name: TEST_CONFIG}) - view.syntax = MagicMock(return_value=sublime.Syntax( + self.assertIsNotNone(self.view) + self.assertIsNotNone(self.window) + manager = WindowConfigManager(self.window, {TEST_CONFIG.name: TEST_CONFIG}) + self.view.syntax = MagicMock(return_value=sublime.Syntax( path="Packages/Text/Plain text.tmLanguage", name="Plain Text", scope="text.plain", hidden=False )) - view.settings().set("lsp_uri", "file:///foo/bar.txt") - self.assertEqual(list(manager.match_view(view)), [TEST_CONFIG]) + self.view.settings().set("lsp_uri", "file:///foo/bar.txt") + self.assertEqual(list(manager.match_view(self.view)), [TEST_CONFIG]) def test_applies_project_settings(self): - window = sublime.active_window() - view = window.active_view() - assert view - window.project_data = MagicMock(return_value={ + self.window.project_data = MagicMock(return_value={ "settings": { "LSP": { "test": { @@ -61,24 +56,22 @@ def test_applies_project_settings(self): } } }) - manager = WindowConfigManager(window, {DISABLED_CONFIG.name: DISABLED_CONFIG}) - view.syntax = MagicMock(return_value=sublime.Syntax( + manager = WindowConfigManager(self.window, {DISABLED_CONFIG.name: DISABLED_CONFIG}) + self.view.syntax = MagicMock(return_value=sublime.Syntax( path="Packages/Text/Plain text.tmLanguage", name="Plain Text", scope="text.plain", hidden=False )) - view.settings().set("lsp_uri", "file:///foo/bar.txt") - configs = list(manager.match_view(view)) + self.view.settings().set("lsp_uri", "file:///foo/bar.txt") + configs = list(manager.match_view(self.view)) self.assertEqual(len(configs), 1) config = configs[0] self.assertEqual(DISABLED_CONFIG.name, config.name) self.assertTrue(config.enabled) def test_disables_temporarily(self): - window = sublime.active_window() - view = window.active_view() - window.project_data = MagicMock(return_value={ + self.window.project_data = MagicMock(return_value={ "settings": { "LSP": { "test": { @@ -88,7 +81,7 @@ def test_disables_temporarily(self): } }) - manager = WindowConfigManager(window, {DISABLED_CONFIG.name: DISABLED_CONFIG}) + manager = WindowConfigManager(self.window, {DISABLED_CONFIG.name: DISABLED_CONFIG}) # disables config in-memory manager.disable_config(DISABLED_CONFIG.name, only_for_session=True) - self.assertFalse(any(manager.match_view(view))) + self.assertFalse(any(manager.match_view(self.view))) From ea474985d30d9afbb6a4aad223b1112b38db3a86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Sun, 7 Apr 2024 13:45:03 +0200 Subject: [PATCH 09/20] Revert "sometimes things dont need to make sense" This reverts commit f9bab9fa29bc294849bafe7f3961bdf660e13420. --- tests/test_configurations.py | 51 ++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/tests/test_configurations.py b/tests/test_configurations.py index 533c12229..93af0a0ef 100644 --- a/tests/test_configurations.py +++ b/tests/test_configurations.py @@ -1,13 +1,12 @@ from LSP.plugin.core.configurations import WindowConfigManager from test_mocks import DISABLED_CONFIG from test_mocks import TEST_CONFIG -from unittest import TestCase from unittest.mock import MagicMock -from unittesting import ViewTestCase import sublime +import unittest -class GlobalConfigManagerTests(TestCase): +class GlobalConfigManagerTests(unittest.TestCase): def test_empty_configs(self): window_mgr = WindowConfigManager(sublime.active_window(), {}) @@ -25,29 +24,35 @@ def test_override_config(self): self.assertFalse(list(window_mgr.all.values())[0].enabled) -class WindowConfigManagerTests(ViewTestCase): +class WindowConfigManagerTests(unittest.TestCase): def test_no_configs(self): - self.assertIsNotNone(self.view) - self.assertIsNotNone(self.window) - manager = WindowConfigManager(self.window, {}) - self.assertEqual(list(manager.match_view(self.view)), []) + view = sublime.active_window().active_view() + self.assertIsNotNone(view) + assert view + manager = WindowConfigManager(sublime.active_window(), {}) + self.assertEqual(list(manager.match_view(view)), []) def test_with_single_config(self): - self.assertIsNotNone(self.view) - self.assertIsNotNone(self.window) - manager = WindowConfigManager(self.window, {TEST_CONFIG.name: TEST_CONFIG}) - self.view.syntax = MagicMock(return_value=sublime.Syntax( + window = sublime.active_window() + view = window.active_view() + self.assertIsNotNone(view) + assert view + manager = WindowConfigManager(window, {TEST_CONFIG.name: TEST_CONFIG}) + view.syntax = MagicMock(return_value=sublime.Syntax( path="Packages/Text/Plain text.tmLanguage", name="Plain Text", scope="text.plain", hidden=False )) - self.view.settings().set("lsp_uri", "file:///foo/bar.txt") - self.assertEqual(list(manager.match_view(self.view)), [TEST_CONFIG]) + view.settings().set("lsp_uri", "file:///foo/bar.txt") + self.assertEqual(list(manager.match_view(view)), [TEST_CONFIG]) def test_applies_project_settings(self): - self.window.project_data = MagicMock(return_value={ + window = sublime.active_window() + view = window.active_view() + assert view + window.project_data = MagicMock(return_value={ "settings": { "LSP": { "test": { @@ -56,22 +61,24 @@ def test_applies_project_settings(self): } } }) - manager = WindowConfigManager(self.window, {DISABLED_CONFIG.name: DISABLED_CONFIG}) - self.view.syntax = MagicMock(return_value=sublime.Syntax( + manager = WindowConfigManager(window, {DISABLED_CONFIG.name: DISABLED_CONFIG}) + view.syntax = MagicMock(return_value=sublime.Syntax( path="Packages/Text/Plain text.tmLanguage", name="Plain Text", scope="text.plain", hidden=False )) - self.view.settings().set("lsp_uri", "file:///foo/bar.txt") - configs = list(manager.match_view(self.view)) + view.settings().set("lsp_uri", "file:///foo/bar.txt") + configs = list(manager.match_view(view)) self.assertEqual(len(configs), 1) config = configs[0] self.assertEqual(DISABLED_CONFIG.name, config.name) self.assertTrue(config.enabled) def test_disables_temporarily(self): - self.window.project_data = MagicMock(return_value={ + window = sublime.active_window() + view = window.active_view() + window.project_data = MagicMock(return_value={ "settings": { "LSP": { "test": { @@ -81,7 +88,7 @@ def test_disables_temporarily(self): } }) - manager = WindowConfigManager(self.window, {DISABLED_CONFIG.name: DISABLED_CONFIG}) + manager = WindowConfigManager(window, {DISABLED_CONFIG.name: DISABLED_CONFIG}) # disables config in-memory manager.disable_config(DISABLED_CONFIG.name, only_for_session=True) - self.assertFalse(any(manager.match_view(self.view))) + self.assertFalse(any(manager.match_view(view))) From c5a7844f108934ec8e8006645bbcd58c80700e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Mon, 8 Apr 2024 11:49:27 +0200 Subject: [PATCH 10/20] it is just a race condition somewhere... --- tests/test_code_actions.py | 444 ---------- tests/test_collections.py | 181 ----- tests/test_completion.py | 1086 ------------------------- tests/test_configs.py | 179 ---- tests/test_configurations.py | 94 --- tests/test_documents.py | 134 --- tests/test_edit.py | 309 ------- tests/test_file_watcher.py | 122 --- tests/test_message_request_handler.py | 22 - tests/test_protocol.py | 65 -- tests/test_rename_panel.py | 50 -- tests/test_server_notifications.py | 81 -- tests/test_server_panel_circular.py | 41 - tests/test_server_requests.py | 224 ----- tests/test_session.py | 253 ------ tests/test_signature_help.py | 253 ------ tests/test_types.py | 256 ------ tests/test_url.py | 79 -- tests/test_views.py | 408 ---------- tests/test_workspace.py | 48 -- tests/testfile2.txt | 1 + 21 files changed, 1 insertion(+), 4329 deletions(-) delete mode 100644 tests/test_code_actions.py delete mode 100644 tests/test_collections.py delete mode 100644 tests/test_completion.py delete mode 100644 tests/test_configs.py delete mode 100644 tests/test_configurations.py delete mode 100644 tests/test_documents.py delete mode 100644 tests/test_edit.py delete mode 100644 tests/test_message_request_handler.py delete mode 100644 tests/test_protocol.py delete mode 100644 tests/test_rename_panel.py delete mode 100644 tests/test_server_notifications.py delete mode 100644 tests/test_server_panel_circular.py delete mode 100644 tests/test_server_requests.py delete mode 100644 tests/test_session.py delete mode 100644 tests/test_signature_help.py delete mode 100644 tests/test_types.py delete mode 100644 tests/test_url.py delete mode 100644 tests/test_views.py delete mode 100644 tests/test_workspace.py diff --git a/tests/test_code_actions.py b/tests/test_code_actions.py deleted file mode 100644 index e4231917e..000000000 --- a/tests/test_code_actions.py +++ /dev/null @@ -1,444 +0,0 @@ -from copy import deepcopy -from LSP.plugin.code_actions import get_matching_on_save_kinds, kinds_include_kind -from LSP.plugin.core.protocol import Point, Range -from LSP.plugin.core.typing import Any, Dict, Generator, List, Tuple, Optional -from LSP.plugin.core.url import filename_to_uri -from LSP.plugin.core.views import entire_content -from LSP.plugin.documents import DocumentSyncListener -from LSP.plugin.session_view import SessionView -from LSP.plugin.core.views import versioned_text_document_identifier -from setup import TextDocumentTestCase -from test_single_document import TEST_FILE_PATH -import unittest -import sublime - -TEST_FILE_URI = filename_to_uri(TEST_FILE_PATH) - - -def edit_to_lsp(edit: Tuple[str, Range]) -> Dict[str, Any]: - return {"newText": edit[0], "range": edit[1]} - - -def range_from_points(start: Point, end: Point) -> Range: - return { - 'start': start.to_lsp(), - 'end': end.to_lsp() - } - - -def create_code_action_edit(view: sublime.View, version: int, edits: List[Tuple[str, Range]]) -> Dict[str, Any]: - return { - "documentChanges": [ - { - "textDocument": versioned_text_document_identifier(view, version), - "edits": list(map(edit_to_lsp, edits)) - } - ] - } - - -def create_command(command_name: str, command_args: Optional[List[Any]] = None) -> Dict[str, Any]: - result = {"command": command_name} # type: Dict[str, Any] - if command_args is not None: - result["arguments"] = command_args - return result - - -def create_test_code_action(view: sublime.View, version: int, edits: List[Tuple[str, Range]], - kind: Optional[str] = None) -> Dict[str, Any]: - action = { - "title": "Fix errors", - "edit": create_code_action_edit(view, version, edits) - } - if kind: - action['kind'] = kind - return action - - -def create_test_code_action2(command_name: str, command_args: Optional[List[Any]] = None, - kind: Optional[str] = None) -> Dict[str, Any]: - action = { - "title": "Fix errors", - "command": create_command(command_name, command_args) - } - if kind: - action['kind'] = kind - return action - - -def create_disabled_code_action(view: sublime.View, version: int, edits: List[Tuple[str, Range]]) -> Dict[str, Any]: - action = { - "title": "Fix errors", - "edit": create_code_action_edit(view, version, edits), - "disabled": { - "reason": "Do not use" - }, - } - return action - - -def create_test_diagnostics(diagnostics: List[Tuple[str, Range]]) -> Dict: - def diagnostic_to_lsp(diagnostic: Tuple[str, Range]) -> Dict: - message, range = diagnostic - return { - "message": message, - "range": range - } - return { - "uri": TEST_FILE_URI, - "diagnostics": list(map(diagnostic_to_lsp, diagnostics)) - } - - -class CodeActionsOnSaveTestCase(TextDocumentTestCase): - - @classmethod - def init_view_settings(cls) -> None: - super().init_view_settings() - # "quickfix" is not supported but its here for testing purposes - cls.view.settings().set('lsp_code_actions_on_save', {'source.fixAll': True, 'quickfix': True}) - - @classmethod - def get_test_server_capabilities(cls) -> dict: - capabilities = deepcopy(super().get_test_server_capabilities()) - capabilities['capabilities']['codeActionProvider'] = {'codeActionKinds': ['quickfix', 'source.fixAll']} - return capabilities - - def doCleanups(self) -> Generator: - yield from self.await_clear_view_and_save() - yield from super().doCleanups() - - def test_applies_matching_kind(self) -> Generator: - yield from self._setup_document_with_missing_semicolon() - code_action_kind = 'source.fixAll' - code_action = create_test_code_action( - self.view, - self.view.change_count(), - [(';', range_from_points(Point(0, 11), Point(0, 11)))], - code_action_kind - ) - self.set_response('textDocument/codeAction', [code_action]) - self.view.run_command('lsp_save', {'async': True}) - yield from self.await_message('textDocument/codeAction') - yield from self.await_message('textDocument/didSave') - self.assertEquals(entire_content(self.view), 'const x = 1;') - self.assertEquals(self.view.is_dirty(), False) - - def test_requests_with_diagnostics(self) -> Generator: - yield from self._setup_document_with_missing_semicolon() - code_action_kind = 'source.fixAll' - code_action = create_test_code_action( - self.view, - self.view.change_count(), - [(';', range_from_points(Point(0, 11), Point(0, 11)))], - code_action_kind - ) - self.set_response('textDocument/codeAction', [code_action]) - self.view.run_command('lsp_save', {'async': True}) - code_action_request = yield from self.await_message('textDocument/codeAction') - self.assertEquals(len(code_action_request['context']['diagnostics']), 1) - self.assertEquals(code_action_request['context']['diagnostics'][0]['message'], 'Missing semicolon') - yield from self.await_message('textDocument/didSave') - self.assertEquals(entire_content(self.view), 'const x = 1;') - self.assertEquals(self.view.is_dirty(), False) - - def test_applies_only_one_pass(self) -> Generator: - self.insert_characters('const x = 1') - initial_change_count = self.view.change_count() - yield from self.await_client_notification( - "textDocument/publishDiagnostics", - create_test_diagnostics([ - ('Missing semicolon', range_from_points(Point(0, 11), Point(0, 11))), - ]) - ) - code_action_kind = 'source.fixAll' - yield from self.set_responses([ - ( - 'textDocument/codeAction', - [ - create_test_code_action( - self.view, - initial_change_count, - [(';', range_from_points(Point(0, 11), Point(0, 11)))], - code_action_kind - ) - ] - ), - ( - 'textDocument/codeAction', - [ - create_test_code_action( - self.view, - initial_change_count + 1, - [('\nAnd again!', range_from_points(Point(0, 12), Point(0, 12)))], - code_action_kind - ) - ] - ), - ]) - self.view.run_command('lsp_save', {'async': True}) - # Wait for the view to be saved - yield lambda: not self.view.is_dirty() - self.assertEquals(entire_content(self.view), 'const x = 1;') - - def test_applies_immediately_after_text_change(self) -> Generator: - self.insert_characters('const x = 1') - code_action_kind = 'source.fixAll' - code_action = create_test_code_action( - self.view, - self.view.change_count(), - [(';', range_from_points(Point(0, 11), Point(0, 11)))], - code_action_kind - ) - self.set_response('textDocument/codeAction', [code_action]) - self.view.run_command('lsp_save', {'async': True}) - yield from self.await_message('textDocument/codeAction') - yield from self.await_message('textDocument/didSave') - self.assertEquals(entire_content(self.view), 'const x = 1;') - self.assertEquals(self.view.is_dirty(), False) - - def test_no_fix_on_non_matching_kind(self) -> Generator: - yield from self._setup_document_with_missing_semicolon() - initial_content = 'const x = 1' - self.view.run_command('lsp_save', {'async': True}) - yield from self.await_message('textDocument/didSave') - self.assertEquals(entire_content(self.view), initial_content) - self.assertEquals(self.view.is_dirty(), False) - - def test_does_not_apply_unsupported_kind(self) -> Generator: - yield from self._setup_document_with_missing_semicolon() - code_action_kind = 'quickfix' - code_action = create_test_code_action( - self.view, - self.view.change_count(), - [(';', range_from_points(Point(0, 11), Point(0, 11)))], - code_action_kind - ) - self.set_response('textDocument/codeAction', [code_action]) - self.view.run_command('lsp_save', {'async': True}) - yield from self.await_message('textDocument/didSave') - self.assertEquals(entire_content(self.view), 'const x = 1') - - def _setup_document_with_missing_semicolon(self) -> Generator: - self.insert_characters('const x = 1') - yield from self.await_message("textDocument/didChange") - yield from self.await_client_notification( - "textDocument/publishDiagnostics", - create_test_diagnostics([ - ('Missing semicolon', range_from_points(Point(0, 11), Point(0, 11))), - ]) - ) - - -class CodeActionMatchingTestCase(unittest.TestCase): - def test_does_not_match(self) -> None: - actual = get_matching_on_save_kinds({'a.x': True}, ['a.b']) - expected = [] # type: List[str] - self.assertEquals(actual, expected) - - def test_matches_exact_action(self) -> None: - actual = get_matching_on_save_kinds({'a.b': True}, ['a.b']) - expected = ['a.b'] - self.assertEquals(actual, expected) - - def test_matches_more_specific_action(self) -> None: - actual = get_matching_on_save_kinds({'a.b': True}, ['a.b.c']) - expected = ['a.b.c'] - self.assertEquals(actual, expected) - - def test_does_not_match_disabled_action(self) -> None: - actual = get_matching_on_save_kinds({'a.b': True, 'a.b.c': False}, ['a.b.c']) - expected = [] # type: List[str] - self.assertEquals(actual, expected) - - def test_kind_matching(self) -> None: - # Positive - self.assertTrue(kinds_include_kind(['a'], 'a.b')) - self.assertTrue(kinds_include_kind(['a.b'], 'a.b')) - self.assertTrue(kinds_include_kind(['a.b', 'b'], 'b.c')) - # Negative - self.assertFalse(kinds_include_kind(['a'], 'b.a')) - self.assertFalse(kinds_include_kind(['a.b'], 'b')) - self.assertFalse(kinds_include_kind(['a.b'], 'a')) - self.assertFalse(kinds_include_kind(['aa'], 'a')) - self.assertFalse(kinds_include_kind(['aa.b'], 'a')) - self.assertFalse(kinds_include_kind(['aa.b'], 'b')) - - -class CodeActionsListenerTestCase(TextDocumentTestCase): - def setUp(self) -> Generator: - yield from super().setUp() - self.original_debounce_time = DocumentSyncListener.code_actions_debounce_time - DocumentSyncListener.code_actions_debounce_time = 0 - - def tearDown(self) -> None: - DocumentSyncListener.code_actions_debounce_time = self.original_debounce_time - super().tearDown() - - @classmethod - def get_test_server_capabilities(cls) -> dict: - capabilities = deepcopy(super().get_test_server_capabilities()) - capabilities['capabilities']['codeActionProvider'] = {} - return capabilities - - def test_requests_with_diagnostics(self) -> Generator: - initial_content = 'a\nb\nc' - self.insert_characters(initial_content) - yield from self.await_message('textDocument/didChange') - range_a = range_from_points(Point(0, 0), Point(0, 1)) - range_b = range_from_points(Point(1, 0), Point(1, 1)) - range_c = range_from_points(Point(2, 0), Point(2, 1)) - yield from self.await_client_notification( - "textDocument/publishDiagnostics", - create_test_diagnostics([('issue a', range_a), ('issue b', range_b), ('issue c', range_c)]) - ) - code_action_a = create_test_code_action(self.view, self.view.change_count(), [("A", range_a)]) - code_action_b = create_test_code_action(self.view, self.view.change_count(), [("B", range_b)]) - self.set_response('textDocument/codeAction', [code_action_a, code_action_b]) - self.view.run_command('lsp_selection_set', {"regions": [(0, 3)]}) # Select a and b. - yield 100 - params = yield from self.await_message('textDocument/codeAction') - self.assertEquals(params['range']['start']['line'], 0) - self.assertEquals(params['range']['start']['character'], 0) - self.assertEquals(params['range']['end']['line'], 1) - self.assertEquals(params['range']['end']['character'], 1) - self.assertEquals(len(params['context']['diagnostics']), 2) - annotations_range = self.view.get_regions(SessionView.CODE_ACTIONS_KEY) - self.assertEquals(len(annotations_range), 1) - self.assertEquals(annotations_range[0].a, 3) - self.assertEquals(annotations_range[0].b, 0) - - def test_requests_with_no_diagnostics(self) -> Generator: - initial_content = 'a\nb\nc' - self.insert_characters(initial_content) - yield from self.await_message("textDocument/didChange") - range_a = range_from_points(Point(0, 0), Point(0, 1)) - range_b = range_from_points(Point(1, 0), Point(1, 1)) - code_action1 = create_test_code_action(self.view, 0, [("A", range_a)]) - code_action2 = create_test_code_action(self.view, 0, [("B", range_b)]) - self.set_response('textDocument/codeAction', [code_action1, code_action2]) - self.view.run_command('lsp_selection_set', {"regions": [(0, 3)]}) # Select a and b. - yield 100 - params = yield from self.await_message('textDocument/codeAction') - self.assertEquals(params['range']['start']['line'], 0) - self.assertEquals(params['range']['start']['character'], 0) - self.assertEquals(params['range']['end']['line'], 1) - self.assertEquals(params['range']['end']['character'], 1) - self.assertEquals(len(params['context']['diagnostics']), 0) - annotations_range = self.view.get_regions(SessionView.CODE_ACTIONS_KEY) - self.assertEquals(len(annotations_range), 1) - self.assertEquals(annotations_range[0].a, 3) - self.assertEquals(annotations_range[0].b, 0) - - def test_excludes_disabled_code_actions(self) -> Generator: - initial_content = 'a\n' - self.insert_characters(initial_content) - yield from self.await_message("textDocument/didChange") - code_action = create_disabled_code_action( - self.view, - self.view.change_count(), - [(';', range_from_points(Point(0, 0), Point(0, 1)))] - ) - self.set_response('textDocument/codeAction', [code_action]) - self.view.run_command('lsp_selection_set', {"regions": [(0, 1)]}) # Select a - yield 100 - yield from self.await_message('textDocument/codeAction') - code_action_ranges = self.view.get_regions(SessionView.CODE_ACTIONS_KEY) - self.assertEquals(len(code_action_ranges), 0) - - def test_extends_range_to_include_diagnostics(self) -> Generator: - self.insert_characters('x diagnostic') - yield from self.await_message("textDocument/didChange") - yield from self.await_client_notification( - "textDocument/publishDiagnostics", - create_test_diagnostics([ - ('diagnostic word', range_from_points(Point(0, 2), Point(0, 12))), - ('all content', range_from_points(Point(0, 0), Point(0, 12))), - ]) - ) - self.view.run_command('lsp_selection_set', {"regions": [(0, 5)]}) - yield 100 - params = yield from self.await_message('textDocument/codeAction') - # Range should be extended to include range of all intersecting diagnostics - self.assertEquals(params['range']['start']['line'], 0) - self.assertEquals(params['range']['start']['character'], 0) - self.assertEquals(params['range']['end']['line'], 0) - self.assertEquals(params['range']['end']['character'], 12) - self.assertEquals(len(params['context']['diagnostics']), 2) - - -class CodeActionsTestCase(TextDocumentTestCase): - - @classmethod - def get_test_server_capabilities(cls) -> dict: - capabilities = deepcopy(super().get_test_server_capabilities()) - capabilities['capabilities']['codeActionProvider'] = {"resolveProvider": True} - return capabilities - - def test_requests_code_actions_on_newly_published_diagnostics(self) -> Generator: - self.insert_characters('a\nb') - yield from self.await_message("textDocument/didChange") - yield from self.await_client_notification( - "textDocument/publishDiagnostics", - create_test_diagnostics([ - ('issue a', range_from_points(Point(0, 0), Point(0, 1))), - ('issue b', range_from_points(Point(1, 0), Point(1, 1))) - ]) - ) - params = yield from self.await_message('textDocument/codeAction') - self.assertEquals(params['range']['start']['line'], 1) - self.assertEquals(params['range']['start']['character'], 0) - self.assertEquals(params['range']['end']['line'], 1) - self.assertEquals(params['range']['end']['character'], 1) - self.assertEquals(len(params['context']['diagnostics']), 1) - - def test_applies_code_action_with_matching_document_version(self) -> Generator: - code_action = create_test_code_action(self.view, 3, [ - ("c", range_from_points(Point(0, 0), Point(0, 1))), - ("d", range_from_points(Point(1, 0), Point(1, 1))), - ]) - self.insert_characters('a\nb') - yield from self.await_message("textDocument/didChange") - self.assertEqual(self.view.change_count(), 3) - yield from self.await_run_code_action(code_action) - # yield from self.await_message('codeAction/resolve') - self.assertEquals(entire_content(self.view), 'c\nd') - - def test_does_not_apply_with_nonmatching_document_version(self) -> Generator: - initial_content = 'a\nb' - code_action = create_test_code_action(self.view, 0, [ - ("c", range_from_points(Point(0, 0), Point(0, 1))), - ("d", range_from_points(Point(1, 0), Point(1, 1))), - ]) - self.insert_characters(initial_content) - yield from self.await_message("textDocument/didChange") - yield from self.await_run_code_action(code_action) - self.assertEquals(entire_content(self.view), initial_content) - - def test_runs_command_in_resolved_code_action(self) -> Generator: - code_action = create_test_code_action2("dosomethinguseful", ["1", 0, {"hello": "there"}]) - resolved_code_action = deepcopy(code_action) - resolved_code_action["edit"] = create_code_action_edit(self.view, 3, [ - ("c", range_from_points(Point(0, 0), Point(0, 1))), - ("d", range_from_points(Point(1, 0), Point(1, 1))), - ]) - self.set_response('codeAction/resolve', resolved_code_action) - self.set_response('workspace/executeCommand', {"reply": "OK done"}) - self.insert_characters('a\nb') - yield from self.await_message("textDocument/didChange") - self.assertEqual(self.view.change_count(), 3) - yield from self.await_run_code_action(code_action) - yield from self.await_message('codeAction/resolve') - params = yield from self.await_message('workspace/executeCommand') - self.assertEqual(params, {"command": "dosomethinguseful", "arguments": ["1", 0, {"hello": "there"}]}) - self.assertEquals(entire_content(self.view), 'c\nd') - - # Keep this test last as it breaks pyls! - def test_applies_correctly_after_emoji(self) -> Generator: - self.insert_characters('🕵️hi') - yield from self.await_message("textDocument/didChange") - code_action = create_test_code_action(self.view, self.view.change_count(), [ - ("bye", range_from_points(Point(0, 3), Point(0, 5))), - ]) - yield from self.await_run_code_action(code_action) - self.assertEquals(entire_content(self.view), '🕵️bye') diff --git a/tests/test_collections.py b/tests/test_collections.py deleted file mode 100644 index 4137cd399..000000000 --- a/tests/test_collections.py +++ /dev/null @@ -1,181 +0,0 @@ -from unittest import TestCase -from LSP.plugin.core.collections import DottedDict -from LSP.plugin.core.typing import Any - - -class DottedDictTests(TestCase): - - def verify(self, d: DottedDict, path: str, value: Any) -> None: - self.assertEqual(d.get(path), value) - - def test_set_and_get(self) -> None: - d = DottedDict() - d.set("foo", 1) - d.set("bar", 2) - d.set("baz", 3) - self.verify(d, "foo", 1) - self.verify(d, "bar", 2) - self.verify(d, "baz", 3) - d.set("foo.bar", "hello") - self.verify(d, "foo.bar", "hello") - self.assertIsNone(d.get("some.nonexistant.key")) - d.set("foo", "world") - self.verify(d, "foo", "world") - d.set("foo.bar.baz", {"some": "dict"}) - self.verify(d, "foo.bar.baz.some", "dict") - - def test_remove(self) -> None: - d = DottedDict() - d.set("foo", "asdf") - self.assertIn("foo", d) - d.remove("foo") - self.assertNotIn("foo", d) - self.assertIsNone(d.get("foo")) - d.set("foo.bar", {"baz": "qux"}) - self.verify(d, "foo.bar", {"baz": "qux"}) - self.verify(d, "foo", {"bar": {"baz": "qux"}}) - d.set("foo.bar.baz", "qux") - self.verify(d, "foo.bar", {"baz": "qux"}) - self.verify(d, "foo", {"bar": {"baz": "qux"}}) - d.set("foo.hello", "world") - d.remove("foo.bar") - self.verify(d, "foo", {"hello": "world"}) - - def test_assign(self) -> None: - d = DottedDict() - d.assign({ - "a": "b", - "c": { - "x": "a", - "y": "b" - }, - "d": { - "e": { - "f": { - "a": "b", - "c": "d" - } - } - } - }) - self.verify(d, "a", "b") - self.verify(d, "c.x", "a") - self.verify(d, "c.y", "b") - self.verify(d, "d.e.f.a", "b") - self.verify(d, "d.e.f.c", "d") - self.verify(d, "d.e.f", {"a": "b", "c": "d"}) - self.verify(d, "d.e", {"f": {"a": "b", "c": "d"}}) - self.verify(d, "d", {"e": {"f": {"a": "b", "c": "d"}}}) - - def test_update(self) -> None: - d = DottedDict() - d.set("foo.bar.a", "a") - d.set("foo.bar.b", "b") - d.set("foo.bar.c", "c") - self.verify(d, "foo.bar", {"a": "a", "b": "b", "c": "c"}) - d.update({ - "foo": { - "bar": { - "a": "x", - "b": "y" - } - } - }) - self.verify(d, "foo.bar", {"a": "x", "b": "y", "c": "c"}) - - def test_as_dict(self) -> None: - d = DottedDict() - d.set("foo.bar.baz", 1) - d.set("foo.bar.qux", "asdf") - d.set("foo.bar.a", "b") - d.set("foo.b.x", "c") - d.set("foo.b.y", "d") - self.assertEqual(d.get(), { - "foo": { - "bar": { - "baz": 1, - "qux": "asdf", - "a": "b" - }, - "b": { - "x": "c", - "y": "d" - } - } - }) - d.clear() - self.assertEqual(d.get(), {}) - - def test_dunder_bool(self) -> None: - d = DottedDict({"a": {"b": {"c": {"x": "x", "y": "y"}}}}) - self.assertTrue(d) - d.clear() - self.assertFalse(d) - d.update({"a": {"b": {"x": 1, "y": 2}}}) - self.assertTrue(d) - self.verify(d, "a.b.x", 1) - self.verify(d, "a.b.y", 2) - d.clear() - self.assertFalse(d) - - def test_copy_whole(self) -> None: - d = DottedDict({"a": {"b": {"c": {"x": "x", "y": "y"}}}}) - d_copy = d.copy() - d_copy['a'] = None - self.assertNotEqual(d.get()['a'], d_copy['a']) - - def test_copy_partial(self) -> None: - d = DottedDict({"a": {"b": {"c": 'd'}}}) - d_copy = d.copy('a.b') - self.assertEqual(d_copy['c'], 'd') - d_copy['c'] = None - self.assertNotEqual(d.get('a.b.c'), d_copy['c']) - - def test_update_empty_dict(self) -> None: - d = DottedDict({}) - d.update({"a": {}}) - self.assertEqual(d.get(), {"a": {}}) - d.update({"a": {"b": {}}}) - self.assertEqual(d.get(), {"a": {"b": {}}}) - - def test_from_base_and_override(self) -> None: - base = DottedDict({ - "yaml.schemas": {} - }) - override = { - "yaml.schemas": { - "http://foo.com/bar.json": "**/*.json" - } - } - result = DottedDict.from_base_and_override(base, override) - self.assertEqual( - result.get(None), - { - "yaml": { - "schemas": { - "http://foo.com/bar.json": "**/*.json" - } - } - } - ) - - def test_update_with_dicts(self) -> None: - base = { - "settings": { - "yaml.schemas": {} - } - } - overrides = { - "yaml.schemas": { - "http://foo.com/bar.json": "**/*.json" - } - } - settings = DottedDict(base.get("settings", {})) - settings.update(overrides) - self.assertEqual(settings.get(), { - "yaml": { - "schemas": { - "http://foo.com/bar.json": "**/*.json" - } - } - }) diff --git a/tests/test_completion.py b/tests/test_completion.py deleted file mode 100644 index 06e50f8ca..000000000 --- a/tests/test_completion.py +++ /dev/null @@ -1,1086 +0,0 @@ -from copy import deepcopy -from LSP.plugin.completion import format_completion -from LSP.plugin.completion import completion_with_defaults -from LSP.plugin.core.protocol import CompletionItem -from LSP.plugin.core.protocol import CompletionItemDefaults -from LSP.plugin.core.protocol import CompletionItemKind -from LSP.plugin.core.protocol import CompletionItemLabelDetails -from LSP.plugin.core.protocol import CompletionItemTag -from LSP.plugin.core.protocol import InsertTextFormat -from LSP.plugin.core.typing import Any, Generator, List, Dict, Callable, Optional -from setup import TextDocumentTestCase -from unittest import TestCase -import sublime - - -additional_edits = { - 'label': 'asdf', - 'additionalTextEdits': [ - { - 'range': { - 'start': { - 'line': 0, - 'character': 0 - }, - 'end': { - 'line': 0, - 'character': 0 - } - }, - 'newText': 'import asdf;\n' - } - ] -} - - -class CompletionsTestsBase(TextDocumentTestCase): - @classmethod - def init_view_settings(cls) -> None: - super().init_view_settings() - assert cls.view - cls.view.settings().set("auto_complete_selector", "text.plain") - - def type(self, text: str) -> None: - self.view.run_command('append', {'characters': text}) - self.view.run_command('move_to', {'to': 'eol'}) - - def move_cursor(self, row: int, col: int) -> None: - point = self.view.text_point(row, col) - # move cursor to point - s = self.view.sel() - s.clear() - s.add(point) - - def create_commit_completion_closure(self, commit_completion_command="commit_completion") -> Callable[[], bool]: - committed = False - current_change_count = self.view.change_count() - - def commit_completion() -> bool: - if not self.view.is_auto_complete_visible(): - return False - nonlocal committed - nonlocal current_change_count - if not committed: - self.view.run_command(commit_completion_command) - committed = True - return self.view.change_count() > current_change_count - - return commit_completion - - def select_completion(self) -> 'Generator': - self.view.run_command('auto_complete') - yield self.create_commit_completion_closure() - - def shift_select_completion(self) -> 'Generator': - self.view.run_command('auto_complete') - yield self.create_commit_completion_closure("lsp_commit_completion_with_opposite_insert_mode") - - def read_file(self) -> str: - return self.view.substr(sublime.Region(0, self.view.size())) - - def verify(self, *, completion_items: List[Dict[str, Any]], insert_text: str, expected_text: str) -> Generator: - if insert_text: - self.type(insert_text) - self.set_response("textDocument/completion", completion_items) - yield from self.select_completion() - yield from self.await_message("textDocument/completion") - yield from self.await_message("textDocument/didChange") - self.assertEqual(self.read_file(), expected_text) - - -class QueryCompletionsTests(CompletionsTestsBase): - def test_none(self) -> 'Generator': - self.set_response("textDocument/completion", None) - self.view.run_command('auto_complete') - yield lambda: self.view.is_auto_complete_visible() is False - - def test_simple_label(self) -> 'Generator': - yield from self.verify( - completion_items=[{'label': 'asdf'}, {'label': 'efcgh'}], - insert_text='', - expected_text='asdf') - - def test_prefer_insert_text_over_label(self) -> 'Generator': - yield from self.verify( - completion_items=[{"label": "Label text", "insertText": "Insert text"}], - insert_text='', - expected_text='Insert text') - - def test_prefer_text_edit_over_insert_text(self) -> 'Generator': - yield from self.verify( - completion_items=[{ - "label": "Label text", - "insertText": "Insert text", - "textEdit": { - "newText": "Text edit", - "range": { - "end": { - "character": 5, - "line": 0 - }, - "start": { - "character": 0, - "line": 0 - } - } - } - }], - insert_text='', - expected_text='Text edit') - - def test_simple_insert_text(self) -> 'Generator': - yield from self.verify( - completion_items=[{'label': 'asdf', 'insertText': 'asdf()'}], - insert_text="a", - expected_text='asdf()') - - def test_var_prefix_using_label(self) -> 'Generator': - yield from self.verify(completion_items=[{'label': '$what'}], insert_text="$", expected_text="$what") - - def test_var_prefix_added_in_insertText(self) -> 'Generator': - """ - https://github.com/sublimelsp/LSP/issues/294 - - User types '$env:U', server replaces '$env:U' with '$env:USERPROFILE' - """ - yield from self.verify( - completion_items=[{ - 'filterText': '$env:USERPROFILE', - 'insertText': '$env:USERPROFILE', - 'sortText': '0006USERPROFILE', - 'label': 'USERPROFILE', - 'additionalTextEdits': None, - 'data': None, - 'kind': 6, - 'command': None, - 'textEdit': { - 'newText': '$env:USERPROFILE', - 'range': { - 'end': {'line': 0, 'character': 6}, - 'start': {'line': 0, 'character': 0} - } - }, - 'commitCharacters': None, - 'range': None, - 'documentation': None - }], - insert_text="$env:U", - expected_text="$env:USERPROFILE") - - def test_pure_insertion_text_edit(self) -> 'Generator': - """ - https://github.com/sublimelsp/LSP/issues/368 - - User types '$so', server returns pure insertion completion 'meParam', completing it to '$someParam'. - - THIS TEST FAILS - """ - yield from self.verify( - completion_items=[{ - 'textEdit': { - 'newText': 'meParam', - 'range': { - 'end': {'character': 4, 'line': 0}, - 'start': {'character': 4, 'line': 0} # pure insertion! - } - }, - 'label': '$someParam', - 'data': None, - 'command': None, - 'detail': 'null', - 'insertText': None, - 'additionalTextEdits': None, - 'sortText': None, - 'documentation': None, - 'kind': 6 - }], - insert_text="$so", - expected_text="$someParam") - - def test_space_added_in_label(self) -> 'Generator': - """ - Clangd: label=" const", insertText="const" (https://github.com/sublimelsp/LSP/issues/368) - """ - yield from self.verify( - completion_items=[{ - "label": " const", - "sortText": "3f400000const", - "kind": 14, - "textEdit": { - "newText": "const", - "range": { - "end": { - "character": 1, - "line": 0 - }, - "start": { - "character": 3, - "line": 0 - } - } - }, - "insertTextFormat": InsertTextFormat.Snippet, - "insertText": "const", - "filterText": "const", - "score": 6 - }], - insert_text=' co', - expected_text=" const") # NOT 'const' - - def test_dash_missing_from_label(self) -> 'Generator': - """ - Powershell: label="UniqueId", trigger="-UniqueIdd, text to be inserted = "-UniqueId" - - (https://github.com/sublimelsp/LSP/issues/572) - """ - yield from self.verify( - completion_items=[{ - "filterText": "-UniqueId", - "documentation": None, - "textEdit": { - "range": { - "start": {"character": 0, "line": 0}, - "end": {"character": 1, "line": 0} - }, - "newText": "-UniqueId" - }, - "commitCharacters": None, - "command": None, - "label": "UniqueId", - "insertText": "-UniqueId", - "additionalTextEdits": None, - "data": None, - "range": None, - "insertTextFormat": InsertTextFormat.PlainText, - "sortText": "0001UniqueId", - "kind": 6, - "detail": "[string[]]" - }], - insert_text="u", - expected_text="-UniqueId") - - def test_edit_before_cursor(self) -> 'Generator': - """ - https://github.com/sublimelsp/LSP/issues/536 - """ - yield from self.verify( - completion_items=[{ - 'insertTextFormat': 2, - 'data': { - 'symbol': 'example/Foo#myFunction().', - 'target': 'file:/home/ayoub/workspace/testproject/?id=root' - }, - 'detail': 'override def myFunction(): Unit', - 'sortText': '00000', - 'filterText': 'override def myFunction', # the filterText is tricky here - 'preselect': True, - 'label': 'override def myFunction(): Unit', - 'kind': 2, - 'additionalTextEdits': [], - 'textEdit': { - 'newText': 'override def myFunction(): Unit = ${0:???}', - 'range': { - 'start': { - 'line': 0, - 'character': 0 - }, - 'end': { - 'line': 0, - 'character': 7 - } - } - } - }], - insert_text='def myF', - expected_text='override def myFunction(): Unit = ???') - - def test_edit_after_nonword(self) -> 'Generator': - """ - https://github.com/sublimelsp/LSP/issues/645 - """ - yield from self.verify( - completion_items=[{ - "textEdit": { - "newText": "apply($0)", - "range": { - "end": { - "line": 0, - "character": 5 - }, - "start": { - "line": 0, - "character": 5 - } - } - }, - "label": "apply[A](xs: A*): List[A]", - "sortText": "00000", - "preselect": True, - "insertTextFormat": InsertTextFormat.Snippet, - "filterText": "apply", - "data": { - "symbol": "scala/collection/immutable/List.apply().", - "target": "file:/home/user/workspace/testproject/?id=root" - }, - "kind": 2 - }], - insert_text="List.", - expected_text='List.apply()') - - def test_filter_text_is_not_a_prefix_of_label(self) -> 'Generator': - """ - Metals: "Implement all members" - - The filterText is 'e', so when the user types 'e', one of the completion items should be - "Implement all members". - - VSCode doesn't show the filterText in this case; it'll only show "Implement all members". - c.f. https://github.com/microsoft/language-server-protocol/issues/898#issuecomment-593968008 - - In SublimeText, we always show the filterText (a.k.a. trigger). - - This is one of the more confusing and contentious completion items. - - https://github.com/sublimelsp/LSP/issues/771 - """ - yield from self.verify( - completion_items=[{ - "label": "Implement all members", - "kind": 12, - "sortText": "00002", - "filterText": "e", - "insertTextFormat": InsertTextFormat.Snippet, - "textEdit": { - "range": { - "start": {"line": 0, "character": 0}, - "end": {"line": 0, "character": 1} - }, - "newText": "def foo: Int \u003d ${0:???}\n def boo: Int \u003d ${0:???}" - }, - "data": { - "target": "file:/Users/ckipp/Documents/scala-workspace/test-project/?id\u003droot", - "symbol": "local6" - } - }], - insert_text='e', - expected_text='def foo: Int \u003d ???\n def boo: Int \u003d ???') - - def test_additional_edits_if_session_has_the_resolve_capability(self) -> 'Generator': - completion_item = { - 'label': 'asdf' - } - self.set_response("completionItem/resolve", { - 'label': 'asdf', - 'additionalTextEdits': [ - { - 'range': { - 'start': { - 'line': 0, - 'character': 0 - }, - 'end': { - 'line': 0, - 'character': 0 - } - }, - 'newText': 'import asdf;\n' - } - ] - }) - yield from self.verify( - completion_items=[completion_item], - insert_text='', - expected_text='import asdf;\nasdf') - - def test_prefix_should_include_the_dollar_sign(self) -> 'Generator': - self.set_response( - 'textDocument/completion', - { - "items": - [ - { - "label": "$hello", - "textEdit": - { - "newText": "$hello", - "range": {"end": {"line": 2, "character": 3}, "start": {"line": 2, "character": 0}} - }, - "data": 2369386987913238, - "detail": "int", - "kind": 6, - "sortText": "$hello" - } - ], - "isIncomplete": False - }) - - self.type('\n') - # move cursor after `$he|` - self.move_cursor(2, 3) - yield from self.select_completion() - yield from self.await_message('textDocument/completion') - - self.assertEquals(self.read_file(), '\n') - - def test_fuzzy_match_plaintext_insert_text(self) -> 'Generator': - yield from self.verify( - completion_items=[{ - 'insertTextFormat': 1, - 'label': 'aaba', - 'insertText': 'aaca' - }], - insert_text='aa', - expected_text='aaca') - - def test_fuzzy_match_plaintext_text_edit(self) -> 'Generator': - yield from self.verify( - completion_items=[{ - 'insertTextFormat': 1, - 'label': 'aaba', - 'textEdit': { - 'newText': 'aaca', - 'range': {'start': {'line': 0, 'character': 0}, 'end': {'line': 0, 'character': 3}}} - }], - insert_text='aab', - expected_text='aaca') - - def test_fuzzy_match_snippet_insert_text(self) -> 'Generator': - yield from self.verify( - completion_items=[{ - 'insertTextFormat': 2, - 'label': 'aaba', - 'insertText': 'aaca' - }], - insert_text='aab', - expected_text='aaca') - - def test_fuzzy_match_snippet_text_edit(self) -> 'Generator': - yield from self.verify( - completion_items=[{ - 'insertTextFormat': 2, - 'label': 'aaba', - 'textEdit': { - 'newText': 'aaca', - 'range': {'start': {'line': 0, 'character': 0}, 'end': {'line': 0, 'character': 3}}} - }], - insert_text='aab', - expected_text='aaca') - - def verify_multi_cursor(self, completion: Dict[str, Any]) -> 'Generator': - """ - This checks whether `fd` gets replaced by `fmod` when the cursor is at `fd|`. - Turning the `d` into an `m` is an important part of the test. - """ - self.type('fd\nfd\nfd') - selection = self.view.sel() - selection.clear() - selection.add(sublime.Region(2, 2)) - selection.add(sublime.Region(5, 5)) - selection.add(sublime.Region(8, 8)) - self.assertEqual(len(selection), 3) - for region in selection: - self.assertEqual(self.view.substr(self.view.line(region)), "fd") - self.set_response("textDocument/completion", [completion]) - yield from self.select_completion() - yield from self.await_message("textDocument/completion") - self.assertEqual(self.read_file(), 'fmod()\nfmod()\nfmod()') - - def test_multi_cursor_plaintext_insert_text(self) -> 'Generator': - yield from self.verify_multi_cursor({ - 'insertTextFormat': 1, - 'label': 'fmod(a, b)', - 'insertText': 'fmod()' - }) - - def test_multi_cursor_plaintext_text_edit(self) -> 'Generator': - yield from self.verify_multi_cursor({ - 'insertTextFormat': 1, - 'label': 'fmod(a, b)', - 'textEdit': { - 'newText': 'fmod()', - 'range': {'start': {'line': 0, 'character': 0}, 'end': {'line': 0, 'character': 2}} - } - }) - - def test_multi_cursor_snippet_insert_text(self) -> 'Generator': - yield from self.verify_multi_cursor({ - 'insertTextFormat': 2, - 'label': 'fmod(a, b)', - 'insertText': 'fmod($0)' - }) - - def test_multi_cursor_snippet_text_edit(self) -> 'Generator': - yield from self.verify_multi_cursor({ - 'insertTextFormat': 2, - 'label': 'fmod(a, b)', - 'textEdit': { - 'newText': 'fmod($0)', - 'range': {'start': {'line': 0, 'character': 0}, 'end': {'line': 0, 'character': 2}} - } - }) - - def test_nontrivial_text_edit_removal(self) -> 'Generator': - self.type('#include ') - self.move_cursor(0, 11) # Put the cursor inbetween 'u' and '>' - self.set_response("textDocument/completion", [{ - 'filterText': 'uchar.h>', - 'label': ' uchar.h>', - 'textEdit': { - # This range should remove "u>" and then insert "uchar.h>" - 'range': {'start': {'line': 0, 'character': 10}, 'end': {'line': 0, 'character': 12}}, - 'newText': 'uchar.h>' - }, - 'insertText': 'uchar.h>', - 'kind': 17, - 'insertTextFormat': 2 - }]) - yield from self.select_completion() - yield from self.await_message("textDocument/completion") - self.assertEqual(self.read_file(), '#include ') - - def test_nontrivial_text_edit_removal_with_buffer_modifications_clangd(self) -> 'Generator': - self.type('#include ') - self.move_cursor(0, 11) # Put the cursor inbetween 'u' and '>' - self.set_response("textDocument/completion", [{ - 'filterText': 'uchar.h>', - 'label': ' uchar.h>', - 'textEdit': { - # This range should remove "u>" and then insert "uchar.h>" - 'range': {'start': {'line': 0, 'character': 10}, 'end': {'line': 0, 'character': 12}}, - 'newText': 'uchar.h>' - }, - 'insertText': 'uchar.h>', - 'kind': 17, - 'insertTextFormat': 2 - }]) - self.view.run_command('auto_complete') # show the AC widget - yield from self.await_message("textDocument/completion") - yield 100 - self.view.run_command('insert', {'characters': 'c'}) # type characters - yield 100 - self.view.run_command('insert', {'characters': 'h'}) # while the AC widget - yield 100 - self.view.run_command('insert', {'characters': 'a'}) # is visible - yield 100 - # Commit the completion. The buffer has been modified in the meantime, so the old text edit that says to - # remove "u>" is invalid. The code in completion.py must be able to handle this. - yield self.create_commit_completion_closure() - self.assertEqual(self.read_file(), '#include ') - - def test_nontrivial_text_edit_removal_with_buffer_modifications_json(self) -> 'Generator': - self.type('{"k"}') - self.move_cursor(0, 3) # Put the cursor inbetween 'k' and '"' - self.set_response("textDocument/completion", [{ - 'kind': 10, - 'documentation': 'Array of single or multiple keys', - 'insertTextFormat': 2, - 'label': 'keys', - 'textEdit': { - # This range should remove '"k"' and then insert '"keys": []' - 'range': {'start': {'line': 0, 'character': 1}, 'end': {'line': 0, 'character': 4}}, - 'newText': '"keys": [$1]' - }, - "filterText": '"keys"', - "insertText": 'keys": [$1]' - }]) - self.view.run_command('auto_complete') # show the AC widget - yield from self.await_message("textDocument/completion") - yield 100 - self.view.run_command('insert', {'characters': 'e'}) # type characters - yield 100 - self.view.run_command('insert', {'characters': 'y'}) # while the AC widget is open - yield 100 - # Commit the completion. The buffer has been modified in the meantime, so the old text edit that says to - # remove '"k"' is invalid. The code in completion.py must be able to handle this. - yield self.create_commit_completion_closure() - self.assertEqual(self.read_file(), '{"keys": []}') - - def test_text_edit_plaintext_with_multiple_lines_indented(self) -> Generator[None, None, None]: - self.type("\t\n\t") - self.move_cursor(1, 2) - self.set_response("textDocument/completion", [{ - 'label': 'a', - 'textEdit': { - 'range': {'start': {'line': 1, 'character': 4}, 'end': {'line': 1, 'character': 4}}, - 'newText': 'a\n\tb' - }, - 'insertTextFormat': InsertTextFormat.PlainText - }]) - yield from self.select_completion() - yield from self.await_message("textDocument/completion") - # the "b" should be intended one level deeper - self.assertEqual(self.read_file(), '\t\n\ta\n\t\tb') - - def test_insert_insert_mode(self) -> 'Generator': - self.type('{{ title }}') - self.move_cursor(0, 5) # Put the cursor inbetween 'i' and 't' - self.set_response("textDocument/completion", [{ - 'label': 'title', - 'textEdit': { - 'newText': 'title', - 'insert': {'start': {'line': 0, 'character': 3}, 'end': {'line': 0, 'character': 5}}, - 'replace': {'start': {'line': 0, 'character': 3}, 'end': {'line': 0, 'character': 8}} - } - }]) - yield from self.select_completion() - yield from self.await_message("textDocument/completion") - self.assertEqual(self.read_file(), '{{ titletle }}') - - def test_replace_insert_mode(self) -> 'Generator': - self.type('{{ title }}') - self.move_cursor(0, 4) # Put the cursor inbetween 't' and 'i' - self.set_response("textDocument/completion", [{ - 'label': 'turtle', - 'textEdit': { - 'newText': 'turtle', - 'insert': {'start': {'line': 0, 'character': 3}, 'end': {'line': 0, 'character': 4}}, - 'replace': {'start': {'line': 0, 'character': 3}, 'end': {'line': 0, 'character': 8}} - } - }]) - yield from self.shift_select_completion() # commit the opposite insert mode - yield from self.await_message("textDocument/completion") - self.assertEqual(self.read_file(), '{{ turtle }}') - - def test_show_deprecated_flag(self) -> None: - item_with_deprecated_flag = { - "label": 'hello', - "kind": CompletionItemKind.Method, - "deprecated": True - } # type: CompletionItem - formatted_completion_item = format_completion(item_with_deprecated_flag, 0, False, "", {}, self.view.id()) - self.assertIn("DEPRECATED", formatted_completion_item.annotation) - - def test_show_deprecated_tag(self) -> None: - item_with_deprecated_tags = { - "label": 'hello', - "kind": CompletionItemKind.Method, - "tags": [CompletionItemTag.Deprecated] - } # type: CompletionItem - formatted_completion_item = format_completion(item_with_deprecated_tags, 0, False, "", {}, self.view.id()) - self.assertIn("DEPRECATED", formatted_completion_item.annotation) - - def test_strips_carriage_return_in_insert_text(self) -> 'Generator': - yield from self.verify( - completion_items=[{ - 'label': 'greeting', - 'insertText': 'hello\r\nworld' - }], - insert_text='', - expected_text='hello\nworld') - - def test_strips_carriage_return_in_text_edit(self) -> 'Generator': - yield from self.verify( - completion_items=[{ - 'label': 'greeting', - 'textEdit': { - 'range': {'start': {'line': 0, 'character': 0}, 'end': {'line': 0, 'character': 0}}, - 'newText': 'hello\r\nworld' - } - }], - insert_text='', - expected_text='hello\nworld') - - def test_label_details_with_filter_text(self) -> None: - - def check( - resolve_support: bool, - expected_regex: str, - label: str, - label_details: Optional[CompletionItemLabelDetails] - ) -> None: - lsp = {"label": label, "filterText": "force_label_to_go_into_st_detail_field"} # type: CompletionItem - if label_details is not None: - lsp["labelDetails"] = label_details - native = format_completion(lsp, 0, resolve_support, "", {}, self.view.id()) - self.assertRegex(native.details, expected_regex) - - check( - resolve_support=False, - expected_regex=r"^f$", - label="f", - label_details=None - ) - check( - resolve_support=False, - expected_regex=r"^f\(X& x\)$", - label="f", - label_details={"detail": "(X& x)"} - ) - check( - resolve_support=False, - expected_regex=r"^f\(X& x\)$", - label="f", - label_details={"detail": "(X& x)", "description": "does things"} - ) - check( - resolve_support=True, - expected_regex=r"^More \| f$", - label="f", - label_details=None - ) - check( - resolve_support=True, - expected_regex=r"^More \| f\(X& x\)$", - label="f", - label_details={"detail": "(X& x)"} - ) - check( - resolve_support=True, - expected_regex=r"^More \| f\(X& x\)$", # noqa: E501 - label="f", - label_details={"detail": "(X& x)", "description": "does things"} - ) - - def test_label_details_without_filter_text(self) -> None: - - def check( - resolve_support: bool, - expected_regex: str, - label: str, - label_details: Optional[CompletionItemLabelDetails] - ) -> None: - lsp = {"label": label} # type: CompletionItem - if label_details is not None: - lsp["labelDetails"] = label_details - native = format_completion(lsp, 0, resolve_support, "", {}, self.view.id()) - self.assertRegex(native.trigger, expected_regex) - - check( - resolve_support=False, - expected_regex=r"^f$", - label="f", - label_details=None - ) - check( - resolve_support=False, - expected_regex=r"^f\(X& x\)$", - label="f", - label_details={"detail": "(X& x)"} - ) - check( - resolve_support=False, - expected_regex=r"^f\(X& x\)$", - label="f", - label_details={"detail": "(X& x)", "description": "does things"} - ) - - -class QueryCompletionsNoResolverTests(CompletionsTestsBase): - ''' - The difference between QueryCompletionsTests and QueryCompletionsNoResolverTests - is that QueryCompletionsTests has the completion item resolve capability enabled - and the QueryCompletionsNoResolverTests has the resolve capability disabled - ''' - @classmethod - def get_test_server_capabilities(cls) -> dict: - capabilities = deepcopy(super().get_test_server_capabilities()) - capabilities['capabilities']['completionProvider']['resolveProvider'] = False - return capabilities - - def test_additional_edits_if_session_does_not_have_the_resolve_capability(self) -> 'Generator': - completion_item = { - 'label': 'ghjk', - 'additionalTextEdits': [ - { - 'range': { - 'start': { - 'line': 0, - 'character': 0 - }, - 'end': { - 'line': 0, - 'character': 0 - } - }, - 'newText': 'import ghjk;\n' - } - ] - } - yield from self.verify( - completion_items=[completion_item], - insert_text='', - expected_text='import ghjk;\nghjk') - - -class ItemDefaultTests(TestCase): - def test_respects_defaults_for_completion(self): - item = { - 'label': 'Hello' - } # type: CompletionItem - item_defaults = { - 'editRange': { - 'start': {'character': 0, 'line': 0}, - 'end': {'character': 0, 'line': 0}, - }, - 'insertTextFormat': InsertTextFormat.PlainText, - 'data': ['1', '2'] - } # type: CompletionItemDefaults - expected = { - 'label': 'Hello', - 'textEdit': { - 'newText': 'Hello', - 'range': { - 'start': {'character': 0, 'line': 0}, - 'end': {'character': 0, 'line': 0} - } - }, - 'insertTextFormat': InsertTextFormat.PlainText, - 'data': ['1', '2'] - } # type: CompletionItem - self.assertEqual(completion_with_defaults(item, item_defaults), expected) - - def test_defaults_should_not_override_completion_fields_if_present(self): - item = { - 'label': 'Hello', - 'textEdit': { - 'newText': 'Hello', - 'range': { - 'start': {'character': 0, 'line': 0}, - 'end': {'character': 0, 'line': 0} - } - }, - 'insertTextFormat': InsertTextFormat.PlainText, - 'data': ['1', '2'] - } # type: CompletionItem - item_defaults = { - 'editRange': { - 'insert': { - 'start': {'character': 0, 'line': 0}, - 'end': {'character': 0, 'line': 0}, - }, - 'replace': { - 'start': {'character': 0, 'line': 0}, - 'end': {'character': 0, 'line': 0}, - }, - }, - 'insertTextFormat': InsertTextFormat.Snippet, - 'data': ['3', '4'] - } # type: CompletionItemDefaults - expected = { - 'label': 'Hello', - 'textEdit': { - 'newText': 'Hello', - 'range': { - 'start': {'character': 0, 'line': 0}, - 'end': {'character': 0, 'line': 0} - } - }, - 'insertTextFormat': InsertTextFormat.PlainText, - 'data': ['1', '2'] - } # type: CompletionItem - self.assertEqual(completion_with_defaults(item, item_defaults), expected) - - def test_conversion_of_edit_range_to_text_edit_when_it_includes_insert_replace_fields(self): - item = { - 'label': 'Hello', - 'textEditText': 'Text to insert' - } # type: CompletionItem - item_defaults = { - 'editRange': { - 'insert': { - 'start': {'character': 0, 'line': 0}, - 'end': {'character': 0, 'line': 0}, - }, - 'replace': { - 'start': {'character': 0, 'line': 0}, - 'end': {'character': 0, 'line': 0}, - }, - }, - } # type: CompletionItemDefaults - expected = { - 'label': 'Hello', - 'textEditText': 'Text to insert', - 'textEdit': { - 'newText': 'Text to insert', # this text will be inserted - 'insert': { - 'start': {'character': 0, 'line': 0}, - 'end': {'character': 0, 'line': 0}, - }, - 'replace': { - 'start': {'character': 0, 'line': 0}, - 'end': {'character': 0, 'line': 0}, - }, - } - } # type: CompletionItem - self.assertEqual(completion_with_defaults(item, item_defaults), expected) - - -class FormatCompletionsUnitTests(TestCase): - - def _verify_completion( - self, payload: CompletionItem, trigger: str, annotation: str = '', details: str = '', flags: int = 0 - ) -> None: - item = format_completion( - payload, index=0, can_resolve_completion_items=False, session_name='abc', item_defaults={}, view_id=0) - self.assertEquals(item.trigger, trigger) - self.assertEquals(item.annotation, annotation) - self.assertEquals(item.details, details) - self.assertEquals(item.flags, flags) - - def test_label(self) -> None: - self._verify_completion( - { - "label": "banner?", - }, - trigger='banner?', - ) - - def test_detail(self) -> None: - self._verify_completion( - { - "detail": "typescript", - "label": "readConfigFile", - }, - trigger='readConfigFile', - annotation='typescript' - ) - - def test_label_details(self) -> None: - self._verify_completion( - { - "label": "banner?", - "labelDetails": { - "detail": "()" - }, - }, - trigger='banner?()', - ) - - def test_label_details_2(self) -> None: - self._verify_completion( - { - "label": "NaiveDateTime", - "labelDetails": { - "detail": "struct", - "description": "NaiveDateTime" - }, - }, - trigger='NaiveDateTime', - annotation='NaiveDateTime', - details='struct' - ) - - def test_label_details_3(self) -> None: - self._verify_completion( - { - "label": "NaiveDateTime", - "labelDetails": { - "detail": " struct", - "description": "NaiveDateTime" - }, - }, - trigger='NaiveDateTime struct', - annotation='NaiveDateTime' - ) - - def test_label_details_4(self) -> None: - # More relevant "labelDetails.description" ends up in the annotation rather than "detail". - self._verify_completion( - { - "detail": "Auto-import", - "label": "escape", - "labelDetails": { - "description": "html" - }, - }, - trigger='escape', - annotation='html', - details='Auto-import', - ) - - def test_label_details_5(self) -> None: - # filterText overrides label if doesn't match label+labelDetails.detail - self._verify_completion( - { - "detail": "Auto-import", - "filterText": "escapeNew", - "label": "escape", - "labelDetails": { - "detail": "(str)", - }, - }, - trigger='escapeNew', - annotation='Auto-import', - details='escape(str)', - ) - - def test_filter_text_1(self) -> None: - self._verify_completion( - { - "filterText": "banner", - "label": "banner?", - }, - trigger='banner?', - ) - - def test_filter_text_2(self) -> None: - self._verify_completion( - { - "filterText": ".$attrs", - "label": "$attrs", - }, - trigger='.$attrs', - details='$attrs' - ) - - def test_filter_text_3(self) -> None: - self._verify_completion( - { - "filterText": "import { readConfigFile$1 } from 'typescript';", - "label": "readConfigFile", - }, - trigger="import { readConfigFile$1 } from 'typescript';", - details='readConfigFile' - ) - - def test_filter_text_4(self) -> None: - # See the `test_filter_text_is_not_a_prefix_of_label` test above and - # also https://github.com/sublimelsp/LSP/issues/771 - # This is probably a silly server behavior that we probably shouldn't need to support? - self._verify_completion( - { - "label": "Implement all members", - "filterText": "e", - }, - trigger='e', - details='Implement all members' - ) - - def test_filter_text_and_label_details_1(self) -> None: - self._verify_completion( - { - "filterText": "banner", - "label": "banner?", - "labelDetails": { - "detail": "()" - }, - }, - trigger='banner?()', - ) - - def test_filter_text_and_label_details_3(self) -> None: - self._verify_completion( - { - "filterText": ".$attrs", - "label": "$attrs", - "labelDetails": { - "detail": "()" - }, - }, - trigger='.$attrs', - details='$attrs()' - ) - - def test_filter_text_and_label_details_4(self) -> None: - self._verify_completion( - { - 'label': 'create_texture', - 'labelDetails': { - 'description': 'Texture2D', - 'detail': ' (uint width, uint height, ubyte* ptr)' - }, - 'detail': 'Texture2D create_texture(uint width, uint height, ubyte* ptr)' - }, - trigger='create_texture (uint width, uint height, ubyte* ptr)', - annotation='Texture2D' - ) diff --git a/tests/test_configs.py b/tests/test_configs.py deleted file mode 100644 index b3cc51e28..000000000 --- a/tests/test_configs.py +++ /dev/null @@ -1,179 +0,0 @@ -import sublime -from LSP.plugin.core.settings import read_client_config, update_client_config -from LSP.plugin.core.views import get_uri_and_position_from_location -from LSP.plugin.core.views import to_encoded_filename -from os import environ -from os.path import dirname, pathsep -from unittesting import DeferrableTestCase -import unittest -import sys - -test_file_path = dirname(__file__) + "/testfile.txt" - - -class ConfigParsingTests(DeferrableTestCase): - - def test_can_parse_old_client_settings(self): - settings = { - "command": ["pyls"], - "scopes": ["text.html.vue"], - "syntaxes": ["Packages/Python/Python.sublime-syntax"], # it should use this one - "languageId": "java" - } - config = read_client_config("pyls", settings) - self.assertEqual(config.selector, "source.python") - self.assertEqual(config.priority_selector, "(text.html.vue)") - - def test_can_parse_client_settings_with_languages(self): - settings = { - "command": ["pyls"], - # Check that "selector" will be "source.python" - "languages": [{"languageId": "python"}] - } - config = read_client_config("pyls", settings) - self.assertEqual(config.selector, "(source.python)") - self.assertEqual(config.priority_selector, "(source.python)") - - def test_can_parse_settings_with_selector(self): - settings = { - "command": ["pyls"], - "selector": "source.python" - } - config = read_client_config("pyls", settings) - self.assertEqual(config.selector, "source.python") - self.assertEqual(config.priority_selector, "source.python") - - def test_can_update_config(self): - settings = { - "command": ["pyls"], - "document_selector": "source.python", - "languageId": "python" - } - config = read_client_config("pyls", settings) - config = update_client_config(config, {"enabled": True}) - self.assertEqual(config.enabled, True) - - def test_can_read_experimental_capabilities(self): - experimental_capabilities = { - "foo": 1, - "bar": True, - "baz": "abc" - } - settings = { - "command": ["pyls"], - "document_selector": "source.python", - "languageId": "python", - "experimental_capabilities": experimental_capabilities - } - config = read_client_config("pyls", settings) - self.assertEqual(config.experimental_capabilities, experimental_capabilities) - - def test_transport_config_extends_env_path(self): - settings = { - "command": ["pyls"], - "selector": "source.python", - "env": { - "PATH": "/a/b/" - } - } - config = read_client_config("pyls", settings) - transport_config = config.resolve_transport_config({}) - original_path = environ.copy()['PATH'] - resolved_path = transport_config.env['PATH'] - self.assertEqual(resolved_path, '/a/b/{}{}'.format(pathsep, original_path)) - - def test_list_in_environment(self): - settings = { - "command": ["pyls"], - "selector": "source.python", - "env": { - "FOO": ["C:/hello", "X:/there", "Y:/$foobar"], - "BAR": "baz" - } - } - config = read_client_config("pyls", settings) - resolved = config.resolve_transport_config({"foobar": "asdf"}) - if sublime.platform() == "windows": - self.assertEqual(resolved.env["FOO"], "C:/hello;X:/there;Y:/asdf") - else: - self.assertEqual(resolved.env["FOO"], "C:/hello:X:/there:Y:/asdf") - self.assertEqual(resolved.env["BAR"], "baz") - - def test_disabled_capabilities(self): - settings = { - "command": ["pyls"], - "selector": "source.python", - "disabled_capabilities": { - "colorProvider": True, - "completionProvider": {"triggerCharacters": True}, - "codeActionProvider": True - } - } - config = read_client_config("pyls", settings) - self.assertTrue(config.is_disabled_capability("colorProvider")) - # If only a sub path is disabled, the entire capability should not be disabled as a whole - self.assertFalse(config.is_disabled_capability("completionProvider")) - # This sub path should be disabled - self.assertTrue(config.is_disabled_capability("completionProvider.triggerCharacters")) - # But not this sub path - self.assertFalse(config.is_disabled_capability("completionProvider.resolveProvider")) - # The entire codeActionProvider is disabled - self.assertTrue(config.is_disabled_capability("codeActionProvider")) - # If codeActionProvider is disabled, all of its sub paths should be disabled as well - self.assertTrue(config.is_disabled_capability("codeActionProvider.codeActionKinds")) - # This one should be enabled - self.assertFalse(config.is_disabled_capability("definitionProvider")) - - def test_filter_out_disabled_capabilities_ignore_partially(self): - settings = { - "command": ["pyls"], - "selector": "source.python", - "disabled_capabilities": {"completionProvider": {"triggerCharacters": True}} - } - config = read_client_config("pyls", settings) - capability_path = "completionProvider" - options = {"triggerCharacters": ["!"], "resolveProvider": True} - options = config.filter_out_disabled_capabilities(capability_path, options) - self.assertNotIn("triggerCharacters", options) - self.assertIn("resolveProvider", options) - - @unittest.skipIf(sys.platform.startswith("win"), "requires non-Windows") - def test_path_maps(self): - config = read_client_config("asdf", { - "command": ["asdf"], - "selector": "source.foo", - "path_maps": [ - { - "local": "/home/user/projects/myproject", - "remote": "/workspace" - }, - { - "local": "/home/user/projects/another", - "remote": "/workspace2" - } - ] - }) - uri = config.map_client_path_to_server_uri("/home/user/projects/myproject/file.js") - self.assertEqual(uri, "file:///workspace/file.js") - uri = config.map_client_path_to_server_uri("/home/user/projects/another/foo.js") - self.assertEqual(uri, "file:///workspace2/foo.js") - uri = config.map_client_path_to_server_uri("/some/path/with/no/mapping.py") - self.assertEqual(uri, "file:///some/path/with/no/mapping.py") - path = config.map_server_uri_to_client_path("file:///workspace/bar.html") - self.assertEqual(path, "/home/user/projects/myproject/bar.html") - path = config.map_server_uri_to_client_path("file:///workspace2/style.css") - self.assertEqual(path, "/home/user/projects/another/style.css") - - # Test to_encoded_filename - uri, position = get_uri_and_position_from_location({ - 'uri': 'file:///foo/bar', - 'range': {'start': {'line': 0, 'character': 5}} - }) # type: ignore - path = config.map_server_uri_to_client_path(uri) - self.assertEqual(to_encoded_filename(path, position), '/foo/bar:1:6') - uri, position = get_uri_and_position_from_location({ - 'targetUri': 'file:///foo/bar', - 'targetSelectionRange': {'start': {'line': 1234, 'character': 4321}} - }) # type: ignore - path = config.map_server_uri_to_client_path(uri) - self.assertEqual(to_encoded_filename(path, position), '/foo/bar:1235:4322') diff --git a/tests/test_configurations.py b/tests/test_configurations.py deleted file mode 100644 index 93af0a0ef..000000000 --- a/tests/test_configurations.py +++ /dev/null @@ -1,94 +0,0 @@ -from LSP.plugin.core.configurations import WindowConfigManager -from test_mocks import DISABLED_CONFIG -from test_mocks import TEST_CONFIG -from unittest.mock import MagicMock -import sublime -import unittest - - -class GlobalConfigManagerTests(unittest.TestCase): - - def test_empty_configs(self): - window_mgr = WindowConfigManager(sublime.active_window(), {}) - self.assertNotIn(TEST_CONFIG.name, window_mgr.all) - - def test_global_config(self): - window_mgr = WindowConfigManager(sublime.active_window(), {TEST_CONFIG.name: TEST_CONFIG}) - self.assertIn(TEST_CONFIG.name, window_mgr.all) - - def test_override_config(self): - self.assertTrue(TEST_CONFIG.enabled) - win = sublime.active_window() - win.project_data = MagicMock(return_value={'settings': {'LSP': {TEST_CONFIG.name: {"enabled": False}}}}) - window_mgr = WindowConfigManager(win, {TEST_CONFIG.name: TEST_CONFIG}) - self.assertFalse(list(window_mgr.all.values())[0].enabled) - - -class WindowConfigManagerTests(unittest.TestCase): - - def test_no_configs(self): - view = sublime.active_window().active_view() - self.assertIsNotNone(view) - assert view - manager = WindowConfigManager(sublime.active_window(), {}) - self.assertEqual(list(manager.match_view(view)), []) - - def test_with_single_config(self): - window = sublime.active_window() - view = window.active_view() - self.assertIsNotNone(view) - assert view - manager = WindowConfigManager(window, {TEST_CONFIG.name: TEST_CONFIG}) - view.syntax = MagicMock(return_value=sublime.Syntax( - path="Packages/Text/Plain text.tmLanguage", - name="Plain Text", - scope="text.plain", - hidden=False - )) - view.settings().set("lsp_uri", "file:///foo/bar.txt") - self.assertEqual(list(manager.match_view(view)), [TEST_CONFIG]) - - def test_applies_project_settings(self): - window = sublime.active_window() - view = window.active_view() - assert view - window.project_data = MagicMock(return_value={ - "settings": { - "LSP": { - "test": { - "enabled": True - } - } - } - }) - manager = WindowConfigManager(window, {DISABLED_CONFIG.name: DISABLED_CONFIG}) - view.syntax = MagicMock(return_value=sublime.Syntax( - path="Packages/Text/Plain text.tmLanguage", - name="Plain Text", - scope="text.plain", - hidden=False - )) - view.settings().set("lsp_uri", "file:///foo/bar.txt") - configs = list(manager.match_view(view)) - self.assertEqual(len(configs), 1) - config = configs[0] - self.assertEqual(DISABLED_CONFIG.name, config.name) - self.assertTrue(config.enabled) - - def test_disables_temporarily(self): - window = sublime.active_window() - view = window.active_view() - window.project_data = MagicMock(return_value={ - "settings": { - "LSP": { - "test": { - "enabled": True - } - } - } - }) - - manager = WindowConfigManager(window, {DISABLED_CONFIG.name: DISABLED_CONFIG}) - # disables config in-memory - manager.disable_config(DISABLED_CONFIG.name, only_for_session=True) - self.assertFalse(any(manager.match_view(view))) diff --git a/tests/test_documents.py b/tests/test_documents.py deleted file mode 100644 index 74a994601..000000000 --- a/tests/test_documents.py +++ /dev/null @@ -1,134 +0,0 @@ -from LSP.plugin.core.logging import debug -from LSP.plugin.core.protocol import Request -from LSP.plugin.core.registry import windows -from LSP.plugin.core.types import ClientStates -from LSP.plugin.core.typing import Any, Generator -from LSP.plugin.documents import DocumentSyncListener -from os.path import join -from setup import add_config -from setup import close_test_view -from setup import expand -from setup import make_stdio_test_config -from setup import remove_config -from setup import TIMEOUT_TIME -from setup import YieldPromise -from sublime_plugin import view_event_listeners -from unittesting import DeferrableTestCase -import sublime - - -class WindowDocumentHandlerTests(DeferrableTestCase): - - def ensure_document_listener_created(self) -> bool: - assert self.view - # Bug in ST3? Either that, or CI runs with ST window not in focus and that makes ST3 not trigger some - # events like on_load_async, on_activated, on_deactivated. That makes things not properly initialize on - # opening file (manager missing in DocumentSyncListener) - # Revisit this once we're on ST4. - for listener in view_event_listeners[self.view.id()]: - if isinstance(listener, DocumentSyncListener): - sublime.set_timeout_async(listener.on_activated_async) - return True - return False - - def setUp(self) -> Generator: - init_options = { - "serverResponse": { - "capabilities": { - "textDocumentSync": { - "openClose": True, - "change": 1, - "save": True - }, - } - } - } - self.window = sublime.active_window() - self.assertTrue(self.window) - self.session1 = None - self.session2 = None - self.config1 = make_stdio_test_config() - self.config1.init_options.assign(init_options) - self.config2 = make_stdio_test_config() - self.config2.init_options.assign(init_options) - self.config2.name = "TEST-2" - self.config2.status_key = "lsp_TEST-2" - self.wm = windows.lookup(self.window) - add_config(self.config1) - add_config(self.config2) - self.wm.get_config_manager().all[self.config1.name] = self.config1 - self.wm.get_config_manager().all[self.config2.name] = self.config2 - - def test_sends_did_open_to_multiple_sessions(self) -> Generator: - filename = expand(join("$packages", "LSP", "tests", "testfile.txt"), self.window) - open_view = self.window.find_open_file(filename) - yield from close_test_view(open_view) - self.view = self.window.open_file(filename) - yield {"condition": lambda: not self.view.is_loading(), "timeout": TIMEOUT_TIME} - self.assertTrue(self.wm.get_config_manager().match_view(self.view)) - # self.init_view_settings() - yield {"condition": self.ensure_document_listener_created, "timeout": TIMEOUT_TIME} - yield { - "condition": lambda: self.wm.get_session(self.config1.name, self.view.file_name()) is not None, - "timeout": TIMEOUT_TIME} - yield { - "condition": lambda: self.wm.get_session(self.config2.name, self.view.file_name()) is not None, - "timeout": TIMEOUT_TIME} - self.session1 = self.wm.get_session(self.config1.name, self.view.file_name()) - self.session2 = self.wm.get_session(self.config2.name, self.view.file_name()) - self.assertIsNotNone(self.session1) - self.assertIsNotNone(self.session2) - self.assertEqual(self.session1.config.name, self.config1.name) - self.assertEqual(self.session2.config.name, self.config2.name) - yield {"condition": lambda: self.session1.state == ClientStates.READY, "timeout": TIMEOUT_TIME} - yield {"condition": lambda: self.session2.state == ClientStates.READY, "timeout": TIMEOUT_TIME} - yield from self.await_message("initialize") - yield from self.await_message("initialized") - yield from self.await_message("textDocument/didOpen") - self.view.run_command("insert", {"characters": "a"}) - yield from self.await_message("textDocument/didChange") - self.assertEqual(self.view.get_status("lsp_TEST"), "TEST") - self.assertEqual(self.view.get_status("lsp_TEST-2"), "TEST-2") - yield from close_test_view(self.view) - yield from self.await_message("textDocument/didClose") - - def doCleanups(self) -> Generator: - try: - yield from close_test_view(self.view) - except Exception: - pass - if self.session1: - sublime.set_timeout_async(self.session1.end_async) - yield lambda: self.session1.state == ClientStates.STOPPING - if self.session2: - sublime.set_timeout_async(self.session2.end_async) - yield lambda: self.session2.state == ClientStates.STOPPING - try: - remove_config(self.config2) - except ValueError: - pass - try: - remove_config(self.config1) - except ValueError: - pass - self.wm.get_config_manager().all.pop(self.config2.name, None) - self.wm.get_config_manager().all.pop(self.config1.name, None) - yield from super().doCleanups() - - def await_message(self, method: str) -> Generator: - promise1 = YieldPromise() - promise2 = YieldPromise() - - def handler1(params: Any) -> None: - promise1.fulfill(params) - - def handler2(params: Any) -> None: - promise2.fulfill(params) - - def error_handler(params: 'Any') -> None: - debug("Got error:", params, "awaiting timeout :(") - - self.session1.send_request(Request("$test/getReceived", {"method": method}), handler1, error_handler) - self.session2.send_request(Request("$test/getReceived", {"method": method}), handler2, error_handler) - yield {"condition": promise1, "timeout": TIMEOUT_TIME} - yield {"condition": promise2, "timeout": TIMEOUT_TIME} diff --git a/tests/test_edit.py b/tests/test_edit.py deleted file mode 100644 index bd5d3b108..000000000 --- a/tests/test_edit.py +++ /dev/null @@ -1,309 +0,0 @@ -from LSP.plugin import apply_text_edits -from LSP.plugin.core.edit import parse_workspace_edit -from LSP.plugin.core.protocol import TextDocumentEdit, TextEdit, WorkspaceEdit -from LSP.plugin.core.typing import List -from LSP.plugin.core.url import filename_to_uri -from LSP.plugin.core.views import entire_content -from LSP.plugin.edit import _parse_text_edit as parse_text_edit -from LSP.plugin.edit import _sort_by_application_order as sort_by_application_order -from LSP.plugin.edit import temporary_setting -from setup import TextDocumentTestCase -from test_protocol import LSP_RANGE -import sublime -import unittest - -FILENAME = 'C:\\file.py' if sublime.platform() == "windows" else '/file.py' -URI = filename_to_uri(FILENAME) -LSP_TEXT_EDIT = { - 'newText': 'newText\r\n', - 'range': LSP_RANGE -} # type: TextEdit - -LSP_EDIT_CHANGES = { - 'changes': {URI: [LSP_TEXT_EDIT]} -} # type: WorkspaceEdit - -LSP_TEXT_DOCUMENT_EDIT = { - 'textDocument': {'uri': URI, 'version': None}, - 'edits': [LSP_TEXT_EDIT] -} # type: TextDocumentEdit - -LSP_EDIT_DOCUMENT_CHANGES = { - 'documentChanges': [LSP_TEXT_DOCUMENT_EDIT] -} # type: WorkspaceEdit - -# Check that processing document changes does not result in clobbering. -LSP_EDIT_DOCUMENT_CHANGES_2 = { - "documentChanges": [ - { - "edits": [ - { - "range": { - "end": { - "character": 9, - "line": 14 - }, - "start": { - "character": 5, - "line": 14 - } - }, - "newText": "Test" - } - ], - "textDocument": { - "uri": URI, - "version": 6 - } - }, - { - "edits": [ - { - "range": { - "end": { - "character": 25, - "line": 11 - }, - "start": { - "character": 21, - "line": 11 - } - }, - "newText": "Test" - } - ], - "textDocument": { - "uri": URI, - "version": 6 - } - }, - { - "edits": [ - { - "range": { - "end": { - "character": 32, - "line": 26 - }, - "start": { - "character": 28, - "line": 26 - } - }, - "newText": "Test" - } - ], - "textDocument": { - "uri": URI, - "version": 6 - } - }, - { - "edits": [ - { - "range": { - "end": { - "character": 32, - "line": 27 - }, - "start": { - "character": 28, - "line": 27 - } - }, - "newText": "Test" - } - ], - "textDocument": { - "uri": URI, - "version": 6 - } - }, - { - "edits": [ - { - "range": { - "end": { - "character": 30, - "line": 39 - }, - "start": { - "character": 26, - "line": 39 - } - }, - "newText": "Test" - } - ], - "textDocument": { - "uri": URI, - "version": 6 - } - } - ] -} # type: WorkspaceEdit - -LSP_EDIT_DOCUMENT_CHANGES_3 = { - 'changes': { - "file:///asdf/foo/bar": [ - {"newText": "hello there", "range": LSP_RANGE}, - {"newText": "general", "range": LSP_RANGE}, - {"newText": "kenobi", "range": LSP_RANGE} - ] - }, - 'documentChanges': [LSP_TEXT_DOCUMENT_EDIT] -} # type: WorkspaceEdit - - -class TextEditTests(unittest.TestCase): - - def test_parse_from_lsp(self): - (start, end, newText) = parse_text_edit(LSP_TEXT_EDIT) - self.assertEqual(newText, 'newText\n') # Without the \r - self.assertEqual(start[0], 10) - self.assertEqual(start[1], 4) - self.assertEqual(end[0], 11) - self.assertEqual(end[1], 3) - - -class WorkspaceEditTests(unittest.TestCase): - - def test_parse_no_changes_from_lsp(self): - changes = parse_workspace_edit({}) - self.assertEqual(len(changes), 0) - - def test_parse_changes_from_lsp(self): - changes = parse_workspace_edit(LSP_EDIT_CHANGES) - self.assertIn(URI, changes) - self.assertEqual(len(changes), 1) - self.assertEqual(len(changes[URI][0]), 1) - - def test_parse_document_changes_from_lsp(self): - changes = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES) - self.assertIn(URI, changes) - self.assertEqual(len(changes), 1) - self.assertEqual(len(changes[URI][0]), 1) - - def test_no_clobbering_of_previous_edits(self): - changes = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES_2) - self.assertIn(URI, changes) - self.assertEqual(len(changes), 1) - self.assertEqual(len(changes[URI][0]), 5) - - def test_prefers_document_edits_over_changes(self): - changes = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES_3) - self.assertIn(URI, changes) - self.assertEqual(len(changes), 1) - self.assertEqual(len(changes[URI][0]), 1) # not 3 - - -class SortByApplicationOrderTests(unittest.TestCase): - - def test_empty_sort(self): - self.assertEqual(sort_by_application_order([]), []) - - def test_sorts_in_application_order(self): - edits = [ - ((0, 0), (0, 0), 'b'), - ((0, 0), (0, 0), 'a'), - ((0, 2), (0, 2), 'c') - ] - # expect 'c' (higher start), 'a' now reverse order before 'b' - sorted_edits = sort_by_application_order(edits) - self.assertEqual(sorted_edits[0][2], 'b') - self.assertEqual(sorted_edits[1][2], 'a') - self.assertEqual(sorted_edits[2][2], 'c') - - def test_sorts_in_application_order2(self): - changes = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES_2) - (edits, version) = changes[URI] - self.assertEqual(version, 6) - parsed_edits = [parse_text_edit(edit) for edit in edits] - sorted_edits = list(reversed(sort_by_application_order(parsed_edits))) - self.assertEqual(sorted_edits[0][0], (39, 26)) - self.assertEqual(sorted_edits[0][1], (39, 30)) - self.assertEqual(sorted_edits[1][0], (27, 28)) - self.assertEqual(sorted_edits[1][1], (27, 32)) - - -class ApplyDocumentEditTestCase(TextDocumentTestCase): - - def test_applies_text_edit(self) -> None: - self.insert_characters('abc') - edits = [{ - 'newText': 'x$0y', - 'range': { - 'start': { - 'line': 0, - 'character': 1, - }, - 'end': { - 'line': 0, - 'character': 2, - } - } - }] # type: List[TextEdit] - apply_text_edits(self.view, edits) - self.assertEquals(entire_content(self.view), 'ax$0yc') - - def test_applies_text_edit_with_placeholder(self) -> None: - self.insert_characters('abc') - edits = [{ - 'newText': 'x$0y', - 'range': { - 'start': { - 'line': 0, - 'character': 1, - }, - 'end': { - 'line': 0, - 'character': 2, - } - } - }] # type: List[TextEdit] - apply_text_edits(self.view, edits, process_placeholders=True) - self.assertEquals(entire_content(self.view), 'axyc') - self.assertEqual(len(self.view.sel()), 1) - self.assertEqual(self.view.sel()[0], sublime.Region(2, 2)) - - def test_applies_multiple_text_edits_with_placeholders(self) -> None: - self.insert_characters('ab') - newline_edit = { - 'newText': '\n$0', - 'range': { - 'start': { - 'line': 0, - 'character': 1, - }, - 'end': { - 'line': 0, - 'character': 1, - } - } - } # type: TextEdit - edits = [newline_edit, newline_edit] # type: List[TextEdit] - apply_text_edits(self.view, edits, process_placeholders=True) - self.assertEquals(entire_content(self.view), 'a\n\nb') - self.assertEqual(len(self.view.sel()), 2) - self.assertEqual(self.view.sel()[0], sublime.Region(2, 2)) - self.assertEqual(self.view.sel()[1], sublime.Region(3, 3)) - - -class TemporarySetting(unittest.TestCase): - - def test_basics(self) -> None: - v = sublime.active_window().active_view() - s = v.settings() - key = "__some_setting_that_should_not_exist__" - with temporary_setting(s, key, "hello"): - # The value should be modified while in the with-context - self.assertEqual(s.get(key), "hello") - # The key should be erased once out of the with-context, because it was not present before. - self.assertFalse(s.has(key)) - s.set(key, "hello there") - with temporary_setting(s, key, "general kenobi"): - # value key should be modified while in the with-context - self.assertEqual(s.get(key), "general kenobi") - # The key should remain present, and the value should be restored. - self.assertEqual(s.get(key), "hello there") - s.erase(key) diff --git a/tests/test_file_watcher.py b/tests/test_file_watcher.py index b3281e3ee..6074a75cf 100644 --- a/tests/test_file_watcher.py +++ b/tests/test_file_watcher.py @@ -15,20 +15,6 @@ import unittest -def setup_workspace_folder() -> str: - window = sublime.active_window() - folder_path = expand(join('$packages', 'LSP', 'tests'), window) - window.set_project_data({ - 'folders': [ - { - 'name': 'folder', - 'path': folder_path, - } - ] - }) - return folder_path - - class TestFileWatcher(FileWatcher): # The list of watchers created by active sessions. @@ -74,114 +60,6 @@ def trigger_async(): sublime.set_timeout_async(trigger_async) -class FileWatcherDocumentTestCase(TextDocumentTestCase): - """ - Changes TextDocumentTestCase behavior so that the initialization and destroy of the config - and the view happens before and after every test rather than per-testsuite. - """ - - @classmethod - def setUpClass(cls) -> None: - # Don't call the superclass. - register_file_watcher_implementation(TestFileWatcher) - - @classmethod - def tearDownClass(cls) -> None: - # Don't call the superclass. - pass - - def setUp(self) -> Generator: - self.assertEqual(len(TestFileWatcher._active_watchers), 0) - # Watchers are only registered when there are workspace folders so add a folder. - self.folder_root_path = setup_workspace_folder() - yield from super().setUpClass() - yield from super().setUp() - - def tearDown(self) -> Generator: - yield from super().tearDownClass() - self.assertEqual(len(TestFileWatcher._active_watchers), 0) - # Restore original project data. - window = sublime.active_window() - window.set_project_data({}) - - -class FileWatcherStaticTests(FileWatcherDocumentTestCase): - - @classmethod - def get_stdio_test_config(cls) -> ClientConfig: - return ClientConfig.from_config( - super().get_stdio_test_config(), - { - 'file_watcher': { - 'patterns': ['*.js'], - 'events': ['change'], - 'ignores': ['.git'], - } - } - ) - - def test_initialize_params_includes_capability(self) -> None: - self.assertIn('didChangeWatchedFiles', self.initialize_params['capabilities']['workspace']) - - def test_creates_static_watcher(self) -> None: - # Starting a session should have created a watcher. - self.assertEqual(len(TestFileWatcher._active_watchers), 1) - watcher = TestFileWatcher._active_watchers[0] - self.assertEqual(watcher.patterns, ['*.js']) - self.assertEqual(watcher.events, ['change']) - self.assertEqual(watcher.ignores, ['.git']) - self.assertEqual(watcher.root_path, self.folder_root_path) - - def test_handles_file_event(self) -> Generator: - watcher = TestFileWatcher._active_watchers[0] - filepath = join(self.folder_root_path, 'file.js') - watcher.trigger_event([('change', filepath)]) - sent_notification = yield from self.await_message('workspace/didChangeWatchedFiles') - self.assertIs(type(sent_notification['changes']), list) - self.assertEqual(len(sent_notification['changes']), 1) - change = sent_notification['changes'][0] - self.assertEqual(change['type'], file_watcher_event_type_to_lsp_file_change_type('change')) - self.assertTrue(change['uri'].endswith('file.js')) - - -class FileWatcherDynamicTests(FileWatcherDocumentTestCase): - - def test_handles_dynamic_watcher_registration(self) -> Generator: - registration_params = { - 'registrations': [ - { - 'id': '111', - 'method': 'workspace/didChangeWatchedFiles', - 'registerOptions': { - 'watchers': [ - { - 'globPattern': '*.py', - 'kind': WatchKind.Create | WatchKind.Change | WatchKind.Delete, - } - ] - } - } - ] - } - yield self.make_server_do_fake_request('client/registerCapability', registration_params) - self.assertEqual(len(TestFileWatcher._active_watchers), 1) - watcher = TestFileWatcher._active_watchers[0] - self.assertEqual(watcher.patterns, ['*.py']) - self.assertEqual(watcher.events, ['create', 'change', 'delete']) - self.assertEqual(watcher.root_path, self.folder_root_path) - # Trigger the file event - filepath = join(self.folder_root_path, 'file.py') - watcher.trigger_event([('create', filepath), ('change', filepath)]) - sent_notification = yield from self.await_message('workspace/didChangeWatchedFiles') - self.assertIs(type(sent_notification['changes']), list) - self.assertEqual(len(sent_notification['changes']), 2) - change1 = sent_notification['changes'][0] - self.assertEqual(change1['type'], file_watcher_event_type_to_lsp_file_change_type('create')) - self.assertTrue(change1['uri'].endswith('file.py')) - change2 = sent_notification['changes'][1] - self.assertEqual(change2['type'], file_watcher_event_type_to_lsp_file_change_type('change')) - self.assertTrue(change2['uri'].endswith('file.py')) - class PatternToGlobTests(unittest.TestCase): diff --git a/tests/test_message_request_handler.py b/tests/test_message_request_handler.py deleted file mode 100644 index 1660c2527..000000000 --- a/tests/test_message_request_handler.py +++ /dev/null @@ -1,22 +0,0 @@ -import unittest -from test_mocks import MockSession -from LSP.plugin.core.message_request_handler import MessageRequestHandler -import sublime - - -class MessageRequestHandlerTest(unittest.TestCase): - def test_show_popup(self): - window = sublime.active_window() - view = window.active_view() - session = MockSession() - params = { - 'type': 1, - 'message': 'hello', - 'actions': [ - {'title': "abc"}, - {'title': "def"} - ] - } - handler = MessageRequestHandler(view, session, "1", params, 'lsp server') - handler.show() - self.assertTrue(view.is_popup_visible()) diff --git a/tests/test_protocol.py b/tests/test_protocol.py deleted file mode 100644 index f2860e6b9..000000000 --- a/tests/test_protocol.py +++ /dev/null @@ -1,65 +0,0 @@ -from LSP.plugin.core.protocol import Point, Position, Range, Request, Notification -from LSP.plugin.core.transports import JsonRpcProcessor -import unittest - - -LSP_START_POSITION = {'line': 10, 'character': 4} # type: Position -LSP_END_POSITION = {'line': 11, 'character': 3} # type: Position -LSP_RANGE = {'start': LSP_START_POSITION, 'end': LSP_END_POSITION} # type: Range - - -class PointTests(unittest.TestCase): - - def test_lsp_conversion(self) -> None: - point = Point.from_lsp(LSP_START_POSITION) - self.assertEqual(point.row, 10) - self.assertEqual(point.col, 4) - lsp_point = point.to_lsp() - self.assertEqual(lsp_point['line'], 10) - self.assertEqual(lsp_point['character'], 4) - - -class EncodingTests(unittest.TestCase): - def test_encode(self) -> None: - encoded = JsonRpcProcessor._encode({"text": "😃"}) - self.assertEqual(encoded, b'{"text":"\xF0\x9F\x98\x83"}') - decoded = JsonRpcProcessor._decode(encoded) - self.assertEqual(decoded, {"text": "😃"}) - - -class RequestTests(unittest.TestCase): - - def test_initialize(self) -> None: - req = Request.initialize({"param": 1}) - payload = req.to_payload(1) - self.assertEqual(payload["jsonrpc"], "2.0") - self.assertEqual(payload["id"], 1) - self.assertEqual(payload["method"], "initialize") - self.assertEqual(payload["params"], {"param": 1}) - - def test_shutdown(self) -> None: - req = Request.shutdown() - payload = req.to_payload(1) - self.assertEqual(payload["jsonrpc"], "2.0") - self.assertEqual(payload["id"], 1) - self.assertEqual(payload["method"], "shutdown") - self.assertNotIn('params', payload) - - -class NotificationTests(unittest.TestCase): - - def test_initialized(self) -> None: - notification = Notification.initialized() - payload = notification.to_payload() - self.assertEqual(payload["jsonrpc"], "2.0") - self.assertNotIn("id", payload) - self.assertEqual(payload["method"], "initialized") - self.assertEqual(payload["params"], dict()) - - def test_exit(self) -> None: - notification = Notification.exit() - payload = notification.to_payload() - self.assertEqual(payload["jsonrpc"], "2.0") - self.assertNotIn("id", payload) - self.assertEqual(payload["method"], "exit") - self.assertNotIn('params', payload) diff --git a/tests/test_rename_panel.py b/tests/test_rename_panel.py deleted file mode 100644 index a9fd78a7a..000000000 --- a/tests/test_rename_panel.py +++ /dev/null @@ -1,50 +0,0 @@ -from LSP.plugin.rename import utf16_to_code_points -import unittest - - -class LspRenamePanelTests(unittest.TestCase): - - def test_utf16_ascii(self): - s = 'abc' - self.assertEqual(utf16_to_code_points(s, 0), 0) - self.assertEqual(utf16_to_code_points(s, 1), 1) - self.assertEqual(utf16_to_code_points(s, 2), 2) - self.assertEqual(utf16_to_code_points(s, 3), 3) # EOL after last character should count as its own code point - self.assertEqual(utf16_to_code_points(s, 1337), 3) # clamp to EOL - - def test_utf16_deseret_letter(self): - # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocuments - s = 'a𐐀b' - self.assertEqual(len(s), 3) - self.assertEqual(utf16_to_code_points(s, 0), 0) - self.assertEqual(utf16_to_code_points(s, 1), 1) - self.assertEqual(utf16_to_code_points(s, 2), 1) # 𐐀 needs 2 UTF-16 code units, so this is still at code point 1 - self.assertEqual(utf16_to_code_points(s, 3), 2) - self.assertEqual(utf16_to_code_points(s, 4), 3) - self.assertEqual(utf16_to_code_points(s, 1337), 3) - - def test_utf16_emoji(self): - s = 'a😀x' - self.assertEqual(len(s), 3) - self.assertEqual(utf16_to_code_points(s, 0), 0) - self.assertEqual(utf16_to_code_points(s, 1), 1) - self.assertEqual(utf16_to_code_points(s, 2), 1) - self.assertEqual(utf16_to_code_points(s, 3), 2) - self.assertEqual(utf16_to_code_points(s, 4), 3) - self.assertEqual(utf16_to_code_points(s, 1337), 3) - - def test_utf16_emoji_zwj_sequence(self): - # https://unicode.org/emoji/charts/emoji-zwj-sequences.html - s = 'a😵‍💫x' - self.assertEqual(len(s), 5) - self.assertEqual(s, 'a\U0001f635\u200d\U0001f4abx') - # 😵‍💫 consists of 5 UTF-16 code units and Python treats it as 3 characters - self.assertEqual(utf16_to_code_points(s, 0), 0) # a - self.assertEqual(utf16_to_code_points(s, 1), 1) # \U0001f635 - self.assertEqual(utf16_to_code_points(s, 2), 1) # \U0001f635 - self.assertEqual(utf16_to_code_points(s, 3), 2) # \u200d (zero width joiner) - self.assertEqual(utf16_to_code_points(s, 4), 3) # \U0001f4ab - self.assertEqual(utf16_to_code_points(s, 5), 3) # \U0001f4ab - self.assertEqual(utf16_to_code_points(s, 6), 4) # x - self.assertEqual(utf16_to_code_points(s, 7), 5) # after x - self.assertEqual(utf16_to_code_points(s, 1337), 5) diff --git a/tests/test_server_notifications.py b/tests/test_server_notifications.py deleted file mode 100644 index e49d655c6..000000000 --- a/tests/test_server_notifications.py +++ /dev/null @@ -1,81 +0,0 @@ -from LSP.plugin.core.protocol import DiagnosticSeverity -from LSP.plugin.core.protocol import DiagnosticTag -from LSP.plugin.core.protocol import PublishDiagnosticsParams -from LSP.plugin.core.typing import Generator -from LSP.plugin.core.url import filename_to_uri -from setup import TextDocumentTestCase -import sublime - - -class ServerNotifications(TextDocumentTestCase): - - def test_publish_diagnostics(self) -> Generator: - self.insert_characters("a b c\n") - params = { - 'uri': filename_to_uri(self.view.file_name() or ''), - 'diagnostics': [ - { - 'message': "foo", - 'severity': DiagnosticSeverity.Error, - 'source': 'qux', - 'range': {'end': {'character': 1, 'line': 0}, 'start': {'character': 0, 'line': 0}} - }, - { - 'message': 'bar', - 'severity': DiagnosticSeverity.Warning, - 'source': 'qux', - 'range': {'end': {'character': 3, 'line': 0}, 'start': {'character': 2, 'line': 0}} - }, - { - 'message': "baz", - 'severity': DiagnosticSeverity.Information, - 'source': 'qux', - 'range': {'end': {'character': 5, 'line': 0}, 'start': {'character': 4, 'line': 0}}, - 'tags': [DiagnosticTag.Unnecessary] - } - ] - } # type: PublishDiagnosticsParams - yield from self.await_client_notification("textDocument/publishDiagnostics", params) - errors_icon_regions = self.view.get_regions("lspTESTds1_icon") - errors_underline_regions = self.view.get_regions("lspTESTds1_underline") - warnings_icon_regions = self.view.get_regions("lspTESTds2_icon") - warnings_underline_regions = self.view.get_regions("lspTESTds2_underline") - info_icon_regions = self.view.get_regions("lspTESTds3_icon") - info_underline_regions = self.view.get_regions("lspTESTds3_underline") - yield lambda: len(errors_icon_regions) == len(errors_underline_regions) == 1 - yield lambda: len(warnings_icon_regions) == len(warnings_underline_regions) == 1 - yield lambda: len(info_icon_regions) == len(info_underline_regions) == 1 - yield lambda: len(self.view.get_regions("lspTESTds3_tags")) == 0 - self.assertEqual(errors_underline_regions[0], sublime.Region(0, 1)) - self.assertEqual(warnings_underline_regions[0], sublime.Region(2, 3)) - self.assertEqual(info_underline_regions[0], sublime.Region(4, 5)) - - # Testing whether the cursor position moves along with lsp_next_diagnostic - - self.view.window().run_command("lsp_next_diagnostic") - self.assertEqual(self.view.sel()[0].a, self.view.sel()[0].b) - self.assertEqual(self.view.sel()[0].b, 0) - - self.view.window().run_command("lsp_next_diagnostic") - self.assertEqual(self.view.sel()[0].a, self.view.sel()[0].b) - self.assertEqual(self.view.sel()[0].b, 2) - - self.view.window().run_command("lsp_next_diagnostic") - self.assertEqual(self.view.sel()[0].a, self.view.sel()[0].b) - self.assertEqual(self.view.sel()[0].b, 4) - - # lsp_prev_diagnostic should work as well - - self.view.window().run_command("lsp_prev_diagnostic") - self.assertEqual(self.view.sel()[0].a, self.view.sel()[0].b) - self.assertEqual(self.view.sel()[0].b, 2) - - self.view.window().run_command("lsp_prev_diagnostic") - self.assertEqual(self.view.sel()[0].a, self.view.sel()[0].b) - self.assertEqual(self.view.sel()[0].b, 0) - - # Testing to wrap around if there are no more diagnostics in the direction - - self.view.window().run_command("lsp_prev_diagnostic") - self.assertEqual(self.view.sel()[0].a, self.view.sel()[0].b) - self.assertEqual(self.view.sel()[0].b, 4) diff --git a/tests/test_server_panel_circular.py b/tests/test_server_panel_circular.py deleted file mode 100644 index d4ca28415..000000000 --- a/tests/test_server_panel_circular.py +++ /dev/null @@ -1,41 +0,0 @@ -from LSP.plugin.core.panels import MAX_LOG_LINES_LIMIT_ON -from LSP.plugin.core.panels import PanelName -from LSP.plugin.core.registry import windows -from unittesting import DeferrableTestCase -import sublime - - -class LspServerPanelTests(DeferrableTestCase): - - def setUp(self): - super().setUp() - self.window = sublime.active_window() - self.assertIsNotNone(self.window) - self.wm = windows.lookup(self.window) - self.assertIsNotNone(self.wm) - if not self.wm: - return - self.view = self.window.active_view() - self.panel = self.wm.panel_manager.get_panel(PanelName.Log) - self.assertIsNotNone(self.panel) - if not self.panel: - return - self.panel.run_command("lsp_clear_panel") - - def assert_total_lines_equal(self, expected_total_lines): - actual_total_lines = len(self.panel.split_by_newlines(sublime.Region(0, self.panel.size()))) - self.assertEqual(actual_total_lines, expected_total_lines) - - def update_panel(self, msg: str) -> None: - self.wm.log_server_message("test", msg) - - def test_server_panel_circular_behavior(self): - n = MAX_LOG_LINES_LIMIT_ON - for i in range(0, n + 1): - self.update_panel(str(i)) - self.update_panel("overflow") - self.update_panel("overflow") - self.update_panel("one\ntwo\nthree") - # The panel only updates when visible but we don't want to test that as - # it would hide the unittesting panel. - self.assert_total_lines_equal(1) diff --git a/tests/test_server_requests.py b/tests/test_server_requests.py deleted file mode 100644 index 2ac9d5814..000000000 --- a/tests/test_server_requests.py +++ /dev/null @@ -1,224 +0,0 @@ -from LSP.plugin.core.protocol import ErrorCodes -from LSP.plugin.core.protocol import TextDocumentSyncKind -from LSP.plugin.core.sessions import SessionBufferProtocol -from LSP.plugin.core.types import ClientConfig -from LSP.plugin.core.typing import Any, Dict, Generator, Optional, List -from LSP.plugin.core.url import filename_to_uri -from setup import TextDocumentTestCase -import os -import sublime -import tempfile - - -def get_auto_complete_trigger(sb: SessionBufferProtocol) -> Optional[List[Dict[str, str]]]: - for sv in sb.session_views: - triggers = sv.view.settings().get("auto_complete_triggers") - for trigger in triggers: - if "server" in trigger and "registration_id" in trigger: - return trigger - return None - - -def verify(testcase: TextDocumentTestCase, method: str, input_params: Any, expected_output_params: Any) -> Generator: - promise = testcase.make_server_do_fake_request(method, input_params) - yield from testcase.await_promise(promise) - testcase.assertEqual(promise.result(), expected_output_params) - - -class ServerRequests(TextDocumentTestCase): - - def test_unknown_method(self) -> Generator: - yield from verify(self, "foobar/qux", {}, {"code": ErrorCodes.MethodNotFound, "message": "foobar/qux"}) - - def test_m_workspace_workspaceFolders(self) -> Generator: - expected_output = [{"name": os.path.basename(f), "uri": filename_to_uri(f)} - for f in sublime.active_window().folders()] - self.maxDiff = None - yield from verify(self, "workspace/workspaceFolders", {}, expected_output) - - def test_m_workspace_configuration(self) -> Generator: - self.session.config.settings.set("foo.bar", "$hello") - self.session.config.settings.set("foo.baz", "$world") - self.session.config.settings.set("foo.a", 1) - self.session.config.settings.set("foo.b", None) - self.session.config.settings.set("foo.c", ["asdf ${hello} ${world}"]) - - class TempPlugin: - - @classmethod - def additional_variables(cls) -> Optional[Dict[str, str]]: - return {"hello": "X", "world": "Y"} - - self.session._plugin_class = TempPlugin # type: ignore - method = "workspace/configuration" - params = {"items": [{"section": "foo"}]} - expected_output = [{"bar": "X", "baz": "Y", "a": 1, "b": None, "c": ["asdf X Y"]}] - yield from verify(self, method, params, expected_output) - self.session.config.settings.clear() - - def test_m_workspace_applyEdit(self) -> Generator: - old_change_count = self.insert_characters("hello\nworld\n") - edit = { - "newText": "there", - "range": {"start": {"line": 1, "character": 0}, "end": {"line": 1, "character": 5}}} - params = {"edit": {"changes": {filename_to_uri(self.view.file_name()): [edit]}}} - yield from verify(self, "workspace/applyEdit", params, {"applied": True}) - yield lambda: self.view.change_count() > old_change_count - self.assertEqual(self.view.substr(sublime.Region(0, self.view.size())), "hello\nthere\n") - - def test_m_workspace_applyEdit_with_nontrivial_promises(self) -> Generator: - with tempfile.TemporaryDirectory() as dirpath: - initial_text = ["a b", "c d"] - file_paths = [] - for i in range(0, 2): - file_paths.append(os.path.join(dirpath, "file{}.txt".format(i))) - with open(file_paths[-1], "w") as fp: - fp.write(initial_text[i]) - yield from verify( - self, - "workspace/applyEdit", - { - "edit": { - "changes": { - filename_to_uri(file_paths[0]): - [ - { - "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 1}}, - "newText": "hello" - }, - { - "range": {"start": {"line": 0, "character": 2}, "end": {"line": 0, "character": 3}}, - "newText": "there" - } - ], - filename_to_uri(file_paths[1]): - [ - { - "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 1}}, - "newText": "general" - }, - { - "range": {"start": {"line": 0, "character": 2}, "end": {"line": 0, "character": 3}}, - "newText": "kenobi" - } - ] - } - } - }, - {"applied": True} - ) - # Changes should have been applied - expected = ["hello there", "general kenobi"] - for i in range(0, 2): - view = self.view.window().find_open_file(file_paths[i]) - self.assertTrue(view) - view.set_scratch(True) - self.assertTrue(view.is_valid()) - self.assertFalse(view.is_loading()) - self.assertEqual(view.substr(sublime.Region(0, view.size())), expected[i]) - view.close() - - def test_m_client_registerCapability(self) -> Generator: - yield from verify( - self, - "client/registerCapability", - { - "registrations": - [ - {"method": "foo/bar", "id": "hello"}, - {"method": "bar/baz", "id": "world", "registerOptions": {"frobnicatable": True}}, - {"method": "workspace/didChangeWorkspaceFolders", "id": "asdf"}, - {"method": "textDocument/didOpen", "id": "1"}, - {"method": "textDocument/willSaveWaitUntil", "id": "2", - "registerOptions": {"documentSelector": [{"language": "plaintext"}]}}, - {"method": "textDocument/didChange", "id": "adsf", - "registerOptions": {"syncKind": TextDocumentSyncKind.Full, "documentSelector": [ - {"language": "plaintext"} - ]}}, - {"method": "textDocument/completion", "id": "myCompletionRegistrationId", - "registerOptions": {"triggerCharacters": ["!", "@", "#"], "documentSelector": [ - {"language": "plaintext"} - ]}} - ] - }, - None) - self.assertIn("barProvider", self.session.capabilities) - self.assertEqual(self.session.capabilities.get("barProvider.id"), "hello") - self.assertIn("bazProvider", self.session.capabilities) - self.assertEqual(self.session.capabilities.get("bazProvider"), {"id": "world", "frobnicatable": True}) - self.assertEqual(self.session.capabilities.get("workspace.workspaceFolders.changeNotifications"), "asdf") - self.assertEqual(self.session.capabilities.get("textDocumentSync.didOpen"), {"id": "1"}) - self.assertFalse(self.session.capabilities.get("textDocumentSync.didClose")) - - # willSaveWaitUntil is *only* registered on the buffer - self.assertFalse(self.session.capabilities.get("textDocumentSync.willSaveWaitUntil")) - sb = next(self.session.session_buffers_async()) - self.assertEqual(sb.capabilities.text_sync_kind(), TextDocumentSyncKind.Full) - self.assertEqual(sb.capabilities.get("textDocumentSync.willSaveWaitUntil"), {"id": "2"}) - self.assertEqual(self.session.capabilities.text_sync_kind(), TextDocumentSyncKind.Incremental) - - # Check that textDocument/completion was registered onto the SessionBuffer, and check that the trigger - # characters for each view were updated - self.assertEqual(sb.capabilities.get("completionProvider.id"), "myCompletionRegistrationId") - self.assertEqual(sb.capabilities.get("completionProvider.triggerCharacters"), ["!", "@", "#"]) - trigger = get_auto_complete_trigger(sb) - self.assertTrue(trigger) - self.assertEqual(trigger.get("characters"), "!@#") - - def test_m_client_unregisterCapability(self) -> Generator: - yield from verify( - self, - "client/registerCapability", - {"registrations": [{"method": "foo/bar", "id": "hello"}]}, - None) - self.assertIn("barProvider", self.session.capabilities) - yield from verify( - self, - "client/unregisterCapability", - {"unregisterations": [{"method": "foo/bar", "id": "hello"}]}, - None) - self.assertNotIn("barProvider", self.session.capabilities) - - -class ServerRequestsWithAutoCompleteSelector(TextDocumentTestCase): - - @classmethod - def get_stdio_test_config(cls) -> ClientConfig: - return ClientConfig.from_config( - super().get_stdio_test_config(), - { - "auto_complete_selector": "punctuation.section", - "disabled_capabilities": { - "completionProvider": { - "triggerCharacters": True - } - } - } - ) - - def test_m_client_registerCapability(self) -> Generator: - yield from verify( - self, - "client/registerCapability", - { - "registrations": - [ - # Note that the triggerCharacters are disabled in the configuration. - {"method": "textDocument/completion", "id": "anotherCompletionRegistrationId", - "registerOptions": {"triggerCharacters": ["!", "@", "#"], "documentSelector": [ - {"language": "plaintext"} - ]}} - ] - }, - None) - sb = next(self.session.session_buffers_async()) - # Check that textDocument/completion was registered onto the SessionBuffer - self.assertEqual(sb.capabilities.get("completionProvider.id"), "anotherCompletionRegistrationId") - # Trigger characters should not have been registered - self.assertFalse(sb.capabilities.get("completionProvider.triggerCharacters")) - trigger = get_auto_complete_trigger(sb) - self.assertTrue(trigger) - # No triggers should have been assigned - self.assertFalse(trigger.get("characters")) - # The selector should have been set - self.assertEqual(trigger.get("selector"), "punctuation.section") diff --git a/tests/test_session.py b/tests/test_session.py deleted file mode 100644 index b6a82d61e..000000000 --- a/tests/test_session.py +++ /dev/null @@ -1,253 +0,0 @@ -from LSP.plugin.core.collections import DottedDict -from LSP.plugin.core.protocol import Diagnostic -from LSP.plugin.core.protocol import DocumentUri -from LSP.plugin.core.protocol import Error -from LSP.plugin.core.protocol import TextDocumentSyncKind -from LSP.plugin.core.sessions import get_initialize_params -from LSP.plugin.core.sessions import Logger -from LSP.plugin.core.sessions import Manager -from LSP.plugin.core.sessions import Session -from LSP.plugin.core.types import ClientConfig -from LSP.plugin.core.typing import Any, Optional, Generator, List, Dict -from LSP.plugin.core.workspace import WorkspaceFolder -from test_mocks import TEST_CONFIG -import sublime -import unittest -import unittest.mock -import weakref - - -class MockManager(Manager): - - def __init__(self, window: sublime.Window) -> None: - self._window = window - - def window(self) -> sublime.Window: - return self._window - - def sessions(self, view: sublime.View, capability: Optional[str] = None) -> Generator[Session, None, None]: - pass - - def get_project_path(self, file_name: str) -> Optional[str]: - return None - - def should_ignore_diagnostics(self, uri: DocumentUri, configuration: ClientConfig) -> Optional[str]: - return None - - def start_async(self, configuration: ClientConfig, initiating_view: sublime.View) -> None: - pass - - def on_post_exit_async(self, session: Session, exit_code: int, exception: Optional[Exception]) -> None: - pass - - def on_diagnostics_updated(self) -> None: - pass - - -class MockLogger(Logger): - - def stderr_message(self, message: str) -> None: - pass - - def outgoing_response(self, request_id: Any, params: Any) -> None: - pass - - def outgoing_error_response(self, request_id: Any, error: Error) -> None: - pass - - def outgoing_request(self, request_id: int, method: str, params: Any, blocking: bool) -> None: - pass - - def outgoing_notification(self, method: str, params: Any) -> None: - pass - - def incoming_response(self, request_id: Optional[int], params: Any, is_error: bool, blocking: bool) -> None: - pass - - def incoming_request(self, request_id: Any, method: str, params: Any) -> None: - pass - - def incoming_notification(self, method: str, params: Any, unhandled: bool) -> None: - pass - - -class MockSessionBuffer: - - def __init__(self, session: Session, mock_uri: str, mock_language_id: str) -> None: - self.session = session - self.session_views = weakref.WeakSet() - self.mock_uri = mock_uri - self.mock_language_id = mock_language_id - - def get_uri(self) -> Optional[DocumentUri]: - return self.mock_uri - - def get_language_id(self) -> Optional[str]: - return self.mock_language_id - - def register_capability_async( - self, - registration_id: str, - capability_path: str, - registration_path: str, - options: Dict[str, Any] - ) -> None: - pass - - def unregister_capability_async( - self, - registration_id: str, - capability_path: str, - registration_path: str - ) -> None: - pass - - def on_diagnostics_async(self, raw_diagnostics: List[Diagnostic], version: Optional[int]) -> None: - pass - - -class SessionTest(unittest.TestCase): - - def test_experimental_capabilities(self) -> None: - wf = WorkspaceFolder.from_path("/foo/bar/baz") - params = get_initialize_params( - {}, - [wf], - ClientConfig(name="test", command=[""], selector="", tcp_port=None, experimental_capabilities=None)) - self.assertNotIn("experimental", params["capabilities"]) - - params = get_initialize_params( - {}, - [wf], - ClientConfig(name="test", command=[""], selector="", tcp_port=None, experimental_capabilities={})) - self.assertIn("experimental", params["capabilities"]) - self.assertEqual(params["capabilities"]["experimental"], {}) - - experimental_capabilities = { - "foo": 1, - "bar": True, - "baz": "abc" - } - config = ClientConfig( - name="test", - command=[""], - selector="", - tcp_port=None, - experimental_capabilities=experimental_capabilities - ) - params = get_initialize_params({}, [wf], config) - self.assertIn("experimental", params["capabilities"]) - self.assertEqual(params["capabilities"]["experimental"], experimental_capabilities) - - def test_initialize_params(self) -> None: - wf = WorkspaceFolder.from_path("/foo/bar/baz") - params = get_initialize_params( - {}, [wf], ClientConfig(name="test", command=[""], selector="", tcp_port=None, init_options=DottedDict())) - self.assertIn("initializationOptions", params) - self.assertEqual(params["initializationOptions"], {}) - params = get_initialize_params( - {}, [wf], ClientConfig( - name="test", command=[""], selector="", tcp_port=None, init_options=DottedDict({"foo": "bar"}))) - self.assertIn("initializationOptions", params) - self.assertEqual(params["initializationOptions"], {"foo": "bar"}) - - def test_document_sync_capabilities(self) -> None: - manager = MockManager(sublime.active_window()) - session = Session(manager=manager, logger=MockLogger(), workspace_folders=[], config=TEST_CONFIG, - plugin_class=None) - session.capabilities.assign({ - 'textDocumentSync': { - "openClose": True, - "change": TextDocumentSyncKind.Full, - "save": True}}) # A boolean with value true means "send didSave" - self.assertTrue(session.should_notify_did_open()) - self.assertTrue(session.should_notify_did_close()) - self.assertEqual(session.text_sync_kind(), TextDocumentSyncKind.Full) - self.assertFalse(session.should_notify_will_save()) - self.assertEqual(session.should_notify_did_save(), (True, False)) - - session.capabilities.assign({ - 'textDocumentSync': { - "didOpen": {}, - "didClose": {}, - "change": TextDocumentSyncKind.Full, - "save": True}}) # A boolean with value true means "send didSave" - self.assertTrue(session.should_notify_did_open()) - self.assertTrue(session.should_notify_did_close()) - self.assertEqual(session.text_sync_kind(), TextDocumentSyncKind.Full) - self.assertFalse(session.should_notify_will_save()) - self.assertEqual(session.should_notify_did_save(), (True, False)) - - session.capabilities.assign({ - 'textDocumentSync': { - "openClose": False, - "change": TextDocumentSyncKind.None_, - "save": {}, # An empty dict means "send didSave" - "willSave": True, - "willSaveWaitUntil": False}}) - self.assertFalse(session.should_notify_did_open()) - self.assertFalse(session.should_notify_did_close()) - self.assertEqual(session.text_sync_kind(), TextDocumentSyncKind.None_) - self.assertTrue(session.should_notify_will_save()) - self.assertEqual(session.should_notify_did_save(), (True, False)) - # Nested capabilities. - self.assertTrue(session.has_capability('textDocumentSync.change')) - self.assertTrue(session.has_capability('textDocumentSync.save')) - self.assertTrue(session.has_capability('textDocumentSync.willSave')) - self.assertFalse(session.has_capability('textDocumentSync.willSaveUntil')) - self.assertFalse(session.has_capability('textDocumentSync.aintthere')) - - session.capabilities.assign({ - 'textDocumentSync': { - "openClose": False, - "change": TextDocumentSyncKind.Incremental, - "save": {"includeText": True}, - "willSave": False, - "willSaveWaitUntil": True}}) - self.assertFalse(session.should_notify_did_open()) - self.assertFalse(session.should_notify_did_close()) - self.assertEqual(session.text_sync_kind(), TextDocumentSyncKind.Incremental) - self.assertFalse(session.should_notify_will_save()) - self.assertEqual(session.should_notify_did_save(), (True, True)) - - session.capabilities.assign({'textDocumentSync': TextDocumentSyncKind.Incremental}) - self.assertTrue(session.should_notify_did_open()) - self.assertTrue(session.should_notify_did_close()) - self.assertEqual(session.text_sync_kind(), TextDocumentSyncKind.Incremental) - self.assertFalse(session.should_notify_will_save()) # old-style text sync will never send willSave - # old-style text sync will always send didSave - self.assertEqual(session.should_notify_did_save(), (True, False)) - - session.capabilities.assign({'textDocumentSync': TextDocumentSyncKind.None_}) - self.assertTrue(session.should_notify_did_open()) # old-style text sync will always send didOpen - self.assertTrue(session.should_notify_did_close()) # old-style text sync will always send didClose - self.assertEqual(session.text_sync_kind(), TextDocumentSyncKind.None_) - self.assertFalse(session.should_notify_will_save()) - self.assertEqual(session.should_notify_did_save(), (True, False)) - - session.capabilities.assign({ - 'textDocumentSync': { - "openClose": True, - "save": False, - "change": TextDocumentSyncKind.Incremental}}) - self.assertTrue(session.should_notify_did_open()) - self.assertTrue(session.should_notify_did_close()) - self.assertEqual(session.text_sync_kind(), TextDocumentSyncKind.Incremental) - self.assertFalse(session.should_notify_will_save()) - self.assertEqual(session.should_notify_did_save(), (False, False)) - - def test_get_session_buffer_for_uri_with_nonfiles(self) -> None: - manager = MockManager(sublime.active_window()) - session = Session(manager=manager, logger=MockLogger(), workspace_folders=[], config=TEST_CONFIG, - plugin_class=None) - original = MockSessionBuffer(session, "some-scheme://whatever", "somelang") - session.register_session_buffer_async(original) - sb = session.get_session_buffer_for_uri_async("some-scheme://whatever") - self.assertIsNotNone(sb) - assert sb - self.assertEqual(sb.get_language_id(), "somelang") - self.assertEqual(sb.get_uri(), "some-scheme://whatever") - - def test_get_session_buffer_for_uri_with_files(self) -> None: - # todo: write windows-only test - pass diff --git a/tests/test_signature_help.py b/tests/test_signature_help.py deleted file mode 100644 index 8de8d43f6..000000000 --- a/tests/test_signature_help.py +++ /dev/null @@ -1,253 +0,0 @@ -from LSP.plugin.core.protocol import SignatureHelp -from LSP.plugin.core.signature_help import SigHelp -import sublime -import unittest - - -class SignatureHelpTest(unittest.TestCase): - - def setUp(self) -> None: - self.view = sublime.active_window().active_view() - - def test_no_signature(self) -> None: - help = SigHelp.from_lsp(None, None) - self.assertIsNone(help) - - def test_empty_signature_list(self) -> None: - help = SigHelp.from_lsp({"signatures": []}, None) - self.assertIsNone(help) - - def assert_render(self, input: SignatureHelp, regex: str) -> None: - help = SigHelp(input, None) - assert self.view - self.assertRegex(help.render(self.view), regex.replace("\n", "").replace(" ", "")) - - def test_signature(self) -> None: - self.assert_render( - { - "signatures": - [ - { - "label": "f(x)", - "documentation": "f does interesting things", - "parameters": - [ - { - "label": "x", - "documentation": "must be in the frobnicate range" - } - ] - } - ], - "activeSignature": 0, - "activeParameter": 0 - }, - r''' -
-            f\(
-            x
-            \)
-            
-

must be in the frobnicate range

-
-

f does interesting things

- ''' - ) - - def test_markdown(self) -> None: - self.assert_render( - { - "signatures": - [ - { - "label": "f(x)", - "documentation": - { - "value": "f does _interesting_ things", - "kind": "markdown" - }, - "parameters": - [ - { - "label": "x", - "documentation": - { - "value": "must be in the **frobnicate** range", - "kind": "markdown" - } - } - ] - } - ], - "activeSignature": 0, - "activeParameter": 0 - }, - r''' -
-            f\(
-            x
-            \)
-            
-

must be in the frobnicate range

-
-

f does interesting things

- ''' - ) - - def test_second_parameter(self) -> None: - self.assert_render( - { - "signatures": - [ - { - "label": "f(x, y)", - "parameters": - [ - { - "label": "x" - }, - { - "label": "y", - "documentation": "hello there" - } - ] - } - ], - "activeSignature": 0, - "activeParameter": 1 - }, - r''' -
-            f\(
-            x
-            , 
-            y
-            \)
-            
-

hello there

- ''' - ) - - def test_parameter_ranges(self) -> None: - self.assert_render( - { - "signatures": - [ - { - "label": "f(x, y)", - "parameters": - [ - { - "label": [2, 3], - }, - { - "label": [5, 6], - "documentation": "hello there" - } - ] - } - ], - "activeSignature": 0, - "activeParameter": 1 - }, - r''' -
-            f\(
-            x
-            , 
-            y
-            \)
-            
-

hello there

- ''' - ) - - def test_overloads(self) -> None: - self.assert_render( - { - "signatures": - [ - { - "label": "f(x, y)", - "parameters": - [ - { - "label": [2, 3] - }, - { - "label": [5, 6], - "documentation": "hello there" - } - ] - }, - { - "label": "f(x, a, b)", - "parameters": - [ - { - "label": [2, 3] - }, - { - "label": [5, 6] - }, - { - "label": [8, 9] - } - ] - } - ], - "activeSignature": 1, - "activeParameter": 0 - }, - r''' -

-

- 2 of 2 overloads \(use to navigate, press Esc to hide\): -
-

-
f\(
-            x
-            , 
-            a
-            , 
-            b
-            \)
-            
- ''' - ) - - def test_dockerfile_signature(self) -> None: - self.assert_render( - { - "signatures": - [ - { - "label": 'RUN [ "command" "parameters", ... ]', - "parameters": - [ - {'label': '['}, - {'label': '"command"'}, - {'label': '"parameters"'}, - {'label': '...'}, - {'label': ']'} - ] - } - ], - "activeSignature": 0, - "activeParameter": 2 - }, - r''' -
-            RUN 
-            \[
-             
-            "command"
-             
-            "parameters"
-            , 
-            \.\.\.
-             
-            \]
-            
- ''' - ) diff --git a/tests/test_types.py b/tests/test_types.py deleted file mode 100644 index 5b1806cfd..000000000 --- a/tests/test_types.py +++ /dev/null @@ -1,256 +0,0 @@ -from LSP.plugin.core.types import diff -from LSP.plugin.core.types import basescope2languageid -from LSP.plugin.core.types import DocumentSelector -from LSP.plugin.core.typing import List -from unittest.mock import MagicMock -import sublime -import unittest - - -class TestDiff(unittest.TestCase): - - def test_add(self) -> None: - added, removed = diff(("a", "b", "c"), ("a", "b", "c", "d")) - self.assertEqual(added, set(("d",))) - self.assertFalse(removed) - - def test_remove(self) -> None: - added, removed = diff(("a", "b", "c"), ("c", "b")) - self.assertFalse(added) - self.assertEqual(removed, set(("a",))) - - def test_add_and_remove(self) -> None: - added, removed = diff(("a", "b", "c"), ("c", "d")) - self.assertEqual(added, set(("d",))) - self.assertEqual(removed, set(("a", "b"))) - - def test_with_sets(self) -> None: - added, removed = diff(set(("a", "b", "c")), ("x", "y", "z")) - self.assertEqual(added, set(("x", "y", "z"))) - self.assertEqual(removed, set(("a", "b", "c"))) - - def test_with_more_sets(self) -> None: - added, removed = diff(set(("a", "b")), set(("b", "c"))) - self.assertEqual(added, set(("c",))) - self.assertEqual(removed, set(("a",))) - - def test_completely_new(self) -> None: - new = {"ocaml", "polymer-ide", "elixir-ls", "jdtls", "dart", "reason", "golsp", "clangd", "pwsh", "vhdl_ls"} - added, removed = diff(set(), new) - self.assertEqual(added, new) - self.assertFalse(removed) - - -class TestDocumentSelector(unittest.TestCase): - - def setUp(self) -> None: - self._opened_views = [] # type: List[sublime.View] - - def tearDown(self) -> None: - for view in self._opened_views: - view.close() - self._opened_views.clear() - - def _make_view(self, syntax: str, file_name: str) -> sublime.View: - view = sublime.active_window().new_file(0, syntax) - self._opened_views.append(view) - view.set_scratch(True) - self.assertFalse(view.is_loading()) - view.file_name = MagicMock(return_value=file_name) - return view - - def test_language(self) -> None: - selector = DocumentSelector([{"language": "plaintext"}]) - view = self._make_view("Packages/Text/Plain text.tmLanguage", "foobar.txt") - self.assertTrue(selector.matches(view)) - view = self._make_view("Packages/Python/Python.sublime-syntax", "hello.py") - self.assertFalse(selector.matches(view)) - - def test_pattern_basics(self) -> None: - selector = DocumentSelector([{"language": "html", "pattern": "**/*.component.html"}]) - view = self._make_view("Packages/HTML/HTML.sublime-syntax", "index.html") - self.assertFalse(selector.matches(view)) - view = self._make_view("Packages/HTML/HTML.sublime-syntax", "components/foo.component.html") - self.assertTrue(selector.matches(view)) - - def _make_html_view(self, file_name: str) -> sublime.View: - return self._make_view("Packages/HTML/HTML.sublime-syntax", file_name) - - def test_pattern_asterisk(self) -> None: - """`*` to match one or more characters in a path segment""" - selector = DocumentSelector([{"language": "html", "pattern": "a*c.html"}]) - # self.assertFalse(selector.matches(self._make_html_view("ac.html"))) - self.assertTrue(selector.matches(self._make_html_view("abc.html"))) - self.assertTrue(selector.matches(self._make_html_view("axyc.html"))) - - def test_pattern_optional(self) -> None: - """`?` to match on one character in a path segment""" - selector = DocumentSelector([{"language": "html", "pattern": "a?c.html"}]) - self.assertTrue(selector.matches(self._make_html_view("axc.html"))) - self.assertTrue(selector.matches(self._make_html_view("ayc.html"))) - self.assertFalse(selector.matches(self._make_html_view("ac.html"))) - self.assertFalse(selector.matches(self._make_html_view("axyc.html"))) - - def test_pattern_globstar(self) -> None: - """`**` to match any number of path segments, including none""" - selector = DocumentSelector([{"language": "html", "pattern": "**/abc.html"}]) - self.assertTrue(selector.matches(self._make_html_view("foo/bar/abc.html"))) - self.assertFalse(selector.matches(self._make_html_view("asdf/qwerty/abc.htm"))) - - def test_pattern_grouping(self) -> None: - """`{}` to group conditions (e.g. `**/*.{ts,js}` matches all TypeScript and JavaScript files)""" - selector = DocumentSelector([{"pattern": "**/*.{ts,js}"}]) - self.assertTrue(selector.matches(self._make_view( - "Packages/JavaScript/TypeScript.sublime-syntax", "foo/bar.ts"))) - self.assertTrue(selector.matches(self._make_view( - "Packages/JavaScript/JavaScript.sublime-syntax", "asdf/qwerty.js"))) - self.assertFalse(selector.matches(self._make_view( - "Packages/JavaScript/TypeScript.sublime-syntax", "foo/bar.no-match-ts"))) - self.assertFalse(selector.matches(self._make_view( - "Packages/JavaScript/JavaScript.sublime-syntax", "asdf/qwerty.no-match-js"))) - - def test_pattern_character_range(self) -> None: - """ - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on - `example.0`, `example.1`, …) - """ - selector = DocumentSelector([{"language": "html", "pattern": "example.[0-9]"}]) - self.assertTrue(selector.matches(self._make_html_view("example.0"))) - self.assertTrue(selector.matches(self._make_html_view("example.1"))) - self.assertTrue(selector.matches(self._make_html_view("example.2"))) - self.assertTrue(selector.matches(self._make_html_view("example.3"))) - self.assertTrue(selector.matches(self._make_html_view("example.4"))) - self.assertTrue(selector.matches(self._make_html_view("example.5"))) - self.assertTrue(selector.matches(self._make_html_view("example.6"))) - self.assertTrue(selector.matches(self._make_html_view("example.7"))) - self.assertTrue(selector.matches(self._make_html_view("example.8"))) - self.assertTrue(selector.matches(self._make_html_view("example.9"))) - self.assertFalse(selector.matches(self._make_html_view("example.10"))) - - def test_pattern_negated_character_range(self) -> None: - """ - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on - `example.a`, `example.b`, but not `example.0`) - """ - selector = DocumentSelector([{"language": "html", "pattern": "example.[!0-9]"}]) - self.assertTrue(selector.matches(self._make_html_view("example.a"))) - self.assertTrue(selector.matches(self._make_html_view("example.b"))) - self.assertTrue(selector.matches(self._make_html_view("example.c"))) - self.assertFalse(selector.matches(self._make_html_view("example.0"))) - self.assertFalse(selector.matches(self._make_html_view("example.1"))) - self.assertFalse(selector.matches(self._make_html_view("example.2"))) - self.assertFalse(selector.matches(self._make_html_view("example.3"))) - self.assertFalse(selector.matches(self._make_html_view("example.4"))) - self.assertFalse(selector.matches(self._make_html_view("example.5"))) - self.assertFalse(selector.matches(self._make_html_view("example.6"))) - self.assertFalse(selector.matches(self._make_html_view("example.7"))) - self.assertFalse(selector.matches(self._make_html_view("example.8"))) - self.assertFalse(selector.matches(self._make_html_view("example.9"))) - - def test_base_scope_to_language_id_mappings(self) -> None: - scope_test_map = { - "source.js.vite": "javascript", - "source.c++": "cpp", - "source.coffee.gulpfile": "coffeescript", - "source.cs": "csharp", - "source.css.tailwind": "css", - "source.dosbatch": "bat", - "source.fixedform-fortran": "fortran", - "source.groovy.gradle": "groovy", - "source.groovy.jenkins": "groovy", - "source.js": "javascript", - "source.js.eslint": "javascript", - "source.js.gruntfile": "javascript", - "source.js.gulpfile": "javascript", - "source.js.postcss": "javascript", - "source.js.puglint": "javascript", - "source.js.react": "javascriptreact", - "source.js.stylelint": "javascript", - "source.js.unittest": "javascript", - "source.js.webpack": "javascript", - "source.json-tmlanguage": "jsonc", - "source.json.babel": "json", - "source.json.bower": "json", - "source.json.composer": "json", - "source.json.eslint": "json", - "source.json.npm": "json", - "source.json.postcss": "json", - "source.json.puglint": "json", - "source.json.settings": "json", - "source.json.stylelint": "json", - "source.json.sublime": "jsonc", - "source.json.sublime.build": "jsonc", - "source.json.sublime.color-scheme": "jsonc", - "source.json.sublime.commands": "jsonc", - "source.json.sublime.completions": "jsonc", - "source.json.sublime.keymap": "jsonc", - "source.json.sublime.macro": "jsonc", - "source.json.sublime.menu": "jsonc", - "source.json.sublime.mousemap": "jsonc", - "source.json.sublime.project": "jsonc", - "source.json.sublime.settings": "jsonc", - "source.json.sublime.theme": "jsonc", - "source.json.tern": "json", - "source.jsx": "javascriptreact", - "source.jsx.unittest": "javascriptreact", - "source.Kotlin": "kotlin", - "source.modern-fortran": "fortran", - "source.objc": "objective-c", - "source.objc++": "objective-cpp", - "source.shader": "shaderlab", - "source.shell.bash": "shellscript", - "source.shell.docker": "shellscript", - "source.shell.eslint": "shellscript", - "source.shell.npm": "shellscript", - "source.shell.ruby": "shellscript", - "source.shell.stylelint": "shellscript", - "source.ts": "typescript", - "source.ts.react": "typescriptreact", - "source.ts.unittest": "typescript", - "source.tsx": "typescriptreact", - "source.tsx.unittest": "typescriptreact", - "source.unity.unity_shader": "shaderlab", - "source.viml.vimrc": "viml", - "source.yaml-tmlanguage": "yaml", - "source.yaml.circleci": "yaml", - "source.yaml.docker": "yaml", - "source.yaml.eslint": "yaml", - "source.yaml.lock": "yaml", - "source.yaml.procfile": "yaml", - "source.yaml.stylelint": "yaml", - "source.yaml.sublime.syntax": "yaml", - "source.yaml.yarn": "yaml", - "text.advanced_csv": "csv", - "text.django": "html", - "text.html.basic": "html", - "text.html.elixir": "html", - "text.html.markdown.academicmarkdown": "markdown", - "text.html.markdown.license": "markdown", - "text.html.markdown.rmarkdown": "r", - "text.html.ngx": "html", - "text.jinja": "html", - "text.plain": "plaintext", - "text.plain.buildpacks": "plaintext", - "text.plain.eslint": "plaintext", - "text.plain.fastq": "plaintext", - "text.plain.license": "plaintext", - "text.plain.lnk": "plaintext", - "text.plain.log": "plaintext", - "text.plain.nodejs": "plaintext", - "text.plain.pcb": "plaintext", - "text.plain.ps": "plaintext", - "text.plain.python": "plaintext", - "text.plain.readme": "plaintext", - "text.plain.ruby": "plaintext", - "text.plain.sketch": "plaintext", - "text.plain.visualstudio": "plaintext", - "text.plist": "xml", - "text.xml.plist": "xml", - "text.xml.plist.textmate.preferences": "xml", - "text.xml.sublime.snippet": "xml", - "text.xml.svg": "xml", - "text.xml.visualstudio": "xml", - } - - for base_scope, expected_language_id in scope_test_map.items(): - self.assertEqual(basescope2languageid(base_scope), expected_language_id) diff --git a/tests/test_url.py b/tests/test_url.py deleted file mode 100644 index 31a06556f..000000000 --- a/tests/test_url.py +++ /dev/null @@ -1,79 +0,0 @@ -from LSP.plugin.core.url import filename_to_uri -from LSP.plugin.core.url import parse_uri -from LSP.plugin.core.url import view_to_uri -import os -import sublime -import sys -import unittest -import unittest.mock - - -@unittest.skipUnless(sys.platform.startswith("win"), "requires Windows") -class WindowsTests(unittest.TestCase): - - def test_converts_path_to_uri(self): - self.assertEqual("file:///C:/dir%20ectory/file.txt", filename_to_uri("c:\\dir ectory\\file.txt")) - - def test_converts_uri_to_path(self): - self.assertEqual("C:\\dir ectory\\file.txt", parse_uri("file:///c:/dir ectory/file.txt")[1]) - - def test_converts_encoded_bad_drive_uri_to_path(self): - # url2pathname does not understand %3A - self.assertEqual("C:\\dir ectory\\file.txt", parse_uri("file:///c%3A/dir%20ectory/file.txt")[1]) - - def test_view_to_uri_with_valid_filename(self): - view = sublime.active_window().active_view() - assert view - view.file_name = unittest.mock.MagicMock( - return_value="C:\\Users\\A b\\popups.css" - ) - uri = view_to_uri(view) - self.assertEqual(uri, "file:///C:/Users/A%20b/popups.css") - - def test_unc_path(self): - scheme, path = parse_uri('file://192.168.80.2/D%24/www/File.php') - self.assertEqual(scheme, "file") - self.assertEqual(path, R'\\192.168.80.2\D$\www\File.php') - - def test_wsl_path(self): - scheme, path = parse_uri('file://wsl%24/Ubuntu-20.04/File.php') - self.assertEqual(scheme, "file") - self.assertEqual(path, R'\\wsl$\Ubuntu-20.04\File.php') - - -@unittest.skipIf(sys.platform.startswith("win"), "requires non-Windows") -class NixTests(unittest.TestCase): - - def test_converts_path_to_uri(self): - self.assertEqual("file:///dir%20ectory/file.txt", filename_to_uri("/dir ectory/file.txt")) - - def test_converts_uri_to_path(self): - self.assertEqual("/dir ectory/file.txt", parse_uri("file:///dir ectory/file.txt")[1]) - - def test_view_to_uri_with_valid_filename(self): - view = sublime.active_window().active_view() - assert view - view.file_name = unittest.mock.MagicMock(return_value="/foo/bar/baz.txt") - uri = view_to_uri(view) - self.assertEqual(uri, "file:///foo/bar/baz.txt") - - -class MultiplatformTests(unittest.TestCase): - - def test_resource_path(self): - uri = filename_to_uri(os.path.join(sublime.installed_packages_path(), "Package Control", "dir", "file.py")) - self.assertEqual(uri, "res:/Packages/Package%20Control/dir/file.py") - - def test_buffer_uri(self): - view = sublime.active_window().active_view() - assert view - view.file_name = unittest.mock.MagicMock(return_value=None) - view.buffer_id = unittest.mock.MagicMock(return_value=42) - uri = view_to_uri(view) - self.assertEqual(uri, "buffer:42") - - def test_parse_uri(self): - scheme, _ = parse_uri("buffer:42") - self.assertEqual(scheme, "buffer") - scheme, _ = parse_uri("www.example.com/foo:bar") - self.assertEqual(scheme, "") diff --git a/tests/test_views.py b/tests/test_views.py deleted file mode 100644 index 9b6780800..000000000 --- a/tests/test_views.py +++ /dev/null @@ -1,408 +0,0 @@ -from copy import deepcopy -from LSP.plugin.core.protocol import CodeActionKind -from LSP.plugin.core.protocol import Diagnostic -from LSP.plugin.core.protocol import Point -from LSP.plugin.core.protocol import DiagnosticSeverity -from LSP.plugin.core.types import Any -from LSP.plugin.core.url import filename_to_uri -from LSP.plugin.core.views import did_change -from LSP.plugin.core.views import did_open -from LSP.plugin.core.views import did_save -from LSP.plugin.core.views import document_color_params -from LSP.plugin.core.views import format_diagnostic_for_html -from LSP.plugin.core.views import FORMAT_STRING, FORMAT_MARKED_STRING, FORMAT_MARKUP_CONTENT, minihtml -from LSP.plugin.core.views import lsp_color_to_html -from LSP.plugin.core.views import lsp_color_to_phantom -from LSP.plugin.core.views import MissingUriError -from LSP.plugin.core.views import point_to_offset -from LSP.plugin.core.views import range_to_region -from LSP.plugin.core.views import selection_range_params -from LSP.plugin.core.views import text2html -from LSP.plugin.core.views import text_document_code_action_params -from LSP.plugin.core.views import text_document_formatting -from LSP.plugin.core.views import text_document_position_params -from LSP.plugin.core.views import text_document_range_formatting -from LSP.plugin.core.views import uri_from_view -from LSP.plugin.core.views import will_save -from LSP.plugin.core.views import will_save_wait_until -from setup import make_stdio_test_config -from unittest.mock import MagicMock -from unittesting import DeferrableTestCase -import re -import sublime - - -class ViewsTest(DeferrableTestCase): - - def setUp(self) -> None: - super().setUp() - self.view = sublime.active_window().new_file() # new_file() always returns a ready view - self.view.set_scratch(True) - self.mock_file_name = "C:/Windows" if sublime.platform() == "windows" else "/etc" - self.view.file_name = MagicMock(return_value=self.mock_file_name) - self.view.run_command("insert", {"characters": "hello world\nfoo bar baz"}) - - def tearDown(self) -> None: - self.view.close() - return super().tearDown() - - def test_missing_uri(self) -> None: - self.view.settings().erase("lsp_uri") - with self.assertRaises(MissingUriError): - uri_from_view(self.view) - - def test_nonmissing_uri(self) -> None: - - class MockSettings: - - def get(value: str, default: Any) -> Any: - return "file:///hello/there.txt" - - mock_settings = MockSettings() - self.view.settings = MagicMock(return_value=mock_settings) - uri = uri_from_view(self.view) - self.assertEqual(uri, "file:///hello/there.txt") - - def test_did_open(self) -> None: - self.view.settings().set("lsp_uri", filename_to_uri(self.mock_file_name)) - self.assertEqual(did_open(self.view, "python").params, { - "textDocument": { - "uri": filename_to_uri(self.mock_file_name), - "languageId": "python", - "text": "hello world\nfoo bar baz", - "version": self.view.change_count() - } - }) - - def test_did_change_full(self) -> None: - version = self.view.change_count() - self.view.settings().set("lsp_uri", filename_to_uri(self.mock_file_name)) - self.assertEqual(did_change(self.view, version).params, { - "textDocument": { - "uri": filename_to_uri(self.mock_file_name), - "version": version - }, - "contentChanges": [{"text": "hello world\nfoo bar baz"}] - }) - - def test_will_save(self) -> None: - self.view.settings().set("lsp_uri", filename_to_uri(self.mock_file_name)) - self.assertEqual(will_save(filename_to_uri(self.mock_file_name), 42).params, { - "textDocument": {"uri": filename_to_uri(self.mock_file_name)}, - "reason": 42 - }) - - def test_will_save_wait_until(self) -> None: - self.view.settings().set("lsp_uri", filename_to_uri(self.mock_file_name)) - self.assertEqual(will_save_wait_until(self.view, 1337).params, { - "textDocument": {"uri": filename_to_uri(self.mock_file_name)}, - "reason": 1337 - }) - - def test_did_save(self) -> None: - self.view.settings().set("lsp_uri", filename_to_uri(self.mock_file_name)) - self.assertEqual(did_save(self.view, include_text=False).params, { - "textDocument": {"uri": filename_to_uri(self.mock_file_name)} - }) - self.assertEqual(did_save(self.view, include_text=True).params, { - "textDocument": {"uri": filename_to_uri(self.mock_file_name)}, - "text": "hello world\nfoo bar baz" - }) - - def test_text_document_position_params(self) -> None: - self.view.settings().set("lsp_uri", filename_to_uri(self.mock_file_name)) - self.assertEqual(text_document_position_params(self.view, 2), { - "textDocument": {"uri": filename_to_uri(self.mock_file_name)}, - "position": {"line": 0, "character": 2} - }) - - def test_text_document_formatting(self) -> None: - self.view.settings = MagicMock(return_value={ - "translate_tabs_to_spaces": False, - "tab_size": 1234, - "ensure_newline_at_eof_on_save": True, - "lsp_uri": filename_to_uri(self.mock_file_name) - }) - self.assertEqual(text_document_formatting(self.view).params, { - "textDocument": {"uri": filename_to_uri(self.mock_file_name)}, - "options": { - "tabSize": 1234, - "insertSpaces": False, - "trimTrailingWhitespace": False, - "insertFinalNewline": True, - "trimFinalNewlines": True - } - }) - - def test_text_document_range_formatting(self) -> None: - self.view.settings = MagicMock(return_value={ - "tab_size": 4321, - "lsp_uri": filename_to_uri(self.mock_file_name) - }) - self.assertEqual(text_document_range_formatting(self.view, sublime.Region(0, 2)).params, { - "textDocument": {"uri": filename_to_uri(self.mock_file_name)}, - "options": { - "tabSize": 4321, - "insertSpaces": False, - "trimTrailingWhitespace": False, - "insertFinalNewline": False, - "trimFinalNewlines": False - }, - "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 2}} - }) - - def test_point_to_offset(self) -> None: - first_line_length = len(self.view.line(0)) - self.assertEqual(point_to_offset(Point(1, 2), self.view), first_line_length + 3) - self.assertEqual(point_to_offset(Point(0, first_line_length + 9999), self.view), first_line_length) - - def test_point_to_offset_utf16(self) -> None: - self.view.run_command("insert", {"characters": "🍺foo"}) - foobarbaz_length = len("foo bar baz") - offset = point_to_offset(Point(1, foobarbaz_length), self.view) - # Sanity check - self.assertEqual(self.view.substr(offset), "🍺") - # When we move two UTF-16 points further, we should encompass the beer emoji. - # So that means that the code point offsets should have a difference of 1. - self.assertEqual(point_to_offset(Point(1, foobarbaz_length + 2), self.view) - offset, 1) - - def test_selection_range_params(self) -> None: - self.view.run_command("lsp_selection_set", {"regions": [(0, 5), (6, 11)]}) - self.view.settings().set("lsp_uri", filename_to_uri(self.mock_file_name)) - self.assertEqual(len(self.view.sel()), 2) - self.assertEqual(self.view.substr(self.view.sel()[0]), "hello") - self.assertEqual(self.view.substr(self.view.sel()[1]), "world") - self.assertEqual(selection_range_params(self.view), { - "textDocument": {"uri": filename_to_uri(self.mock_file_name)}, - "positions": [ - {"line": 0, "character": 5}, - {"line": 0, "character": 11} - ] - }) - - def test_minihtml_no_allowed_formats(self) -> None: - content = "
text\n
" - with self.assertRaises(Exception): - minihtml(self.view, content, allowed_formats=0) - - def test_minihtml_conflicting_formats(self) -> None: - content = "
text\n
" - with self.assertRaises(Exception): - minihtml(self.view, content, allowed_formats=FORMAT_STRING | FORMAT_MARKED_STRING) - - def test_minihtml_format_string(self) -> None: - content = "
text\n
" - expect = "

<div>text
</div>

" - self.assertEqual(minihtml(self.view, content, allowed_formats=FORMAT_STRING), expect) - - def test_minihtml_format_marked_string(self) -> None: - content = "
text\n
" - expect = "
text\n
" - self.assertEqual(minihtml(self.view, content, allowed_formats=FORMAT_MARKED_STRING), expect) - - def test_minihtml_format_markup_content(self) -> None: - content = {'value': 'This is **bold** text', 'kind': 'markdown'} - expect = "

This is bold text

" - self.assertEqual(minihtml(self.view, content, allowed_formats=FORMAT_MARKUP_CONTENT), expect) - - def test_minihtml_handles_markup_content_plaintext(self) -> None: - content = {'value': 'type TVec2i = specialize TGVec2', 'kind': 'plaintext'} - expect = "

type TVec2i = specialize TGVec2<Integer>

" - allowed_formats = FORMAT_MARKED_STRING | FORMAT_MARKUP_CONTENT - self.assertEqual(minihtml(self.view, content, allowed_formats=allowed_formats), expect) - - def test_minihtml_handles_marked_string(self) -> None: - content = {'value': 'import json', 'language': 'python'} - expect = '
import json
' - allowed_formats = FORMAT_MARKED_STRING | FORMAT_MARKUP_CONTENT - formatted = self._strip_style_attributes(minihtml(self.view, content, allowed_formats=allowed_formats)) - self.assertEqual(formatted, expect) - - def test_minihtml_handles_marked_string_mutiple_spaces(self) -> None: - content = {'value': 'import json', 'language': 'python'} - expect = '
import  json
' - allowed_formats = FORMAT_MARKED_STRING | FORMAT_MARKUP_CONTENT - formatted = self._strip_style_attributes(minihtml(self.view, content, allowed_formats=allowed_formats)) - self.assertEqual(formatted, expect) - - def test_minihtml_handles_marked_string_array(self) -> None: - content = [ - {'value': 'import sys', 'language': 'python'}, - {'value': 'let x', 'language': 'js'} - ] - expect = '\n\n'.join([ - '
import sys
', - '
let x
' - ]) - allowed_formats = FORMAT_MARKED_STRING | FORMAT_MARKUP_CONTENT - formatted = self._strip_style_attributes(minihtml(self.view, content, allowed_formats=allowed_formats)) - self.assertEqual(formatted, expect) - - def test_minihtml_ignores_non_allowed_string(self) -> None: - content = "
text\n
" - expect = "" - self.assertEqual(minihtml(self.view, content, allowed_formats=FORMAT_MARKUP_CONTENT), expect) - - def test_minihtml_ignores_non_allowed_marked_string(self) -> None: - content = {'value': 'import sys', 'language': 'python'} - expect = "" - self.assertEqual(minihtml(self.view, content, allowed_formats=FORMAT_MARKUP_CONTENT), expect) - - def test_minihtml_ignores_non_allowed_marked_string_array(self) -> None: - content = ["a", "b"] - expect = "" - self.assertEqual(minihtml(self.view, content, allowed_formats=FORMAT_MARKUP_CONTENT), expect) - - def test_minihtml_ignores_non_allowed_markup_content(self) -> None: - content = {'value': 'ab', 'kind': 'plaintext'} - expect = "" - self.assertEqual(minihtml(self.view, content, allowed_formats=FORMAT_STRING), expect) - - def test_minihtml_magiclinks(self) -> None: - content = {'value': 'https://github.com/sublimelsp/LSP', 'kind': 'markdown'} - expect_attributes = [ - 'class="magiclink magiclink-github magiclink-repository"', - 'href="https://github.com/sublimelsp/LSP"', - 'title="GitHub Repository: sublimelsp/LSP"' - ] - expect = '

sublimelsp/LSP

'.format(' '.join(expect_attributes)) - self.assertEqual(minihtml(self.view, content, allowed_formats=FORMAT_MARKUP_CONTENT), expect) - - def _strip_style_attributes(self, content: str) -> str: - return re.sub(r'\s+style="[^"]+"', '', content) - - def test_text2html_replaces_tabs_with_br(self) -> None: - self.assertEqual(text2html("Hello,\t world "), "Hello,     world ") - - def test_text2html_non_breaking_space_and_control_char_with_entity(self) -> None: - self.assertEqual(text2html("no\xc2\xa0breaks"), "no  breaks") - - def test_text2html_replaces_two_or_more_spaces_with_nbsp(self) -> None: - content = " One Two Three One Four" - expect = " One  Two   Three One    Four" - self.assertEqual(text2html(content), expect) - - def test_text2html_does_not_replace_one_space_with_nbsp(self) -> None: - content = " John has one apple " - self.assertEqual(text2html(content), content) - - def test_text2html_replaces_newlines_with_br(self) -> None: - self.assertEqual(text2html("a\nb"), "a
b") - - def test_text2html_parses_link_simple(self) -> None: - content = "https://github.com/sublimelsp/LSP" - expect = "https://github.com/sublimelsp/LSP" - self.assertEqual(text2html(content), expect) - - def test_text2html_parses_link_in_angle_brackets(self) -> None: - content = "" - expect = "<https://github.com/sublimelsp/LSP>" - self.assertEqual(text2html(content), expect) - - def test_text2html_parses_link_in_double_quotes(self) -> None: - content = "\"https://github.com/sublimelsp/LSP\"" - expect = "\"https://github.com/sublimelsp/LSP\"" - self.assertEqual(text2html(content), expect) - - def test_text2html_parses_link_in_single_quotes(self) -> None: - content = "'https://github.com/sublimelsp/LSP'" - expect = "'https://github.com/sublimelsp/LSP'" - self.assertEqual(text2html(content), expect) - - def test_lsp_color_to_phantom(self) -> None: - response = [ - { - "color": { - "green": 0.9725490196078431, - "blue": 1, - "red": 0.9411764705882353, - "alpha": 1 - }, - "range": { - "start": { - "character": 0, - "line": 0 - }, - "end": { - "character": 5, - "line": 0 - } - } - } - ] - phantom = lsp_color_to_phantom(self.view, response[0]) - self.assertEqual(phantom.content, lsp_color_to_html(response[0])) - self.assertEqual(phantom.region, range_to_region(response[0]["range"], self.view)) - - def test_document_color_params(self) -> None: - self.view.settings().set("lsp_uri", filename_to_uri(self.mock_file_name)) - self.assertEqual( - document_color_params(self.view), - {"textDocument": {"uri": filename_to_uri(self.mock_file_name)}}) - - def test_text_document_code_action_params(self) -> None: - self.view.settings().set("lsp_uri", filename_to_uri(self.mock_file_name)) - diagnostic = { - "message": "oops", - "severity": DiagnosticSeverity.Error, - "range": { - "start": { - "character": 0, - "line": 0 - }, - "end": { - "character": 1, - "line": 0 - } - } - } # type: Diagnostic - self.view.run_command("append", {"characters": "a b c\n"}) - params = text_document_code_action_params( - view=self.view, - region=sublime.Region(0, 1), - diagnostics=[diagnostic], - only_kinds=[CodeActionKind.Refactor] - ) - self.assertEqual(params["textDocument"], {"uri": filename_to_uri(self.mock_file_name)}) - - def test_format_diagnostic_for_html(self) -> None: - diagnostic1 = { - "message": "oops", - "severity": DiagnosticSeverity.Error, - # The relatedInformation is present here, but it's an empty list. - # This should have the same behavior as having no relatedInformation present. - "relatedInformation": [], - "range": { - "start": { - "character": 0, - "line": 0 - }, - "end": { - "character": 5, - "line": 0 - } - } - } # type: Diagnostic - # Make the same diagnostic but without the relatedInformation - diagnostic2 = deepcopy(diagnostic1) - diagnostic2.pop("relatedInformation") - self.assertIn("relatedInformation", diagnostic1) - self.assertNotIn("relatedInformation", diagnostic2) - client_config = make_stdio_test_config() - # They should result in the same minihtml. - self.assertEqual( - format_diagnostic_for_html(client_config, diagnostic1, "/foo/bar"), - format_diagnostic_for_html(client_config, diagnostic2, "/foo/bar") - ) - - def test_escaped_newline_in_markdown(self) -> None: - self.assertEqual( - minihtml(self.view, {"kind": "markdown", "value": "hello\\\nworld"}, FORMAT_MARKUP_CONTENT), - "

hello\\\nworld

" - ) - - def test_single_backslash_in_markdown(self) -> None: - self.assertEqual( - minihtml(self.view, {"kind": "markdown", "value": "A\\B"}, FORMAT_MARKUP_CONTENT), - "

A\\B

" - ) diff --git a/tests/test_workspace.py b/tests/test_workspace.py deleted file mode 100644 index 3d73191b7..000000000 --- a/tests/test_workspace.py +++ /dev/null @@ -1,48 +0,0 @@ -from LSP.plugin.core.workspace import sorted_workspace_folders, is_subpath_of, WorkspaceFolder -import os -import unittest -import tempfile - - -class SortedWorkspaceFoldersTest(unittest.TestCase): - - def test_get_workspace_from_multi_folder_project(self) -> None: - nearest_project_path = os.path.dirname(__file__) - unrelated_project_path = tempfile.gettempdir() - parent_project_path = os.path.abspath(os.path.join(nearest_project_path, '..')) - folders = sorted_workspace_folders([unrelated_project_path, parent_project_path, nearest_project_path], - __file__) - nearest_folder = WorkspaceFolder.from_path(nearest_project_path) - parent_folder = WorkspaceFolder.from_path(parent_project_path) - unrelated_folder = WorkspaceFolder.from_path(unrelated_project_path) - self.assertEqual(folders[0], nearest_folder) - self.assertEqual(folders[1], parent_folder) - self.assertEqual(folders[2], unrelated_folder) - - def test_longest_prefix(self) -> None: - folders = sorted_workspace_folders(["/longer-path", "/short-path"], "/short-path/file.js") - self.assertEqual(folders[0].path, "/short-path") - - -class WorkspaceFolderTest(unittest.TestCase): - - def test_workspace_str(self) -> None: - workspace = WorkspaceFolder("LSP", "/foo/bar/baz") - self.assertEqual(str(workspace), "/foo/bar/baz") - - def test_workspace_repr(self) -> None: - workspace = WorkspaceFolder("LSP", "/foo/bar/baz") - # This also tests the equality operator - self.assertEqual(workspace, eval(repr(workspace))) - - def test_workspace_to_dict(self) -> None: - workspace = WorkspaceFolder("LSP", "/foo/bar/baz") - lsp_dict = workspace.to_lsp() - self.assertEqual(lsp_dict, {"name": "LSP", "uri": "file:///foo/bar/baz"}) - - -class IsSubpathOfTest(unittest.TestCase): - - def is_subpath_case_differs(self) -> None: - self.assertTrue(is_subpath_of(r"e:\WWW\nthu-ee-iframe\public\include\list_faculty_functions.php", - r"E:\WWW\nthu-ee-iframe")) diff --git a/tests/testfile2.txt b/tests/testfile2.txt index e69de29bb..3b1246497 100644 --- a/tests/testfile2.txt +++ b/tests/testfile2.txt @@ -0,0 +1 @@ +TEST \ No newline at end of file From d710022fef505828158eac13623edd30a6efed98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Mon, 8 Apr 2024 11:53:59 +0200 Subject: [PATCH 11/20] remove test_file_watcher --- tests/test_file_watcher.py | 139 ------------------------------------- 1 file changed, 139 deletions(-) delete mode 100644 tests/test_file_watcher.py diff --git a/tests/test_file_watcher.py b/tests/test_file_watcher.py deleted file mode 100644 index 6074a75cf..000000000 --- a/tests/test_file_watcher.py +++ /dev/null @@ -1,139 +0,0 @@ -from LSP.plugin import FileWatcher -from LSP.plugin import FileWatcherEvent -from LSP.plugin import FileWatcherEventType -from LSP.plugin import FileWatcherProtocol -from LSP.plugin.core.file_watcher import file_watcher_event_type_to_lsp_file_change_type -from LSP.plugin.core.file_watcher import register_file_watcher_implementation -from LSP.plugin.core.protocol import WatchKind -from LSP.plugin.core.types import ClientConfig -from LSP.plugin.core.types import sublime_pattern_to_glob -from LSP.plugin.core.typing import Generator, List, Optional -from os.path import join -from setup import expand -from setup import TextDocumentTestCase -import sublime -import unittest - - -class TestFileWatcher(FileWatcher): - - # The list of watchers created by active sessions. - _active_watchers = [] # type: List[TestFileWatcher] - - @classmethod - def create( - cls, - root_path: str, - patterns: List[str], - events: List[FileWatcherEventType], - ignores: List[str], - handler: FileWatcherProtocol - ) -> 'TestFileWatcher': - watcher = TestFileWatcher(root_path, patterns, events, ignores, handler) - cls._active_watchers.append(watcher) - return watcher - - def __init__( - self, - root_path: str, - patterns: List[str], - events: List[FileWatcherEventType], - ignores: List[str], - handler: FileWatcherProtocol - ) -> None: - self.root_path = root_path - self.patterns = patterns - self.events = events - self.ignores = ignores - self.handler = handler - - def destroy(self) -> None: - self.handler = None - self._active_watchers.remove(self) - - def trigger_event(self, events: List[FileWatcherEvent]) -> None: - - def trigger_async(): - if self.handler: - self.handler.on_file_event_async(events) - - sublime.set_timeout_async(trigger_async) - - - -class PatternToGlobTests(unittest.TestCase): - - def test_basic_directory_patterns(self): - patterns = [ - '.git', - 'CVS', - '.Trash-*', - ] - self._verify_patterns( - patterns, - [ - '**/.git/**', - '**/CVS/**', - '**/.Trash-*/**', - ], - is_directory_pattern=True) - - def test_complex_directory_patterns(self): - patterns = [ - '*/foo', - 'foo/bar', - 'foo/bar/', - '/foo', - ] - self._verify_patterns( - patterns, - [ - '**/foo/**', - '**/foo/bar/**', - '**/foo/bar/**', - '/foo/**', - ], - is_directory_pattern=True) - - def test_basic_file_patterns(self): - self._verify_patterns( - [ - '*.pyc', - ".DS_Store", - - ], - [ - '**/*.pyc', - '**/.DS_Store', - ], - is_directory_pattern=False) - - def test_complex_file_patterns(self): - self._verify_patterns( - [ - "/*.pyo", - ], - [ - '/*.pyo', - ], - is_directory_pattern=False) - - def test_project_relative_patterns(self): - self._verify_patterns(['//foo'], ['/Users/me/foo/**'], is_directory_pattern=True, root_path='/Users/me') - self._verify_patterns(['//*.pyo'], ['/Users/me/*.pyo'], is_directory_pattern=False, root_path='/Users/me') - # Without root_path those will be treated as absolute paths even when starting with multiple slashes. - self._verify_patterns(['//foo'], ['//foo/**'], is_directory_pattern=True) - self._verify_patterns(['//*.pyo'], ['//*.pyo'], is_directory_pattern=False) - - def _verify_patterns( - self, - patterns: List[str], - expected: List[str], - is_directory_pattern: bool, - root_path: Optional[str] = None - ) -> None: - glob_patterns = [ - sublime_pattern_to_glob(pattern, is_directory_pattern=is_directory_pattern, root_path=root_path) - for pattern in patterns - ] - self.assertEqual(glob_patterns, expected) From 520473fdfd6b01709d81e42d0fc0d28db834f4a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B?= Date: Wed, 10 Apr 2024 16:26:51 +0200 Subject: [PATCH 12/20] leave only 3 tests --- tests/test_single_document.py | 328 ---------------------------------- 1 file changed, 328 deletions(-) diff --git a/tests/test_single_document.py b/tests/test_single_document.py index 4ac3b4255..afe772aae 100644 --- a/tests/test_single_document.py +++ b/tests/test_single_document.py @@ -59,25 +59,6 @@ def test_did_open(self) -> None: # -> "shutdown" -> client shut down pass - def test_out_of_bounds_column_for_text_document_edit(self) -> None: - self.insert_characters("a\nb\nc\n") - apply_text_edits(self.view, [ - { - 'newText': 'hello there', - 'range': { - 'start': { - 'line': 1, - 'character': 0, - }, - 'end': { - 'line': 1, - 'character': 10000, - } - } - }, - ]) - self.assertEqual(entire_content(self.view), "a\nhello there\nc\n") - def test_did_close(self) -> 'Generator': self.assertTrue(self.view) self.assertTrue(self.view.is_valid()) @@ -109,312 +90,3 @@ def test_did_change(self) -> 'Generator': } }) - def test_sends_save_with_purge(self) -> 'Generator': - assert self.view - self.view.settings().set("lsp_format_on_save", False) - self.insert_characters("A") - self.view.run_command("lsp_save", {'async': True}) - yield from self.await_message("textDocument/didChange") - yield from self.await_message("textDocument/didSave") - yield from self.await_clear_view_and_save() - - def test_formats_on_save(self) -> 'Generator': - assert self.view - self.view.settings().set("lsp_format_on_save", True) - self.insert_characters("A") - yield from self.await_message("textDocument/didChange") - self.set_response('textDocument/formatting', [{ - 'newText': "BBB", - 'range': { - 'start': {'line': 0, 'character': 0}, - 'end': {'line': 0, 'character': 1} - } - }]) - self.view.run_command("lsp_save", {'async': True}) - yield from self.await_message("textDocument/formatting") - yield from self.await_message("textDocument/didChange") - yield from self.await_message("textDocument/didSave") - text = self.view.substr(sublime.Region(0, self.view.size())) - self.assertEquals("BBB", text) - yield from self.await_clear_view_and_save() - - def test_hover_info(self) -> 'Generator': - assert self.view - self.set_response('textDocument/hover', {"contents": "greeting"}) - self.view.run_command('insert', {"characters": "Hello Wrld"}) - self.assertFalse(self.view.is_popup_visible()) - self.view.run_command('lsp_hover', {'point': 3}) - yield lambda: self.view.is_popup_visible() - last_content = _test_contents[-1] - self.assertTrue("greeting" in last_content) - - def test_remove_line_and_then_insert_at_that_line_at_end(self) -> 'Generator': - original = ( - 'a\n' - 'b\n' - 'c' - ) - file_changes = [ - ((2, 0), (3, 0), ''), # out-of-bounds end position, but this is fine - ((3, 0), (3, 0), 'c\n') # out-of-bounds start and end, this line doesn't exist - ] - expected = ( - 'a\n' - 'b\n' - 'c\n' - ) - # Old behavior: - # 1) first we end up with ('a\n', 'b\n', 'cc\n') - # 2) then we end up with ('a\n', 'b\n', '') - # New behavior: - # 1) line index 3 is "created" ('a\n', 'b\n', 'c\n', c\n')) - # 2) deletes line index 2. - yield from self.__run_formatting_test(original, expected, file_changes) - - def test_apply_formatting(self) -> 'Generator': - original = ( - '\n' - '\n' - '\n' - '\n' - ) - file_changes = [ - ((0, 28), (1, 0), ''), # delete first \n - ((1, 0), (1, 15), ''), # delete second line (but not the \n) - ((2, 10), (2, 10), '\n '), # insert after