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'''
-
- 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'''
-
- 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'''
-
- 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'''
-
- 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 = ''
- 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 = ''
- 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([
- '',
- ''
- ])
- 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'
- '\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
- ]
- expected = (
- '\n'
- '\n'
- ' \n'
- '\n'
- '\n'
- )
- yield from self.__run_formatting_test(original, expected, file_changes)
-
- def test_apply_formatting_and_preserve_order(self) -> 'Generator':
- original = (
- 'abcde\n'
- 'fghij\n'
- )
- # Note that (1, 2) comes before (0, 1) in the text.
- file_changes = [
- ((1, 2), (1, 2), '4'), # insert after the g
- ((1, 2), (1, 2), '5'),
- ((1, 2), (1, 3), '6'), # replace the h
- ((0, 1), (0, 1), '1'), # insert after a
- ((0, 1), (0, 1), '2'),
- ((0, 1), (0, 1), '3'),
- ]
- expected = (
- 'a123bcde\n'
- 'fg456ij\n'
- )
- yield from self.__run_formatting_test(original, expected, file_changes)
-
- def test_tabs_are_respected_even_when_translate_tabs_to_spaces_is_set_to_true(self) -> 'Generator':
- original = ' ' * 4
- file_changes = [((0, 0), (0, 4), '\t')]
- expected = '\t'
- assert self.view
- self.view.settings().set("translate_tabs_to_spaces", True)
- yield from self.__run_formatting_test(original, expected, file_changes)
- # Make sure the user's settings haven't changed
- self.assertTrue(self.view.settings().get("translate_tabs_to_spaces"))
-
- def __run_formatting_test(
- self,
- original: 'Iterable[str]',
- expected: 'Iterable[str]',
- file_changes: 'List[Tuple[Tuple[int, int], Tuple[int, int], str]]'
- ) -> 'Generator':
- assert self.view
- original_change_count = self.insert_characters(''.join(original))
- # self.assertEqual(original_change_count, 1)
- self.set_response('textDocument/formatting', [{
- 'newText': new_text,
- 'range': {
- 'start': {'line': start[0], 'character': start[1]},
- 'end': {'line': end[0], 'character': end[1]}}} for start, end, new_text in file_changes])
- self.view.run_command('lsp_format_document')
- yield from self.await_message('textDocument/formatting')
- yield from self.await_view_change(original_change_count + len(file_changes))
- edited_content = self.view.substr(sublime.Region(0, self.view.size()))
- self.assertEquals(edited_content, ''.join(expected))
-
- def __run_goto_test(self, response: list, text_document_request: str, subl_command_suffix: str) -> 'Generator':
- assert self.view
- self.insert_characters(GOTO_CONTENT)
- # Put the cursor back at the start of the buffer, otherwise is_at_word fails in goto.py.
- self.view.sel().clear()
- self.view.sel().add(sublime.Region(0, 0))
- method = 'textDocument/{}'.format(text_document_request)
- self.set_response(method, response)
- self.view.run_command('lsp_symbol_{}'.format(subl_command_suffix))
- yield from self.await_message(method)
-
- def condition() -> bool:
- nonlocal self
- assert self.view
- s = self.view.sel()
- if len(s) != 1:
- return False
- return s[0].begin() > 0
-
- yield {"condition": condition, "timeout": TIMEOUT_TIME}
- first = self.view.sel()[0].begin()
- self.assertEqual(self.view.substr(sublime.Region(first, first + 1)), "F")
-
- def test_definition(self) -> 'Generator':
- yield from self.__run_goto_test(GOTO_RESPONSE, 'definition', 'definition')
-
- def test_definition_location_link(self) -> 'Generator':
- yield from self.__run_goto_test(GOTO_RESPONSE_LOCATION_LINK, 'definition', 'definition')
-
- def test_type_definition(self) -> 'Generator':
- yield from self.__run_goto_test(GOTO_RESPONSE, 'typeDefinition', 'type_definition')
-
- def test_type_definition_location_link(self) -> 'Generator':
- yield from self.__run_goto_test(GOTO_RESPONSE_LOCATION_LINK, 'typeDefinition', 'type_definition')
-
- def test_declaration(self) -> 'Generator':
- yield from self.__run_goto_test(GOTO_RESPONSE, 'declaration', 'declaration')
-
- def test_declaration_location_link(self) -> 'Generator':
- yield from self.__run_goto_test(GOTO_RESPONSE_LOCATION_LINK, 'declaration', 'declaration')
-
- def test_implementation(self) -> 'Generator':
- yield from self.__run_goto_test(GOTO_RESPONSE, 'implementation', 'implementation')
-
- def test_implementation_location_link(self) -> 'Generator':
- yield from self.__run_goto_test(GOTO_RESPONSE_LOCATION_LINK, 'implementation', 'implementation')
-
- def test_expand_selection(self) -> 'Generator':
- self.insert_characters("abcba\nabcba\nabcba\n")
- self.view.run_command("lsp_selection_set", {"regions": [(2, 2)]})
- self.assertEqual(len(self.view.sel()), 1)
- self.assertEqual(self.view.substr(self.view.sel()[0]), "")
- self.assertEqual(self.view.substr(self.view.sel()[0].a), "c")
- response = [{
- "parent": {
- "parent": {
- "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 5}}
- },
- "range": {"start": {"line": 0, "character": 1}, "end": {"line": 0, "character": 3}}
- },
- "range": {"start": {"line": 0, "character": 2}, "end": {"line": 0, "character": 3}}
- }]
-
- def expand_and_check(a: int, b: int) -> 'Generator':
- self.set_response("textDocument/selectionRange", response)
- self.view.run_command("lsp_expand_selection")
- yield from self.await_message("textDocument/selectionRange")
- yield lambda: self.view.sel()[0] == sublime.Region(a, b)
-
- yield from expand_and_check(2, 3)
- yield from expand_and_check(1, 3)
- yield from expand_and_check(0, 5)
-
- def test_rename(self) -> 'Generator':
- self.insert_characters("foo\nfoo\nfoo\n")
- self.set_response("textDocument/rename", {
- 'changes': {
- filename_to_uri(TEST_FILE_PATH): [
- {
- 'range': {'start': {'character': 0, 'line': 0}, 'end': {'character': 3, 'line': 0}},
- 'newText': 'bar'
- },
- {
- 'range': {'start': {'character': 0, 'line': 1}, 'end': {'character': 3, 'line': 1}},
- 'newText': 'bar'
- },
- {
- 'range':
- {
- 'start': {'character': 0, 'line': 2},
- # Check that lsp_apply_document_edit guards for overflow over LSP spec limit of UINT_MAX
- 'end': {'character': UINT_MAX + 1, 'line': 2}
- },
- 'newText': 'bar'
- }
- ]
- }
- }
- )
- self.view.run_command("lsp_selection_set", {"regions": [(0, 0)]})
- self.view.run_command("lsp_symbol_rename", {"new_name": "bar"})
- yield from self.await_message("textDocument/rename")
- yield from self.await_view_change(9)
- self.assertEqual(self.view.substr(sublime.Region(0, self.view.size())), "bar\nbar\nbar\n")
-
- def test_run_command(self) -> 'Generator':
- self.set_response("workspace/executeCommand", {"canReturnAnythingHere": "asdf"})
- promise = YieldPromise()
- sublime.set_timeout_async(
- lambda: self.session.execute_command(
- {"command": "foo", "arguments": ["hello", "there", "general", "kenobi"]},
- progress=False,
- view=self.view,
- ).then(promise.fulfill)
- )
- yield from self.await_promise(promise)
- yield from self.await_message("workspace/executeCommand")
- self.assertEqual(promise.result(), {"canReturnAnythingHere": "asdf"})
-
- def test_progress(self) -> 'Generator':
- request = Request("foobar", {"hello": "world"}, self.view, progress=True)
- self.set_response("foobar", {"general": "kenobi"})
- promise = self.session.send_request_task(request)
- yield lambda: "workDoneToken" in request.params
- result = yield from self.await_promise(promise)
- self.assertEqual(result, {"general": "kenobi"})
-
-
-class WillSaveWaitUntilTestCase(TextDocumentTestCase):
-
- @classmethod
- def get_test_server_capabilities(cls) -> dict:
- capabilities = deepcopy(super().get_test_server_capabilities())
- capabilities['capabilities']['textDocumentSync']['willSaveWaitUntil'] = True
- return capabilities
-
- def test_will_save_wait_until(self) -> 'Generator':
- assert self.view
- self.insert_characters("A")
- yield from self.await_message("textDocument/didChange")
- self.set_response('textDocument/willSaveWaitUntil', [{
- 'newText': "BBB",
- 'range': {
- 'start': {'line': 0, 'character': 0},
- 'end': {'line': 0, 'character': 1}
- }
- }])
- self.view.settings().set("lsp_format_on_save", False)
- self.view.run_command("lsp_save", {'async': True})
- yield from self.await_message("textDocument/willSaveWaitUntil")
- 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()
-
-
-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", {
- "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 09bca595b1bea1cbb90d19e44cf41c7a26820082 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:29:49 +0200
Subject: [PATCH 13/20] fix style
---
tests/test_single_document.py | 8 --------
1 file changed, 8 deletions(-)
diff --git a/tests/test_single_document.py b/tests/test_single_document.py
index afe772aae..f54ce92d5 100644
--- a/tests/test_single_document.py
+++ b/tests/test_single_document.py
@@ -1,14 +1,7 @@
-from copy import deepcopy
-from LSP.plugin import apply_text_edits, Request
-from LSP.plugin.core.protocol import UINT_MAX
from LSP.plugin.core.url import filename_to_uri
-from LSP.plugin.core.views import entire_content
-from LSP.plugin.hover import _test_contents
from setup import TextDocumentTestCase
-from setup import TIMEOUT_TIME
from setup import YieldPromise
import os
-import sublime
try:
@@ -89,4 +82,3 @@ def test_did_change(self) -> 'Generator':
'uri': filename_to_uri(TEST_FILE_PATH)
}
})
-
From 9e6bf1a4bb9076cda86ce871c91590bf78bbe27a 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:32:18 +0200
Subject: [PATCH 14/20] bring all tests from SingleDocumentTestCase
---
tests/test_single_document.py | 336 ++++++++++++++++++++++++++++++++++
1 file changed, 336 insertions(+)
diff --git a/tests/test_single_document.py b/tests/test_single_document.py
index f54ce92d5..4ac3b4255 100644
--- a/tests/test_single_document.py
+++ b/tests/test_single_document.py
@@ -1,7 +1,14 @@
+from copy import deepcopy
+from LSP.plugin import apply_text_edits, Request
+from LSP.plugin.core.protocol import UINT_MAX
from LSP.plugin.core.url import filename_to_uri
+from LSP.plugin.core.views import entire_content
+from LSP.plugin.hover import _test_contents
from setup import TextDocumentTestCase
+from setup import TIMEOUT_TIME
from setup import YieldPromise
import os
+import sublime
try:
@@ -52,6 +59,25 @@ 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())
@@ -82,3 +108,313 @@ def test_did_change(self) -> 'Generator':
'uri': filename_to_uri(TEST_FILE_PATH)
}
})
+
+ def test_sends_save_with_purge(self) -> 'Generator':
+ assert self.view
+ self.view.settings().set("lsp_format_on_save", False)
+ 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'
+ '\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
+ ]
+ expected = (
+ '\n'
+ '\n'
+ ' \n'
+ '\n'
+ '\n'
+ )
+ yield from self.__run_formatting_test(original, expected, file_changes)
+
+ def test_apply_formatting_and_preserve_order(self) -> 'Generator':
+ original = (
+ 'abcde\n'
+ 'fghij\n'
+ )
+ # Note that (1, 2) comes before (0, 1) in the text.
+ file_changes = [
+ ((1, 2), (1, 2), '4'), # insert after the g
+ ((1, 2), (1, 2), '5'),
+ ((1, 2), (1, 3), '6'), # replace the h
+ ((0, 1), (0, 1), '1'), # insert after a
+ ((0, 1), (0, 1), '2'),
+ ((0, 1), (0, 1), '3'),
+ ]
+ expected = (
+ 'a123bcde\n'
+ 'fg456ij\n'
+ )
+ yield from self.__run_formatting_test(original, expected, file_changes)
+
+ def test_tabs_are_respected_even_when_translate_tabs_to_spaces_is_set_to_true(self) -> 'Generator':
+ original = ' ' * 4
+ file_changes = [((0, 0), (0, 4), '\t')]
+ expected = '\t'
+ assert self.view
+ self.view.settings().set("translate_tabs_to_spaces", True)
+ yield from self.__run_formatting_test(original, expected, file_changes)
+ # Make sure the user's settings haven't changed
+ self.assertTrue(self.view.settings().get("translate_tabs_to_spaces"))
+
+ def __run_formatting_test(
+ self,
+ original: 'Iterable[str]',
+ expected: 'Iterable[str]',
+ file_changes: 'List[Tuple[Tuple[int, int], Tuple[int, int], str]]'
+ ) -> 'Generator':
+ assert self.view
+ original_change_count = self.insert_characters(''.join(original))
+ # self.assertEqual(original_change_count, 1)
+ self.set_response('textDocument/formatting', [{
+ 'newText': new_text,
+ 'range': {
+ 'start': {'line': start[0], 'character': start[1]},
+ 'end': {'line': end[0], 'character': end[1]}}} for start, end, new_text in file_changes])
+ self.view.run_command('lsp_format_document')
+ yield from self.await_message('textDocument/formatting')
+ yield from self.await_view_change(original_change_count + len(file_changes))
+ edited_content = self.view.substr(sublime.Region(0, self.view.size()))
+ self.assertEquals(edited_content, ''.join(expected))
+
+ def __run_goto_test(self, response: list, text_document_request: str, subl_command_suffix: str) -> 'Generator':
+ assert self.view
+ self.insert_characters(GOTO_CONTENT)
+ # Put the cursor back at the start of the buffer, otherwise is_at_word fails in goto.py.
+ self.view.sel().clear()
+ self.view.sel().add(sublime.Region(0, 0))
+ method = 'textDocument/{}'.format(text_document_request)
+ self.set_response(method, response)
+ self.view.run_command('lsp_symbol_{}'.format(subl_command_suffix))
+ yield from self.await_message(method)
+
+ def condition() -> bool:
+ nonlocal self
+ assert self.view
+ s = self.view.sel()
+ if len(s) != 1:
+ return False
+ return s[0].begin() > 0
+
+ yield {"condition": condition, "timeout": TIMEOUT_TIME}
+ first = self.view.sel()[0].begin()
+ self.assertEqual(self.view.substr(sublime.Region(first, first + 1)), "F")
+
+ def test_definition(self) -> 'Generator':
+ yield from self.__run_goto_test(GOTO_RESPONSE, 'definition', 'definition')
+
+ def test_definition_location_link(self) -> 'Generator':
+ yield from self.__run_goto_test(GOTO_RESPONSE_LOCATION_LINK, 'definition', 'definition')
+
+ def test_type_definition(self) -> 'Generator':
+ yield from self.__run_goto_test(GOTO_RESPONSE, 'typeDefinition', 'type_definition')
+
+ def test_type_definition_location_link(self) -> 'Generator':
+ yield from self.__run_goto_test(GOTO_RESPONSE_LOCATION_LINK, 'typeDefinition', 'type_definition')
+
+ def test_declaration(self) -> 'Generator':
+ yield from self.__run_goto_test(GOTO_RESPONSE, 'declaration', 'declaration')
+
+ def test_declaration_location_link(self) -> 'Generator':
+ yield from self.__run_goto_test(GOTO_RESPONSE_LOCATION_LINK, 'declaration', 'declaration')
+
+ def test_implementation(self) -> 'Generator':
+ yield from self.__run_goto_test(GOTO_RESPONSE, 'implementation', 'implementation')
+
+ def test_implementation_location_link(self) -> 'Generator':
+ yield from self.__run_goto_test(GOTO_RESPONSE_LOCATION_LINK, 'implementation', 'implementation')
+
+ def test_expand_selection(self) -> 'Generator':
+ self.insert_characters("abcba\nabcba\nabcba\n")
+ self.view.run_command("lsp_selection_set", {"regions": [(2, 2)]})
+ self.assertEqual(len(self.view.sel()), 1)
+ self.assertEqual(self.view.substr(self.view.sel()[0]), "")
+ self.assertEqual(self.view.substr(self.view.sel()[0].a), "c")
+ response = [{
+ "parent": {
+ "parent": {
+ "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 5}}
+ },
+ "range": {"start": {"line": 0, "character": 1}, "end": {"line": 0, "character": 3}}
+ },
+ "range": {"start": {"line": 0, "character": 2}, "end": {"line": 0, "character": 3}}
+ }]
+
+ def expand_and_check(a: int, b: int) -> 'Generator':
+ self.set_response("textDocument/selectionRange", response)
+ self.view.run_command("lsp_expand_selection")
+ yield from self.await_message("textDocument/selectionRange")
+ yield lambda: self.view.sel()[0] == sublime.Region(a, b)
+
+ yield from expand_and_check(2, 3)
+ yield from expand_and_check(1, 3)
+ yield from expand_and_check(0, 5)
+
+ def test_rename(self) -> 'Generator':
+ self.insert_characters("foo\nfoo\nfoo\n")
+ self.set_response("textDocument/rename", {
+ 'changes': {
+ filename_to_uri(TEST_FILE_PATH): [
+ {
+ 'range': {'start': {'character': 0, 'line': 0}, 'end': {'character': 3, 'line': 0}},
+ 'newText': 'bar'
+ },
+ {
+ 'range': {'start': {'character': 0, 'line': 1}, 'end': {'character': 3, 'line': 1}},
+ 'newText': 'bar'
+ },
+ {
+ 'range':
+ {
+ 'start': {'character': 0, 'line': 2},
+ # Check that lsp_apply_document_edit guards for overflow over LSP spec limit of UINT_MAX
+ 'end': {'character': UINT_MAX + 1, 'line': 2}
+ },
+ 'newText': 'bar'
+ }
+ ]
+ }
+ }
+ )
+ self.view.run_command("lsp_selection_set", {"regions": [(0, 0)]})
+ self.view.run_command("lsp_symbol_rename", {"new_name": "bar"})
+ yield from self.await_message("textDocument/rename")
+ yield from self.await_view_change(9)
+ self.assertEqual(self.view.substr(sublime.Region(0, self.view.size())), "bar\nbar\nbar\n")
+
+ def test_run_command(self) -> 'Generator':
+ self.set_response("workspace/executeCommand", {"canReturnAnythingHere": "asdf"})
+ promise = YieldPromise()
+ sublime.set_timeout_async(
+ lambda: self.session.execute_command(
+ {"command": "foo", "arguments": ["hello", "there", "general", "kenobi"]},
+ progress=False,
+ view=self.view,
+ ).then(promise.fulfill)
+ )
+ yield from self.await_promise(promise)
+ yield from self.await_message("workspace/executeCommand")
+ self.assertEqual(promise.result(), {"canReturnAnythingHere": "asdf"})
+
+ def test_progress(self) -> 'Generator':
+ request = Request("foobar", {"hello": "world"}, self.view, progress=True)
+ self.set_response("foobar", {"general": "kenobi"})
+ promise = self.session.send_request_task(request)
+ yield lambda: "workDoneToken" in request.params
+ result = yield from self.await_promise(promise)
+ self.assertEqual(result, {"general": "kenobi"})
+
+
+class WillSaveWaitUntilTestCase(TextDocumentTestCase):
+
+ @classmethod
+ def get_test_server_capabilities(cls) -> dict:
+ capabilities = deepcopy(super().get_test_server_capabilities())
+ capabilities['capabilities']['textDocumentSync']['willSaveWaitUntil'] = True
+ return capabilities
+
+ def test_will_save_wait_until(self) -> 'Generator':
+ assert self.view
+ self.insert_characters("A")
+ yield from self.await_message("textDocument/didChange")
+ self.set_response('textDocument/willSaveWaitUntil', [{
+ 'newText': "BBB",
+ 'range': {
+ 'start': {'line': 0, 'character': 0},
+ 'end': {'line': 0, 'character': 1}
+ }
+ }])
+ self.view.settings().set("lsp_format_on_save", False)
+ self.view.run_command("lsp_save", {'async': True})
+ yield from self.await_message("textDocument/willSaveWaitUntil")
+ 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()
+
+
+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", {
+ "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 865d4e263bb2c9804f922e6bf4a618166602d2e1 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 17:39:02 +0200
Subject: [PATCH 15/20] just move test_did_change to be the first method...
---
tests/test_single_document.py | 49 +++++++++++++++++------------------
1 file changed, 24 insertions(+), 25 deletions(-)
diff --git a/tests/test_single_document.py b/tests/test_single_document.py
index 4ac3b4255..36ef79bee 100644
--- a/tests/test_single_document.py
+++ b/tests/test_single_document.py
@@ -53,6 +53,30 @@
class SingleDocumentTestCase(TextDocumentTestCase):
+ def test_did_change(self) -> 'Generator':
+ assert self.view
+ self.maxDiff = None
+ self.insert_characters("A")
+ yield from self.await_message("textDocument/didChange")
+ # multiple changes are batched into one didChange notification
+ self.insert_characters("B\n")
+ self.insert_characters("🙂\n")
+ self.insert_characters("D")
+ promise = YieldPromise()
+ yield from self.await_message("textDocument/didChange", promise)
+ self.assertEqual(promise.result(), {
+ 'contentChanges': [
+ {'rangeLength': 0, 'range': {'start': {'line': 0, 'character': 1}, 'end': {'line': 0, 'character': 1}}, 'text': 'B'}, # noqa
+ {'rangeLength': 0, 'range': {'start': {'line': 0, 'character': 2}, 'end': {'line': 0, 'character': 2}}, 'text': '\n'}, # noqa
+ {'rangeLength': 0, 'range': {'start': {'line': 1, 'character': 0}, 'end': {'line': 1, 'character': 0}}, 'text': '🙂'}, # noqa
+ # Note that this is character offset (2) is correct (UTF-16).
+ {'rangeLength': 0, 'range': {'start': {'line': 1, 'character': 2}, 'end': {'line': 1, 'character': 2}}, 'text': '\n'}, # noqa
+ {'rangeLength': 0, 'range': {'start': {'line': 2, 'character': 0}, 'end': {'line': 2, 'character': 0}}, 'text': 'D'}], # noqa
+ 'textDocument': {
+ 'version': self.view.change_count(),
+ 'uri': filename_to_uri(TEST_FILE_PATH)
+ }
+ })
def test_did_open(self) -> None:
# Just the existence of this method checks "initialize" -> "initialized" -> "textDocument/didOpen"
@@ -84,31 +108,6 @@ def test_did_close(self) -> 'Generator':
self.view.close()
yield from self.await_message("textDocument/didClose")
- def test_did_change(self) -> 'Generator':
- assert self.view
- self.maxDiff = None
- self.insert_characters("A")
- yield from self.await_message("textDocument/didChange")
- # multiple changes are batched into one didChange notification
- self.insert_characters("B\n")
- self.insert_characters("🙂\n")
- self.insert_characters("D")
- promise = YieldPromise()
- yield from self.await_message("textDocument/didChange", promise)
- self.assertEqual(promise.result(), {
- 'contentChanges': [
- {'rangeLength': 0, 'range': {'start': {'line': 0, 'character': 1}, 'end': {'line': 0, 'character': 1}}, 'text': 'B'}, # noqa
- {'rangeLength': 0, 'range': {'start': {'line': 0, 'character': 2}, 'end': {'line': 0, 'character': 2}}, 'text': '\n'}, # noqa
- {'rangeLength': 0, 'range': {'start': {'line': 1, 'character': 0}, 'end': {'line': 1, 'character': 0}}, 'text': '🙂'}, # noqa
- # Note that this is character offset (2) is correct (UTF-16).
- {'rangeLength': 0, 'range': {'start': {'line': 1, 'character': 2}, 'end': {'line': 1, 'character': 2}}, 'text': '\n'}, # noqa
- {'rangeLength': 0, 'range': {'start': {'line': 2, 'character': 0}, 'end': {'line': 2, 'character': 0}}, 'text': 'D'}], # noqa
- 'textDocument': {
- 'version': self.view.change_count(),
- 'uri': filename_to_uri(TEST_FILE_PATH)
- }
- })
-
def test_sends_save_with_purge(self) -> 'Generator':
assert self.view
self.view.settings().set("lsp_format_on_save", False)
From f86e381dd860736355a5439f87f9fd4c57170740 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 17:41:20 +0200
Subject: [PATCH 16/20] Revert "just move test_did_change to be the first
method..."
This reverts commit 865d4e263bb2c9804f922e6bf4a618166602d2e1.
---
tests/test_single_document.py | 49 ++++++++++++++++++-----------------
1 file changed, 25 insertions(+), 24 deletions(-)
diff --git a/tests/test_single_document.py b/tests/test_single_document.py
index 36ef79bee..4ac3b4255 100644
--- a/tests/test_single_document.py
+++ b/tests/test_single_document.py
@@ -53,30 +53,6 @@
class SingleDocumentTestCase(TextDocumentTestCase):
- def test_did_change(self) -> 'Generator':
- assert self.view
- self.maxDiff = None
- self.insert_characters("A")
- yield from self.await_message("textDocument/didChange")
- # multiple changes are batched into one didChange notification
- self.insert_characters("B\n")
- self.insert_characters("🙂\n")
- self.insert_characters("D")
- promise = YieldPromise()
- yield from self.await_message("textDocument/didChange", promise)
- self.assertEqual(promise.result(), {
- 'contentChanges': [
- {'rangeLength': 0, 'range': {'start': {'line': 0, 'character': 1}, 'end': {'line': 0, 'character': 1}}, 'text': 'B'}, # noqa
- {'rangeLength': 0, 'range': {'start': {'line': 0, 'character': 2}, 'end': {'line': 0, 'character': 2}}, 'text': '\n'}, # noqa
- {'rangeLength': 0, 'range': {'start': {'line': 1, 'character': 0}, 'end': {'line': 1, 'character': 0}}, 'text': '🙂'}, # noqa
- # Note that this is character offset (2) is correct (UTF-16).
- {'rangeLength': 0, 'range': {'start': {'line': 1, 'character': 2}, 'end': {'line': 1, 'character': 2}}, 'text': '\n'}, # noqa
- {'rangeLength': 0, 'range': {'start': {'line': 2, 'character': 0}, 'end': {'line': 2, 'character': 0}}, 'text': 'D'}], # noqa
- 'textDocument': {
- 'version': self.view.change_count(),
- 'uri': filename_to_uri(TEST_FILE_PATH)
- }
- })
def test_did_open(self) -> None:
# Just the existence of this method checks "initialize" -> "initialized" -> "textDocument/didOpen"
@@ -108,6 +84,31 @@ def test_did_close(self) -> 'Generator':
self.view.close()
yield from self.await_message("textDocument/didClose")
+ def test_did_change(self) -> 'Generator':
+ assert self.view
+ self.maxDiff = None
+ self.insert_characters("A")
+ yield from self.await_message("textDocument/didChange")
+ # multiple changes are batched into one didChange notification
+ self.insert_characters("B\n")
+ self.insert_characters("🙂\n")
+ self.insert_characters("D")
+ promise = YieldPromise()
+ yield from self.await_message("textDocument/didChange", promise)
+ self.assertEqual(promise.result(), {
+ 'contentChanges': [
+ {'rangeLength': 0, 'range': {'start': {'line': 0, 'character': 1}, 'end': {'line': 0, 'character': 1}}, 'text': 'B'}, # noqa
+ {'rangeLength': 0, 'range': {'start': {'line': 0, 'character': 2}, 'end': {'line': 0, 'character': 2}}, 'text': '\n'}, # noqa
+ {'rangeLength': 0, 'range': {'start': {'line': 1, 'character': 0}, 'end': {'line': 1, 'character': 0}}, 'text': '🙂'}, # noqa
+ # Note that this is character offset (2) is correct (UTF-16).
+ {'rangeLength': 0, 'range': {'start': {'line': 1, 'character': 2}, 'end': {'line': 1, 'character': 2}}, 'text': '\n'}, # noqa
+ {'rangeLength': 0, 'range': {'start': {'line': 2, 'character': 0}, 'end': {'line': 2, 'character': 0}}, 'text': 'D'}], # noqa
+ 'textDocument': {
+ 'version': self.view.change_count(),
+ 'uri': filename_to_uri(TEST_FILE_PATH)
+ }
+ })
+
def test_sends_save_with_purge(self) -> 'Generator':
assert self.view
self.view.settings().set("lsp_format_on_save", False)
From a4c038c3e1b8673c317c17631b914278fa03d915 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 17:42:02 +0200
Subject: [PATCH 17/20] narrow
---
tests/test_single_document.py | 47 -----------------------------------
1 file changed, 47 deletions(-)
diff --git a/tests/test_single_document.py b/tests/test_single_document.py
index 4ac3b4255..50403aaa9 100644
--- a/tests/test_single_document.py
+++ b/tests/test_single_document.py
@@ -371,50 +371,3 @@ def test_progress(self) -> 'Generator':
self.assertEqual(result, {"general": "kenobi"})
-class WillSaveWaitUntilTestCase(TextDocumentTestCase):
-
- @classmethod
- def get_test_server_capabilities(cls) -> dict:
- capabilities = deepcopy(super().get_test_server_capabilities())
- capabilities['capabilities']['textDocumentSync']['willSaveWaitUntil'] = True
- return capabilities
-
- def test_will_save_wait_until(self) -> 'Generator':
- assert self.view
- self.insert_characters("A")
- yield from self.await_message("textDocument/didChange")
- self.set_response('textDocument/willSaveWaitUntil', [{
- 'newText': "BBB",
- 'range': {
- 'start': {'line': 0, 'character': 0},
- 'end': {'line': 0, 'character': 1}
- }
- }])
- self.view.settings().set("lsp_format_on_save", False)
- self.view.run_command("lsp_save", {'async': True})
- yield from self.await_message("textDocument/willSaveWaitUntil")
- 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()
-
-
-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", {
- "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 a20388d7f5d939f19bdb597d2dcb7501e3e6d96b 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 17:43:20 +0200
Subject: [PATCH 18/20] disable lint github action
---
.github/workflows/main.yml | 15 ---------------
1 file changed, 15 deletions(-)
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 07a2d9641..41695afc8 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -30,18 +30,3 @@ jobs:
- uses: SublimeText/UnitTesting/actions/run-tests@v1
with:
coverage: true
-
- Lint:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-python@v5
- with:
- python-version: '3.8'
- - run: sudo apt update
- - run: sudo apt install --no-install-recommends -y x11-xserver-utils
- - run: pip3 install mypy==1.7.1 flake8==5.0.4 pyright==1.1.339 --user
- - run: echo "$HOME/.local/bin" >> $GITHUB_PATH
- - run: mypy stubs
- - run: flake8 plugin tests
- - run: pyright plugin
From 68117731b5f1b8861b37a5038e6e8a67e6631396 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 17:47:26 +0200
Subject: [PATCH 19/20] narrow
---
tests/test_single_document.py | 33 ---------------------------------
1 file changed, 33 deletions(-)
diff --git a/tests/test_single_document.py b/tests/test_single_document.py
index 50403aaa9..59ef9c53d 100644
--- a/tests/test_single_document.py
+++ b/tests/test_single_document.py
@@ -109,15 +109,6 @@ 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)
@@ -266,30 +257,6 @@ def condition() -> bool:
first = self.view.sel()[0].begin()
self.assertEqual(self.view.substr(sublime.Region(first, first + 1)), "F")
- def test_definition(self) -> 'Generator':
- yield from self.__run_goto_test(GOTO_RESPONSE, 'definition', 'definition')
-
- def test_definition_location_link(self) -> 'Generator':
- yield from self.__run_goto_test(GOTO_RESPONSE_LOCATION_LINK, 'definition', 'definition')
-
- def test_type_definition(self) -> 'Generator':
- yield from self.__run_goto_test(GOTO_RESPONSE, 'typeDefinition', 'type_definition')
-
- def test_type_definition_location_link(self) -> 'Generator':
- yield from self.__run_goto_test(GOTO_RESPONSE_LOCATION_LINK, 'typeDefinition', 'type_definition')
-
- def test_declaration(self) -> 'Generator':
- yield from self.__run_goto_test(GOTO_RESPONSE, 'declaration', 'declaration')
-
- def test_declaration_location_link(self) -> 'Generator':
- yield from self.__run_goto_test(GOTO_RESPONSE_LOCATION_LINK, 'declaration', 'declaration')
-
- def test_implementation(self) -> 'Generator':
- yield from self.__run_goto_test(GOTO_RESPONSE, 'implementation', 'implementation')
-
- def test_implementation_location_link(self) -> 'Generator':
- yield from self.__run_goto_test(GOTO_RESPONSE_LOCATION_LINK, 'implementation', 'implementation')
-
def test_expand_selection(self) -> 'Generator':
self.insert_characters("abcba\nabcba\nabcba\n")
self.view.run_command("lsp_selection_set", {"regions": [(2, 2)]})
From e0edb0238519bfc6e9c328e6cb4270f0b5a5b2e9 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 17:50:00 +0200
Subject: [PATCH 20/20] try something
---
tests/test_single_document.py | 51 ++++++++++++++++++-----------------
1 file changed, 26 insertions(+), 25 deletions(-)
diff --git a/tests/test_single_document.py b/tests/test_single_document.py
index 59ef9c53d..c25d2896d 100644
--- a/tests/test_single_document.py
+++ b/tests/test_single_document.py
@@ -51,6 +51,32 @@
0123456789
'''
+class SingleDocumentTestCase2(TextDocumentTestCase):
+ def test_did_change(self) -> 'Generator':
+ assert self.view
+ self.maxDiff = None
+ self.insert_characters("A")
+ yield from self.await_message("textDocument/didChange")
+ # multiple changes are batched into one didChange notification
+ self.insert_characters("B\n")
+ self.insert_characters("🙂\n")
+ self.insert_characters("D")
+ promise = YieldPromise()
+ yield from self.await_message("textDocument/didChange", promise)
+ self.assertEqual(promise.result(), {
+ 'contentChanges': [
+ {'rangeLength': 0, 'range': {'start': {'line': 0, 'character': 1}, 'end': {'line': 0, 'character': 1}}, 'text': 'B'}, # noqa
+ {'rangeLength': 0, 'range': {'start': {'line': 0, 'character': 2}, 'end': {'line': 0, 'character': 2}}, 'text': '\n'}, # noqa
+ {'rangeLength': 0, 'range': {'start': {'line': 1, 'character': 0}, 'end': {'line': 1, 'character': 0}}, 'text': '🙂'}, # noqa
+ # Note that this is character offset (2) is correct (UTF-16).
+ {'rangeLength': 0, 'range': {'start': {'line': 1, 'character': 2}, 'end': {'line': 1, 'character': 2}}, 'text': '\n'}, # noqa
+ {'rangeLength': 0, 'range': {'start': {'line': 2, 'character': 0}, 'end': {'line': 2, 'character': 0}}, 'text': 'D'}], # noqa
+ 'textDocument': {
+ 'version': self.view.change_count(),
+ 'uri': filename_to_uri(TEST_FILE_PATH)
+ }
+ })
+
class SingleDocumentTestCase(TextDocumentTestCase):
@@ -84,31 +110,6 @@ def test_did_close(self) -> 'Generator':
self.view.close()
yield from self.await_message("textDocument/didClose")
- def test_did_change(self) -> 'Generator':
- assert self.view
- self.maxDiff = None
- self.insert_characters("A")
- yield from self.await_message("textDocument/didChange")
- # multiple changes are batched into one didChange notification
- self.insert_characters("B\n")
- self.insert_characters("🙂\n")
- self.insert_characters("D")
- promise = YieldPromise()
- yield from self.await_message("textDocument/didChange", promise)
- self.assertEqual(promise.result(), {
- 'contentChanges': [
- {'rangeLength': 0, 'range': {'start': {'line': 0, 'character': 1}, 'end': {'line': 0, 'character': 1}}, 'text': 'B'}, # noqa
- {'rangeLength': 0, 'range': {'start': {'line': 0, 'character': 2}, 'end': {'line': 0, 'character': 2}}, 'text': '\n'}, # noqa
- {'rangeLength': 0, 'range': {'start': {'line': 1, 'character': 0}, 'end': {'line': 1, 'character': 0}}, 'text': '🙂'}, # noqa
- # Note that this is character offset (2) is correct (UTF-16).
- {'rangeLength': 0, 'range': {'start': {'line': 1, 'character': 2}, 'end': {'line': 1, 'character': 2}}, 'text': '\n'}, # noqa
- {'rangeLength': 0, 'range': {'start': {'line': 2, 'character': 0}, 'end': {'line': 2, 'character': 0}}, 'text': 'D'}], # noqa
- 'textDocument': {
- 'version': self.view.change_count(),
- 'uri': filename_to_uri(TEST_FILE_PATH)
- }
- })
-
def test_formats_on_save(self) -> 'Generator':
assert self.view
self.view.settings().set("lsp_format_on_save", True)