diff --git a/tests/probe.txt b/tests/probe.txt deleted file mode 100644 index 460ddf5eb..000000000 --- a/tests/probe.txt +++ /dev/null @@ -1,3 +0,0 @@ -Content-Length: 58 - -{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}} 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_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 deleted file mode 100644 index b3281e3ee..000000000 --- a/tests/test_file_watcher.py +++ /dev/null @@ -1,261 +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 - - -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. - _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 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): - - 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) diff --git a/tests/test_message_request_handler.py b/tests/test_message_request_handler.py deleted file mode 100644 index 1660c2527..000000000 --- a/tests/test_message_request_handler.py +++ /dev/null @@ -1,22 +0,0 @@ -import unittest -from test_mocks import MockSession -from LSP.plugin.core.message_request_handler import MessageRequestHandler -import sublime - - -class MessageRequestHandlerTest(unittest.TestCase): - def test_show_popup(self): - window = sublime.active_window() - view = window.active_view() - session = MockSession() - params = { - 'type': 1, - 'message': 'hello', - 'actions': [ - {'title': "abc"}, - {'title': "def"} - ] - } - handler = MessageRequestHandler(view, session, "1", params, 'lsp server') - handler.show() - self.assertTrue(view.is_popup_visible()) diff --git a/tests/test_protocol.py b/tests/test_protocol.py deleted file mode 100644 index f2860e6b9..000000000 --- a/tests/test_protocol.py +++ /dev/null @@ -1,65 +0,0 @@ -from LSP.plugin.core.protocol import Point, Position, Range, Request, Notification -from LSP.plugin.core.transports import JsonRpcProcessor -import unittest - - -LSP_START_POSITION = {'line': 10, 'character': 4} # type: Position -LSP_END_POSITION = {'line': 11, 'character': 3} # type: Position -LSP_RANGE = {'start': LSP_START_POSITION, 'end': LSP_END_POSITION} # type: Range - - -class PointTests(unittest.TestCase): - - def test_lsp_conversion(self) -> None: - point = Point.from_lsp(LSP_START_POSITION) - self.assertEqual(point.row, 10) - self.assertEqual(point.col, 4) - lsp_point = point.to_lsp() - self.assertEqual(lsp_point['line'], 10) - self.assertEqual(lsp_point['character'], 4) - - -class EncodingTests(unittest.TestCase): - def test_encode(self) -> None: - encoded = JsonRpcProcessor._encode({"text": "😃"}) - self.assertEqual(encoded, b'{"text":"\xF0\x9F\x98\x83"}') - decoded = JsonRpcProcessor._decode(encoded) - self.assertEqual(decoded, {"text": "😃"}) - - -class RequestTests(unittest.TestCase): - - def test_initialize(self) -> None: - req = Request.initialize({"param": 1}) - payload = req.to_payload(1) - self.assertEqual(payload["jsonrpc"], "2.0") - self.assertEqual(payload["id"], 1) - self.assertEqual(payload["method"], "initialize") - self.assertEqual(payload["params"], {"param": 1}) - - def test_shutdown(self) -> None: - req = Request.shutdown() - payload = req.to_payload(1) - self.assertEqual(payload["jsonrpc"], "2.0") - self.assertEqual(payload["id"], 1) - self.assertEqual(payload["method"], "shutdown") - self.assertNotIn('params', payload) - - -class NotificationTests(unittest.TestCase): - - def test_initialized(self) -> None: - notification = Notification.initialized() - payload = notification.to_payload() - self.assertEqual(payload["jsonrpc"], "2.0") - self.assertNotIn("id", payload) - self.assertEqual(payload["method"], "initialized") - self.assertEqual(payload["params"], dict()) - - def test_exit(self) -> None: - notification = Notification.exit() - payload = notification.to_payload() - self.assertEqual(payload["jsonrpc"], "2.0") - self.assertNotIn("id", payload) - self.assertEqual(payload["method"], "exit") - self.assertNotIn('params', payload) diff --git a/tests/test_rename_panel.py b/tests/test_rename_panel.py deleted file mode 100644 index a9fd78a7a..000000000 --- a/tests/test_rename_panel.py +++ /dev/null @@ -1,50 +0,0 @@ -from LSP.plugin.rename import utf16_to_code_points -import unittest - - -class LspRenamePanelTests(unittest.TestCase): - - def test_utf16_ascii(self): - s = 'abc' - self.assertEqual(utf16_to_code_points(s, 0), 0) - self.assertEqual(utf16_to_code_points(s, 1), 1) - self.assertEqual(utf16_to_code_points(s, 2), 2) - self.assertEqual(utf16_to_code_points(s, 3), 3) # EOL after last character should count as its own code point - self.assertEqual(utf16_to_code_points(s, 1337), 3) # clamp to EOL - - def test_utf16_deseret_letter(self): - # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocuments - s = 'a𐐀b' - self.assertEqual(len(s), 3) - self.assertEqual(utf16_to_code_points(s, 0), 0) - self.assertEqual(utf16_to_code_points(s, 1), 1) - self.assertEqual(utf16_to_code_points(s, 2), 1) # 𐐀 needs 2 UTF-16 code units, so this is still at code point 1 - self.assertEqual(utf16_to_code_points(s, 3), 2) - self.assertEqual(utf16_to_code_points(s, 4), 3) - self.assertEqual(utf16_to_code_points(s, 1337), 3) - - def test_utf16_emoji(self): - s = 'a😀x' - self.assertEqual(len(s), 3) - self.assertEqual(utf16_to_code_points(s, 0), 0) - self.assertEqual(utf16_to_code_points(s, 1), 1) - self.assertEqual(utf16_to_code_points(s, 2), 1) - self.assertEqual(utf16_to_code_points(s, 3), 2) - self.assertEqual(utf16_to_code_points(s, 4), 3) - self.assertEqual(utf16_to_code_points(s, 1337), 3) - - def test_utf16_emoji_zwj_sequence(self): - # https://unicode.org/emoji/charts/emoji-zwj-sequences.html - s = 'a😵‍💫x' - self.assertEqual(len(s), 5) - self.assertEqual(s, 'a\U0001f635\u200d\U0001f4abx') - # 😵‍💫 consists of 5 UTF-16 code units and Python treats it as 3 characters - self.assertEqual(utf16_to_code_points(s, 0), 0) # a - self.assertEqual(utf16_to_code_points(s, 1), 1) # \U0001f635 - self.assertEqual(utf16_to_code_points(s, 2), 1) # \U0001f635 - self.assertEqual(utf16_to_code_points(s, 3), 2) # \u200d (zero width joiner) - self.assertEqual(utf16_to_code_points(s, 4), 3) # \U0001f4ab - self.assertEqual(utf16_to_code_points(s, 5), 3) # \U0001f4ab - self.assertEqual(utf16_to_code_points(s, 6), 4) # x - self.assertEqual(utf16_to_code_points(s, 7), 5) # after x - self.assertEqual(utf16_to_code_points(s, 1337), 5) diff --git a/tests/test_server_notifications.py b/tests/test_server_notifications.py deleted file mode 100644 index e49d655c6..000000000 --- a/tests/test_server_notifications.py +++ /dev/null @@ -1,81 +0,0 @@ -from LSP.plugin.core.protocol import DiagnosticSeverity -from LSP.plugin.core.protocol import DiagnosticTag -from LSP.plugin.core.protocol import PublishDiagnosticsParams -from LSP.plugin.core.typing import Generator -from LSP.plugin.core.url import filename_to_uri -from setup import TextDocumentTestCase -import sublime - - -class ServerNotifications(TextDocumentTestCase): - - def test_publish_diagnostics(self) -> Generator: - self.insert_characters("a b c\n") - params = { - 'uri': filename_to_uri(self.view.file_name() or ''), - 'diagnostics': [ - { - 'message': "foo", - 'severity': DiagnosticSeverity.Error, - 'source': 'qux', - 'range': {'end': {'character': 1, 'line': 0}, 'start': {'character': 0, 'line': 0}} - }, - { - 'message': 'bar', - 'severity': DiagnosticSeverity.Warning, - 'source': 'qux', - 'range': {'end': {'character': 3, 'line': 0}, 'start': {'character': 2, 'line': 0}} - }, - { - 'message': "baz", - 'severity': DiagnosticSeverity.Information, - 'source': 'qux', - 'range': {'end': {'character': 5, 'line': 0}, 'start': {'character': 4, 'line': 0}}, - 'tags': [DiagnosticTag.Unnecessary] - } - ] - } # type: PublishDiagnosticsParams - yield from self.await_client_notification("textDocument/publishDiagnostics", params) - errors_icon_regions = self.view.get_regions("lspTESTds1_icon") - errors_underline_regions = self.view.get_regions("lspTESTds1_underline") - warnings_icon_regions = self.view.get_regions("lspTESTds2_icon") - warnings_underline_regions = self.view.get_regions("lspTESTds2_underline") - info_icon_regions = self.view.get_regions("lspTESTds3_icon") - info_underline_regions = self.view.get_regions("lspTESTds3_underline") - yield lambda: len(errors_icon_regions) == len(errors_underline_regions) == 1 - yield lambda: len(warnings_icon_regions) == len(warnings_underline_regions) == 1 - yield lambda: len(info_icon_regions) == len(info_underline_regions) == 1 - yield lambda: len(self.view.get_regions("lspTESTds3_tags")) == 0 - self.assertEqual(errors_underline_regions[0], sublime.Region(0, 1)) - self.assertEqual(warnings_underline_regions[0], sublime.Region(2, 3)) - self.assertEqual(info_underline_regions[0], sublime.Region(4, 5)) - - # Testing whether the cursor position moves along with lsp_next_diagnostic - - self.view.window().run_command("lsp_next_diagnostic") - self.assertEqual(self.view.sel()[0].a, self.view.sel()[0].b) - self.assertEqual(self.view.sel()[0].b, 0) - - self.view.window().run_command("lsp_next_diagnostic") - self.assertEqual(self.view.sel()[0].a, self.view.sel()[0].b) - self.assertEqual(self.view.sel()[0].b, 2) - - self.view.window().run_command("lsp_next_diagnostic") - self.assertEqual(self.view.sel()[0].a, self.view.sel()[0].b) - self.assertEqual(self.view.sel()[0].b, 4) - - # lsp_prev_diagnostic should work as well - - self.view.window().run_command("lsp_prev_diagnostic") - self.assertEqual(self.view.sel()[0].a, self.view.sel()[0].b) - self.assertEqual(self.view.sel()[0].b, 2) - - self.view.window().run_command("lsp_prev_diagnostic") - self.assertEqual(self.view.sel()[0].a, self.view.sel()[0].b) - self.assertEqual(self.view.sel()[0].b, 0) - - # Testing to wrap around if there are no more diagnostics in the direction - - self.view.window().run_command("lsp_prev_diagnostic") - self.assertEqual(self.view.sel()[0].a, self.view.sel()[0].b) - self.assertEqual(self.view.sel()[0].b, 4) diff --git a/tests/test_server_panel_circular.py b/tests/test_server_panel_circular.py deleted file mode 100644 index d4ca28415..000000000 --- a/tests/test_server_panel_circular.py +++ /dev/null @@ -1,41 +0,0 @@ -from LSP.plugin.core.panels import MAX_LOG_LINES_LIMIT_ON -from LSP.plugin.core.panels import PanelName -from LSP.plugin.core.registry import windows -from unittesting import DeferrableTestCase -import sublime - - -class LspServerPanelTests(DeferrableTestCase): - - def setUp(self): - super().setUp() - self.window = sublime.active_window() - self.assertIsNotNone(self.window) - self.wm = windows.lookup(self.window) - self.assertIsNotNone(self.wm) - if not self.wm: - return - self.view = self.window.active_view() - self.panel = self.wm.panel_manager.get_panel(PanelName.Log) - self.assertIsNotNone(self.panel) - if not self.panel: - return - self.panel.run_command("lsp_clear_panel") - - def assert_total_lines_equal(self, expected_total_lines): - actual_total_lines = len(self.panel.split_by_newlines(sublime.Region(0, self.panel.size()))) - self.assertEqual(actual_total_lines, expected_total_lines) - - def update_panel(self, msg: str) -> None: - self.wm.log_server_message("test", msg) - - def test_server_panel_circular_behavior(self): - n = MAX_LOG_LINES_LIMIT_ON - for i in range(0, n + 1): - self.update_panel(str(i)) - self.update_panel("overflow") - self.update_panel("overflow") - self.update_panel("one\ntwo\nthree") - # The panel only updates when visible but we don't want to test that as - # it would hide the unittesting panel. - self.assert_total_lines_equal(1) diff --git a/tests/test_server_requests.py b/tests/test_server_requests.py deleted file mode 100644 index 2ac9d5814..000000000 --- a/tests/test_server_requests.py +++ /dev/null @@ -1,224 +0,0 @@ -from LSP.plugin.core.protocol import ErrorCodes -from LSP.plugin.core.protocol import TextDocumentSyncKind -from LSP.plugin.core.sessions import SessionBufferProtocol -from LSP.plugin.core.types import ClientConfig -from LSP.plugin.core.typing import Any, Dict, Generator, Optional, List -from LSP.plugin.core.url import filename_to_uri -from setup import TextDocumentTestCase -import os -import sublime -import tempfile - - -def get_auto_complete_trigger(sb: SessionBufferProtocol) -> Optional[List[Dict[str, str]]]: - for sv in sb.session_views: - triggers = sv.view.settings().get("auto_complete_triggers") - for trigger in triggers: - if "server" in trigger and "registration_id" in trigger: - return trigger - return None - - -def verify(testcase: TextDocumentTestCase, method: str, input_params: Any, expected_output_params: Any) -> Generator: - promise = testcase.make_server_do_fake_request(method, input_params) - yield from testcase.await_promise(promise) - testcase.assertEqual(promise.result(), expected_output_params) - - -class ServerRequests(TextDocumentTestCase): - - def test_unknown_method(self) -> Generator: - yield from verify(self, "foobar/qux", {}, {"code": ErrorCodes.MethodNotFound, "message": "foobar/qux"}) - - def test_m_workspace_workspaceFolders(self) -> Generator: - expected_output = [{"name": os.path.basename(f), "uri": filename_to_uri(f)} - for f in sublime.active_window().folders()] - self.maxDiff = None - yield from verify(self, "workspace/workspaceFolders", {}, expected_output) - - def test_m_workspace_configuration(self) -> Generator: - self.session.config.settings.set("foo.bar", "$hello") - self.session.config.settings.set("foo.baz", "$world") - self.session.config.settings.set("foo.a", 1) - self.session.config.settings.set("foo.b", None) - self.session.config.settings.set("foo.c", ["asdf ${hello} ${world}"]) - - class TempPlugin: - - @classmethod - def additional_variables(cls) -> Optional[Dict[str, str]]: - return {"hello": "X", "world": "Y"} - - self.session._plugin_class = TempPlugin # type: ignore - method = "workspace/configuration" - params = {"items": [{"section": "foo"}]} - expected_output = [{"bar": "X", "baz": "Y", "a": 1, "b": None, "c": ["asdf X Y"]}] - yield from verify(self, method, params, expected_output) - self.session.config.settings.clear() - - def test_m_workspace_applyEdit(self) -> Generator: - old_change_count = self.insert_characters("hello\nworld\n") - edit = { - "newText": "there", - "range": {"start": {"line": 1, "character": 0}, "end": {"line": 1, "character": 5}}} - params = {"edit": {"changes": {filename_to_uri(self.view.file_name()): [edit]}}} - yield from verify(self, "workspace/applyEdit", params, {"applied": True}) - yield lambda: self.view.change_count() > old_change_count - self.assertEqual(self.view.substr(sublime.Region(0, self.view.size())), "hello\nthere\n") - - def test_m_workspace_applyEdit_with_nontrivial_promises(self) -> Generator: - with tempfile.TemporaryDirectory() as dirpath: - initial_text = ["a b", "c d"] - file_paths = [] - for i in range(0, 2): - file_paths.append(os.path.join(dirpath, "file{}.txt".format(i))) - with open(file_paths[-1], "w") as fp: - fp.write(initial_text[i]) - yield from verify( - self, - "workspace/applyEdit", - { - "edit": { - "changes": { - filename_to_uri(file_paths[0]): - [ - { - "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 1}}, - "newText": "hello" - }, - { - "range": {"start": {"line": 0, "character": 2}, "end": {"line": 0, "character": 3}}, - "newText": "there" - } - ], - filename_to_uri(file_paths[1]): - [ - { - "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 1}}, - "newText": "general" - }, - { - "range": {"start": {"line": 0, "character": 2}, "end": {"line": 0, "character": 3}}, - "newText": "kenobi" - } - ] - } - } - }, - {"applied": True} - ) - # Changes should have been applied - expected = ["hello there", "general kenobi"] - for i in range(0, 2): - view = self.view.window().find_open_file(file_paths[i]) - self.assertTrue(view) - view.set_scratch(True) - self.assertTrue(view.is_valid()) - self.assertFalse(view.is_loading()) - self.assertEqual(view.substr(sublime.Region(0, view.size())), expected[i]) - view.close() - - def test_m_client_registerCapability(self) -> Generator: - yield from verify( - self, - "client/registerCapability", - { - "registrations": - [ - {"method": "foo/bar", "id": "hello"}, - {"method": "bar/baz", "id": "world", "registerOptions": {"frobnicatable": True}}, - {"method": "workspace/didChangeWorkspaceFolders", "id": "asdf"}, - {"method": "textDocument/didOpen", "id": "1"}, - {"method": "textDocument/willSaveWaitUntil", "id": "2", - "registerOptions": {"documentSelector": [{"language": "plaintext"}]}}, - {"method": "textDocument/didChange", "id": "adsf", - "registerOptions": {"syncKind": TextDocumentSyncKind.Full, "documentSelector": [ - {"language": "plaintext"} - ]}}, - {"method": "textDocument/completion", "id": "myCompletionRegistrationId", - "registerOptions": {"triggerCharacters": ["!", "@", "#"], "documentSelector": [ - {"language": "plaintext"} - ]}} - ] - }, - None) - self.assertIn("barProvider", self.session.capabilities) - self.assertEqual(self.session.capabilities.get("barProvider.id"), "hello") - self.assertIn("bazProvider", self.session.capabilities) - self.assertEqual(self.session.capabilities.get("bazProvider"), {"id": "world", "frobnicatable": True}) - self.assertEqual(self.session.capabilities.get("workspace.workspaceFolders.changeNotifications"), "asdf") - self.assertEqual(self.session.capabilities.get("textDocumentSync.didOpen"), {"id": "1"}) - self.assertFalse(self.session.capabilities.get("textDocumentSync.didClose")) - - # willSaveWaitUntil is *only* registered on the buffer - self.assertFalse(self.session.capabilities.get("textDocumentSync.willSaveWaitUntil")) - sb = next(self.session.session_buffers_async()) - self.assertEqual(sb.capabilities.text_sync_kind(), TextDocumentSyncKind.Full) - self.assertEqual(sb.capabilities.get("textDocumentSync.willSaveWaitUntil"), {"id": "2"}) - self.assertEqual(self.session.capabilities.text_sync_kind(), TextDocumentSyncKind.Incremental) - - # Check that textDocument/completion was registered onto the SessionBuffer, and check that the trigger - # characters for each view were updated - self.assertEqual(sb.capabilities.get("completionProvider.id"), "myCompletionRegistrationId") - self.assertEqual(sb.capabilities.get("completionProvider.triggerCharacters"), ["!", "@", "#"]) - trigger = get_auto_complete_trigger(sb) - self.assertTrue(trigger) - self.assertEqual(trigger.get("characters"), "!@#") - - def test_m_client_unregisterCapability(self) -> Generator: - yield from verify( - self, - "client/registerCapability", - {"registrations": [{"method": "foo/bar", "id": "hello"}]}, - None) - self.assertIn("barProvider", self.session.capabilities) - yield from verify( - self, - "client/unregisterCapability", - {"unregisterations": [{"method": "foo/bar", "id": "hello"}]}, - None) - self.assertNotIn("barProvider", self.session.capabilities) - - -class ServerRequestsWithAutoCompleteSelector(TextDocumentTestCase): - - @classmethod - def get_stdio_test_config(cls) -> ClientConfig: - return ClientConfig.from_config( - super().get_stdio_test_config(), - { - "auto_complete_selector": "punctuation.section", - "disabled_capabilities": { - "completionProvider": { - "triggerCharacters": True - } - } - } - ) - - def test_m_client_registerCapability(self) -> Generator: - yield from verify( - self, - "client/registerCapability", - { - "registrations": - [ - # Note that the triggerCharacters are disabled in the configuration. - {"method": "textDocument/completion", "id": "anotherCompletionRegistrationId", - "registerOptions": {"triggerCharacters": ["!", "@", "#"], "documentSelector": [ - {"language": "plaintext"} - ]}} - ] - }, - None) - sb = next(self.session.session_buffers_async()) - # Check that textDocument/completion was registered onto the SessionBuffer - self.assertEqual(sb.capabilities.get("completionProvider.id"), "anotherCompletionRegistrationId") - # Trigger characters should not have been registered - self.assertFalse(sb.capabilities.get("completionProvider.triggerCharacters")) - trigger = get_auto_complete_trigger(sb) - self.assertTrue(trigger) - # No triggers should have been assigned - self.assertFalse(trigger.get("characters")) - # The selector should have been set - self.assertEqual(trigger.get("selector"), "punctuation.section") diff --git a/tests/test_session.py b/tests/test_session.py deleted file mode 100644 index b6a82d61e..000000000 --- a/tests/test_session.py +++ /dev/null @@ -1,253 +0,0 @@ -from LSP.plugin.core.collections import DottedDict -from LSP.plugin.core.protocol import Diagnostic -from LSP.plugin.core.protocol import DocumentUri -from LSP.plugin.core.protocol import Error -from LSP.plugin.core.protocol import TextDocumentSyncKind -from LSP.plugin.core.sessions import get_initialize_params -from LSP.plugin.core.sessions import Logger -from LSP.plugin.core.sessions import Manager -from LSP.plugin.core.sessions import Session -from LSP.plugin.core.types import ClientConfig -from LSP.plugin.core.typing import Any, Optional, Generator, List, Dict -from LSP.plugin.core.workspace import WorkspaceFolder -from test_mocks import TEST_CONFIG -import sublime -import unittest -import unittest.mock -import weakref - - -class MockManager(Manager): - - def __init__(self, window: sublime.Window) -> None: - self._window = window - - def window(self) -> sublime.Window: - return self._window - - def sessions(self, view: sublime.View, capability: Optional[str] = None) -> Generator[Session, None, None]: - pass - - def get_project_path(self, file_name: str) -> Optional[str]: - return None - - def should_ignore_diagnostics(self, uri: DocumentUri, configuration: ClientConfig) -> Optional[str]: - return None - - def start_async(self, configuration: ClientConfig, initiating_view: sublime.View) -> None: - pass - - def on_post_exit_async(self, session: Session, exit_code: int, exception: Optional[Exception]) -> None: - pass - - def on_diagnostics_updated(self) -> None: - pass - - -class MockLogger(Logger): - - def stderr_message(self, message: str) -> None: - pass - - def outgoing_response(self, request_id: Any, params: Any) -> None: - pass - - def outgoing_error_response(self, request_id: Any, error: Error) -> None: - pass - - def outgoing_request(self, request_id: int, method: str, params: Any, blocking: bool) -> None: - pass - - def outgoing_notification(self, method: str, params: Any) -> None: - pass - - def incoming_response(self, request_id: Optional[int], params: Any, is_error: bool, blocking: bool) -> None: - pass - - def incoming_request(self, request_id: Any, method: str, params: Any) -> None: - pass - - def incoming_notification(self, method: str, params: Any, unhandled: bool) -> None: - pass - - -class MockSessionBuffer: - - def __init__(self, session: Session, mock_uri: str, mock_language_id: str) -> None: - self.session = session - self.session_views = weakref.WeakSet() - self.mock_uri = mock_uri - self.mock_language_id = mock_language_id - - def get_uri(self) -> Optional[DocumentUri]: - return self.mock_uri - - def get_language_id(self) -> Optional[str]: - return self.mock_language_id - - def register_capability_async( - self, - registration_id: str, - capability_path: str, - registration_path: str, - options: Dict[str, Any] - ) -> None: - pass - - def unregister_capability_async( - self, - registration_id: str, - capability_path: str, - registration_path: str - ) -> None: - pass - - def on_diagnostics_async(self, raw_diagnostics: List[Diagnostic], version: Optional[int]) -> None: - pass - - -class SessionTest(unittest.TestCase): - - def test_experimental_capabilities(self) -> None: - wf = WorkspaceFolder.from_path("/foo/bar/baz") - params = get_initialize_params( - {}, - [wf], - ClientConfig(name="test", command=[""], selector="", tcp_port=None, experimental_capabilities=None)) - self.assertNotIn("experimental", params["capabilities"]) - - params = get_initialize_params( - {}, - [wf], - ClientConfig(name="test", command=[""], selector="", tcp_port=None, experimental_capabilities={})) - self.assertIn("experimental", params["capabilities"]) - self.assertEqual(params["capabilities"]["experimental"], {}) - - experimental_capabilities = { - "foo": 1, - "bar": True, - "baz": "abc" - } - config = ClientConfig( - name="test", - command=[""], - selector="", - tcp_port=None, - experimental_capabilities=experimental_capabilities - ) - params = get_initialize_params({}, [wf], config) - self.assertIn("experimental", params["capabilities"]) - self.assertEqual(params["capabilities"]["experimental"], experimental_capabilities) - - def test_initialize_params(self) -> None: - wf = WorkspaceFolder.from_path("/foo/bar/baz") - params = get_initialize_params( - {}, [wf], ClientConfig(name="test", command=[""], selector="", tcp_port=None, init_options=DottedDict())) - self.assertIn("initializationOptions", params) - self.assertEqual(params["initializationOptions"], {}) - params = get_initialize_params( - {}, [wf], ClientConfig( - name="test", command=[""], selector="", tcp_port=None, init_options=DottedDict({"foo": "bar"}))) - self.assertIn("initializationOptions", params) - self.assertEqual(params["initializationOptions"], {"foo": "bar"}) - - def test_document_sync_capabilities(self) -> None: - manager = MockManager(sublime.active_window()) - session = Session(manager=manager, logger=MockLogger(), workspace_folders=[], config=TEST_CONFIG, - plugin_class=None) - session.capabilities.assign({ - 'textDocumentSync': { - "openClose": True, - "change": TextDocumentSyncKind.Full, - "save": True}}) # A boolean with value true means "send didSave" - self.assertTrue(session.should_notify_did_open()) - self.assertTrue(session.should_notify_did_close()) - self.assertEqual(session.text_sync_kind(), TextDocumentSyncKind.Full) - self.assertFalse(session.should_notify_will_save()) - self.assertEqual(session.should_notify_did_save(), (True, False)) - - session.capabilities.assign({ - 'textDocumentSync': { - "didOpen": {}, - "didClose": {}, - "change": TextDocumentSyncKind.Full, - "save": True}}) # A boolean with value true means "send didSave" - self.assertTrue(session.should_notify_did_open()) - self.assertTrue(session.should_notify_did_close()) - self.assertEqual(session.text_sync_kind(), TextDocumentSyncKind.Full) - self.assertFalse(session.should_notify_will_save()) - self.assertEqual(session.should_notify_did_save(), (True, False)) - - session.capabilities.assign({ - 'textDocumentSync': { - "openClose": False, - "change": TextDocumentSyncKind.None_, - "save": {}, # An empty dict means "send didSave" - "willSave": True, - "willSaveWaitUntil": False}}) - self.assertFalse(session.should_notify_did_open()) - self.assertFalse(session.should_notify_did_close()) - self.assertEqual(session.text_sync_kind(), TextDocumentSyncKind.None_) - self.assertTrue(session.should_notify_will_save()) - self.assertEqual(session.should_notify_did_save(), (True, False)) - # Nested capabilities. - self.assertTrue(session.has_capability('textDocumentSync.change')) - self.assertTrue(session.has_capability('textDocumentSync.save')) - self.assertTrue(session.has_capability('textDocumentSync.willSave')) - self.assertFalse(session.has_capability('textDocumentSync.willSaveUntil')) - self.assertFalse(session.has_capability('textDocumentSync.aintthere')) - - session.capabilities.assign({ - 'textDocumentSync': { - "openClose": False, - "change": TextDocumentSyncKind.Incremental, - "save": {"includeText": True}, - "willSave": False, - "willSaveWaitUntil": True}}) - self.assertFalse(session.should_notify_did_open()) - self.assertFalse(session.should_notify_did_close()) - self.assertEqual(session.text_sync_kind(), TextDocumentSyncKind.Incremental) - self.assertFalse(session.should_notify_will_save()) - self.assertEqual(session.should_notify_did_save(), (True, True)) - - session.capabilities.assign({'textDocumentSync': TextDocumentSyncKind.Incremental}) - self.assertTrue(session.should_notify_did_open()) - self.assertTrue(session.should_notify_did_close()) - self.assertEqual(session.text_sync_kind(), TextDocumentSyncKind.Incremental) - self.assertFalse(session.should_notify_will_save()) # old-style text sync will never send willSave - # old-style text sync will always send didSave - self.assertEqual(session.should_notify_did_save(), (True, False)) - - session.capabilities.assign({'textDocumentSync': TextDocumentSyncKind.None_}) - self.assertTrue(session.should_notify_did_open()) # old-style text sync will always send didOpen - self.assertTrue(session.should_notify_did_close()) # old-style text sync will always send didClose - self.assertEqual(session.text_sync_kind(), TextDocumentSyncKind.None_) - self.assertFalse(session.should_notify_will_save()) - self.assertEqual(session.should_notify_did_save(), (True, False)) - - session.capabilities.assign({ - 'textDocumentSync': { - "openClose": True, - "save": False, - "change": TextDocumentSyncKind.Incremental}}) - self.assertTrue(session.should_notify_did_open()) - self.assertTrue(session.should_notify_did_close()) - self.assertEqual(session.text_sync_kind(), TextDocumentSyncKind.Incremental) - self.assertFalse(session.should_notify_will_save()) - self.assertEqual(session.should_notify_did_save(), (False, False)) - - def test_get_session_buffer_for_uri_with_nonfiles(self) -> None: - manager = MockManager(sublime.active_window()) - session = Session(manager=manager, logger=MockLogger(), workspace_folders=[], config=TEST_CONFIG, - plugin_class=None) - original = MockSessionBuffer(session, "some-scheme://whatever", "somelang") - session.register_session_buffer_async(original) - sb = session.get_session_buffer_for_uri_async("some-scheme://whatever") - self.assertIsNotNone(sb) - assert sb - self.assertEqual(sb.get_language_id(), "somelang") - self.assertEqual(sb.get_uri(), "some-scheme://whatever") - - def test_get_session_buffer_for_uri_with_files(self) -> None: - # todo: write windows-only test - pass diff --git a/tests/test_signature_help.py b/tests/test_signature_help.py deleted file mode 100644 index 8de8d43f6..000000000 --- a/tests/test_signature_help.py +++ /dev/null @@ -1,253 +0,0 @@ -from LSP.plugin.core.protocol import SignatureHelp -from LSP.plugin.core.signature_help import SigHelp -import sublime -import unittest - - -class SignatureHelpTest(unittest.TestCase): - - def setUp(self) -> None: - self.view = sublime.active_window().active_view() - - def test_no_signature(self) -> None: - help = SigHelp.from_lsp(None, None) - self.assertIsNone(help) - - def test_empty_signature_list(self) -> None: - help = SigHelp.from_lsp({"signatures": []}, None) - self.assertIsNone(help) - - def assert_render(self, input: SignatureHelp, regex: str) -> None: - help = SigHelp(input, None) - assert self.view - self.assertRegex(help.render(self.view), regex.replace("\n", "").replace(" ", "")) - - def test_signature(self) -> None: - self.assert_render( - { - "signatures": - [ - { - "label": "f(x)", - "documentation": "f does interesting things", - "parameters": - [ - { - "label": "x", - "documentation": "must be in the frobnicate range" - } - ] - } - ], - "activeSignature": 0, - "activeParameter": 0 - }, - r''' -
-            f\(
-            x
-            \)
-            
-

must be in the frobnicate range

-
-

f does interesting things

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

must be in the frobnicate range

-
-

f does interesting things

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

hello there

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

hello there

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

-

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

-
f\(
-            x
-            , 
-            a
-            , 
-            b
-            \)
-            
- ''' - ) - - def test_dockerfile_signature(self) -> None: - self.assert_render( - { - "signatures": - [ - { - "label": 'RUN [ "command" "parameters", ... ]', - "parameters": - [ - {'label': '['}, - {'label': '"command"'}, - {'label': '"parameters"'}, - {'label': '...'}, - {'label': ']'} - ] - } - ], - "activeSignature": 0, - "activeParameter": 2 - }, - r''' -
-            RUN 
-            \[
-             
-            "command"
-             
-            "parameters"
-            , 
-            \.\.\.
-             
-            \]
-            
- ''' - ) diff --git a/tests/test_single_document.py b/tests/test_single_document.py deleted file mode 100644 index 4d1bb10d3..000000000 --- a/tests/test_single_document.py +++ /dev/null @@ -1,400 +0,0 @@ -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: - from typing import Generator, Optional, Iterable, Tuple, List - assert Generator and Optional and Iterable and Tuple and List -except ImportError: - pass - -SELFDIR = os.path.dirname(__file__) -TEST_FILE_PATH = os.path.join(SELFDIR, 'testfile.txt') -GOTO_RESPONSE = [ - { - 'uri': filename_to_uri(TEST_FILE_PATH), - 'range': - { - 'start': - { - # Put the cursor at the capital letter "F". - 'character': 5, - 'line': 1 - }, - 'end': - { - 'character': 5, - 'line': 1 - } - } - } -] -GOTO_RESPONSE_LOCATION_LINK = [ - { - 'originSelectionRange': {'start': {'line': 0, 'character': 0}}, - 'targetUri': GOTO_RESPONSE[0]['uri'], - 'targetRange': GOTO_RESPONSE[0]['range'], - 'targetSelectionRange': GOTO_RESPONSE[0]['range'] - } -] -GOTO_CONTENT = r'''abcdefghijklmnopqrstuvwxyz -ABCDEFGHIJKLMNOPQRSTUVWXYZ -0123456789 -''' - - -class SingleDocumentTestCase(TextDocumentTestCase): - - def test_did_open(self) -> None: - # Just the existence of this method checks "initialize" -> "initialized" -> "textDocument/didOpen" - # -> "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()) - 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) - self.insert_characters("A") - self.view.run_command("lsp_save", {'async': True}) - yield from self.await_message("textDocument/didChange") - yield from self.await_message("textDocument/didSave") - yield from self.await_clear_view_and_save() - - def test_formats_on_save(self) -> 'Generator': - assert self.view - self.view.settings().set("lsp_format_on_save", True) - self.insert_characters("A") - yield from self.await_message("textDocument/didChange") - self.set_response('textDocument/formatting', [{ - 'newText': "BBB", - 'range': { - 'start': {'line': 0, 'character': 0}, - 'end': {'line': 0, 'character': 1} - } - }]) - self.view.run_command("lsp_save", {'async': True}) - yield from self.await_message("textDocument/formatting") - yield from self.await_message("textDocument/didChange") - yield from self.await_message("textDocument/didSave") - text = self.view.substr(sublime.Region(0, self.view.size())) - self.assertEquals("BBB", text) - yield from self.await_clear_view_and_save() - - def test_hover_info(self) -> 'Generator': - assert self.view - self.set_response('textDocument/hover', {"contents": "greeting"}) - self.view.run_command('insert', {"characters": "Hello Wrld"}) - self.assertFalse(self.view.is_popup_visible()) - self.view.run_command('lsp_hover', {'point': 3}) - yield lambda: self.view.is_popup_visible() - last_content = _test_contents[-1] - self.assertTrue("greeting" in last_content) - - def test_remove_line_and_then_insert_at_that_line_at_end(self) -> 'Generator': - original = ( - 'a\n' - 'b\n' - 'c' - ) - file_changes = [ - ((2, 0), (3, 0), ''), # out-of-bounds end position, but this is fine - ((3, 0), (3, 0), 'c\n') # out-of-bounds start and end, this line doesn't exist - ] - expected = ( - 'a\n' - 'b\n' - 'c\n' - ) - # Old behavior: - # 1) first we end up with ('a\n', 'b\n', 'cc\n') - # 2) then we end up with ('a\n', 'b\n', '') - # New behavior: - # 1) line index 3 is "created" ('a\n', 'b\n', 'c\n', c\n')) - # 2) deletes line index 2. - yield from self.__run_formatting_test(original, expected, file_changes) - - def test_apply_formatting(self) -> 'Generator': - original = ( - '\n' - '\n' - '\n' - '\n' - ) - file_changes = [ - ((0, 28), (1, 0), ''), # delete first \n - ((1, 0), (1, 15), ''), # delete second line (but not the \n) - ((2, 10), (2, 10), '\n '), # insert after