diff --git a/boot.py b/boot.py index 111317bc0..dbf53f597 100644 --- a/boot.py +++ b/boot.py @@ -47,7 +47,6 @@ def invalidate_caches(self) -> None: from .plugin.core.panels import LspUpdatePanelCommand from .plugin.core.panels import LspUpdateServerPanelCommand from .plugin.core.protocol import Response - from .plugin.core.protocol import WorkspaceFolder from .plugin.core.registry import LspRecheckSessionsCommand from .plugin.core.registry import LspRestartClientCommand from .plugin.core.registry import windows @@ -60,6 +59,7 @@ def invalidate_caches(self) -> None: from .plugin.core.settings import unload_settings from .plugin.core.transports import kill_all_subprocesses from .plugin.core.types import ClientConfig + from .plugin.core.types import WorkspaceFolder from .plugin.core.typing import Optional, List, Type, Callable, Dict, Tuple from .plugin.core.views import LspRunTextCommandHelperCommand from .plugin.diagnostics import LspHideDiagnosticCommand diff --git a/plugin/__init__.py b/plugin/__init__.py index 3c0990869..47fb92ce9 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -3,13 +3,13 @@ from .core.protocol import Notification from .core.protocol import Request from .core.protocol import Response -from .core.protocol import WorkspaceFolder from .core.sessions import AbstractPlugin from .core.sessions import register_plugin from .core.sessions import Session from .core.sessions import SessionBufferProtocol from .core.sessions import unregister_plugin from .core.types import ClientConfig +from .core.types import WorkspaceFolder from .core.url import filename_to_uri from .core.url import uri_to_filename from .core.version import __version__ @@ -21,7 +21,7 @@ 'ClientConfig', 'css', 'DottedDict', - 'filename_to_uri', + 'filename_to_uri', # DEPRECATED: Use ClientConfig.map_client_path_to_server_uri instead 'Notification', 'register_plugin', 'Request', @@ -29,6 +29,6 @@ 'Session', 'SessionBufferProtocol', 'unregister_plugin', - 'uri_to_filename', + 'uri_to_filename', # DEPRECATED: Use ClientConfig.map_server_uri_to_client_path instead 'WorkspaceFolder', ] diff --git a/plugin/code_actions.py b/plugin/code_actions.py index 7519a58b1..ec80ff395 100644 --- a/plugin/code_actions.py +++ b/plugin/code_actions.py @@ -144,7 +144,7 @@ def _request_async( matching_kinds = get_matching_kinds(on_save_actions, supported_kinds or []) if matching_kinds: params = text_document_code_action_params( - view, file_name, request_range, [], matching_kinds) + session.config, view, file_name, request_range, [], matching_kinds) request = Request.codeAction(params, view) session.send_request_async( request, *filtering_collector(session.config.name, matching_kinds, collector)) @@ -153,7 +153,8 @@ def _request_async( diagnostics = diagnostics_by_config.get(config_name, []) if only_with_diagnostics and not diagnostics: continue - params = text_document_code_action_params(view, file_name, request_range, diagnostics) + params = text_document_code_action_params( + session.config, view, file_name, request_range, diagnostics) request = Request.codeAction(params, view) session.send_request_async(request, collector.create_collector(config_name)) if use_cache: diff --git a/plugin/core/edit.py b/plugin/core/edit.py index dbe012c83..b809c7269 100644 --- a/plugin/core/edit.py +++ b/plugin/core/edit.py @@ -1,8 +1,8 @@ from .logging import debug from .open import open_file from .promise import Promise +from .types import ClientConfig from .typing import List, Dict, Any, Iterable, Optional, Tuple -from .url import uri_to_filename from functools import partial import operator import sublime @@ -12,12 +12,13 @@ TextEdit = Tuple[Tuple[int, int], Tuple[int, int], str, Optional[int]] -def parse_workspace_edit(workspace_edit: Dict[str, Any]) -> Dict[str, List[TextEdit]]: +def parse_workspace_edit(config: ClientConfig, workspace_edit: Dict[str, Any]) -> Dict[str, List[TextEdit]]: changes = {} # type: Dict[str, List[TextEdit]] raw_changes = workspace_edit.get('changes') if isinstance(raw_changes, dict): for uri, file_changes in raw_changes.items(): - changes[uri_to_filename(uri)] = list(parse_text_edit(change) for change in file_changes) + path = config.map_server_uri_to_client_path(uri) + changes[path] = list(parse_text_edit(change) for change in file_changes) document_changes = workspace_edit.get('documentChanges') if isinstance(document_changes, list): for document_change in document_changes: @@ -27,7 +28,8 @@ def parse_workspace_edit(workspace_edit: Dict[str, Any]) -> Dict[str, List[TextE uri = document_change.get('textDocument').get('uri') version = document_change.get('textDocument').get('version') text_edit = list(parse_text_edit(change, version) for change in document_change.get('edits')) - changes.setdefault(uri_to_filename(uri), []).extend(text_edit) + path = config.map_server_uri_to_client_path(uri) + changes.setdefault(path, []).extend(text_edit) return changes diff --git a/plugin/core/open.py b/plugin/core/open.py index b0010fffc..c528cbb24 100644 --- a/plugin/core/open.py +++ b/plugin/core/open.py @@ -2,8 +2,8 @@ from .promise import Promise from .promise import ResolveFunc from .protocol import Range +from .types import ClientConfig from .typing import Any, Dict, Tuple, Optional -from .url import uri_to_filename from .views import range_to_region import os import sublime @@ -65,13 +65,13 @@ def open_file_and_center_async(window: sublime.Window, file_path: str, r: Option .then(Promise.on_async_thread) -def open_externally(uri: str, take_focus: bool) -> bool: +def open_externally(config: ClientConfig, uri: str, take_focus: bool) -> bool: """ A blocking function that invokes the OS's "open with default extension" """ if uri.startswith("http:") or uri.startswith("https:"): return webbrowser.open(uri, autoraise=take_focus) - file = uri_to_filename(uri) + file = config.map_server_uri_to_client_path(uri) try: # TODO: handle take_focus if sublime.platform() == "windows": diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index 55272ee53..92f47c327 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -1,7 +1,5 @@ from .typing import Any, Dict, Iterable, List, Mapping, Optional, TypedDict, Union -from .url import filename_to_uri from .url import uri_to_filename -import os import sublime @@ -449,39 +447,3 @@ def __eq__(self, other: object) -> bool: def __repr__(self) -> str: return str(self.range) + ":" + self.message - - -class WorkspaceFolder: - - __slots__ = ('name', 'path') - - def __init__(self, name: str, path: str) -> None: - self.name = name - self.path = path - - @classmethod - def from_path(cls, path: str) -> 'WorkspaceFolder': - return cls(os.path.basename(path) or path, path) - - def __hash__(self) -> int: - return hash((self.name, self.path)) - - def __repr__(self) -> str: - return "{}('{}', '{}')".format(self.__class__.__name__, self.name, self.path) - - def __str__(self) -> str: - return self.path - - def __eq__(self, other: Any) -> bool: - if isinstance(other, WorkspaceFolder): - return self.name == other.name and self.path == other.path - return False - - def to_lsp(self) -> Dict[str, str]: - return {"name": self.name, "uri": self.uri()} - - def uri(self) -> str: - return filename_to_uri(self.path) - - def includes_uri(self, uri: str) -> bool: - return uri.startswith(self.uri()) diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index bd30f70b0..0cc079d86 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -16,7 +16,6 @@ from .protocol import Notification from .protocol import Request from .protocol import Response -from .protocol import WorkspaceFolder from .settings import client_configs from .transports import Transport from .transports import TransportCallbacks @@ -28,8 +27,8 @@ from .types import DocumentSelector from .types import method_to_capability from .types import SettingsRegistration +from .types import WorkspaceFolder from .typing import Callable, cast, Dict, Any, Optional, List, Tuple, Generator, Type, Protocol, Mapping, Union -from .url import uri_to_filename from .version import __version__ from .views import COMPLETION_KINDS from .views import extract_variables @@ -108,7 +107,7 @@ def on_post_exit_async(self, session: 'Session', exit_code: int, exception: Opti def get_initialize_params(variables: Dict[str, str], workspace_folders: List[WorkspaceFolder], - config: ClientConfig) -> dict: + config: ClientConfig) -> Dict[str, Any]: completion_kinds = list(range(1, len(COMPLETION_KINDS) + 1)) symbol_kinds = list(range(1, len(SYMBOL_KINDS) + 1)) completion_tag_value_set = [v for k, v in CompletionItemTag.__dict__.items() if not k.startswith('_')] @@ -259,18 +258,22 @@ def get_initialize_params(variables: Dict[str, str], workspace_folders: List[Wor } if config.experimental_capabilities is not None: capabilities['experimental'] = config.experimental_capabilities - return { - "processId": os.getpid(), + root_uri = first_folder.uri(config) if first_folder else None + root_path = root_uri[len("file://"):] if root_uri else None + result = { "clientInfo": { "name": "Sublime Text LSP", "version": ".".join(map(str, __version__)) }, - "rootUri": first_folder.uri() if first_folder else None, - "rootPath": first_folder.path if first_folder else None, - "workspaceFolders": [folder.to_lsp() for folder in workspace_folders] if workspace_folders else None, + "rootUri": root_uri, + "rootPath": root_path, + "workspaceFolders": [folder.to_lsp(config) for folder in workspace_folders] if workspace_folders else None, "capabilities": capabilities, "initializationOptions": config.init_options.get_resolved(variables) - } + } # type: Dict[str, Any] + if not config.path_maps: + result["processId"] = os.getpid() + return result class SessionViewProtocol(Protocol): @@ -823,7 +826,7 @@ def session_buffers_async(self) -> Generator[SessionBufferProtocol, None, None]: yield from self._session_buffers def get_session_buffer_for_uri_async(self, uri: str) -> Optional[SessionBufferProtocol]: - file_name = uri_to_filename(uri) + file_name = self.config.map_server_uri_to_client_path(uri) for sb in self.session_buffers_async(): try: if sb.file_name == file_name or os.path.samefile(file_name, sb.file_name): @@ -897,8 +900,8 @@ def update_folders(self, folders: List[WorkspaceFolder]) -> None: if added or removed: params = { "event": { - "added": [a.to_lsp() for a in added], - "removed": [r.to_lsp() for r in removed] + "added": [a.to_lsp(self.config) for a in added], + "removed": [r.to_lsp(self.config) for r in removed] } } self.send_notification(Notification.didChangeWorkspaceFolders(params)) @@ -1034,7 +1037,7 @@ def _apply_workspace_edit_async(self, edit: Any) -> Promise: Apply workspace edits, and return a promise that resolves on the async thread again after the edits have been applied. """ - changes = parse_workspace_edit(edit) + changes = parse_workspace_edit(self.config, edit) return Promise.on_main_thread() \ .then(lambda _: apply_workspace_edit(self.window, changes)) \ .then(Promise.on_async_thread) @@ -1055,7 +1058,7 @@ def m_window_logMessage(self, params: Any) -> None: def m_workspace_workspaceFolders(self, _: Any, request_id: Any) -> None: """handles the workspace/workspaceFolders request""" - self.send_response(Response(request_id, [wf.to_lsp() for wf in self._workspace_folders])) + self.send_response(Response(request_id, [wf.to_lsp(self.config) for wf in self._workspace_folders])) def m_workspace_configuration(self, params: Dict[str, Any], request_id: Any) -> None: """handles the workspace/configuration request""" @@ -1134,10 +1137,10 @@ def success(b: bool) -> None: self.send_response(Response(request_id, {"success": b})) if params.get("external"): - success(open_externally(uri, bool(params.get("takeFocus")))) + success(open_externally(self.config, uri, bool(params.get("takeFocus")))) else: # TODO: ST API does not allow us to say "do not focus this new view" - filename = uri_to_filename(uri) + filename = self.config.map_server_uri_to_client_path(uri) selection = params.get("selection") open_file_and_center_async(self.window, filename, selection).then(lambda _: success(True)) diff --git a/plugin/core/transports.py b/plugin/core/transports.py index 542b4b456..1dd408cd7 100644 --- a/plugin/core/transports.py +++ b/plugin/core/transports.py @@ -88,6 +88,7 @@ def _read_loop(self) -> None: try: while self._reader: headers = http.client.parse_headers(self._reader) # type: ignore + # print("headers:", headers) body = self._reader.read(int(headers.get("Content-Length"))) try: payload = _decode(body) diff --git a/plugin/core/types.py b/plugin/core/types.py index 7f73a789a..da5d6f4d3 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -1,5 +1,6 @@ from .collections import DottedDict -from .logging import debug, set_debug_logging +from .logging import debug +from .logging import set_debug_logging from .protocol import TextDocumentSyncKindNone from .typing import Any, Optional, List, Dict, Generator, Callable, Iterable, Union, Set, Tuple, TypeVar from .url import filename_to_uri @@ -465,8 +466,13 @@ def _translate_path(path: str, source: str, destination: str) -> Tuple[str, bool # TODO: Case-insensitive file systems. Maybe this problem needs a much larger refactor. Even Sublime Text doesn't # handle case-insensitive file systems correctly. There are a few other places where case-sensitivity matters, for # example when looking up the correct view for diagnostics, and when finding a view for goto-def. - if path.startswith(source) and len(path) > len(source) and path[len(source)] in ("/", "\\"): - return path.replace(source, destination, 1), True + if path.startswith(source): + if len(path) > len(source): + if path[len(source)] in ("/", "\\"): + return path.replace(source, destination, 1), True + else: + # This means len(path) == len(source), hence path == source + return destination, True return path, False @@ -485,15 +491,12 @@ def parse(cls, json: Any) -> "Optional[List[PathMap]]": result = [] # type: List[PathMap] for path_map in json: if not isinstance(path_map, dict): - debug('path map entry is not an object') continue local = path_map.get("local") if not isinstance(local, str): - debug('missing "local" key for path map entry') continue remote = path_map.get("remote") if not isinstance(remote, str): - debug('missing "remote" key for path map entry') continue result.append(PathMap(local, remote)) return result @@ -509,6 +512,10 @@ def map_from_local_to_remote(self, uri: str) -> Tuple[str, bool]: def map_from_remote_to_local(self, uri: str) -> Tuple[str, bool]: return _translate_path(uri, self._remote, self._local) + def resolve(self, variables: Dict[str, str]) -> None: + self._local = sublime.expand_variables(self._local, variables) + self._remote = sublime.expand_variables(self._remote, variables) + class TransportConfig: __slots__ = ("name", "command", "tcp_port", "env", "listener_socket") @@ -651,6 +658,9 @@ def resolve_transport_config(self, variables: Dict[str, str]) -> TransportConfig env = os.environ.copy() for var, value in self.env.items(): env[var] = sublime.expand_variables(value, variables) + if isinstance(self.path_maps, list): + for path_map in self.path_maps: + path_map.resolve(variables) return TransportConfig(self.name, command, tcp_port, env, listener_socket) def set_view_status(self, view: sublime.View, message: str) -> None: @@ -704,6 +714,45 @@ def __eq__(self, other: Any) -> bool: return True +class WorkspaceFolder: + + __slots__ = ('name', 'path') + + def __init__(self, name: str, path: str) -> None: + self.name = name + self.path = path + + @classmethod + def from_path(cls, path: str) -> 'WorkspaceFolder': + return cls(os.path.basename(path) or path, path) + + def __hash__(self) -> int: + return hash((self.name, self.path)) + + def __repr__(self) -> str: + return "{}('{}', '{}')".format(self.__class__.__name__, self.name, self.path) + + def __str__(self) -> str: + return self.path + + def __eq__(self, other: Any) -> bool: + if isinstance(other, WorkspaceFolder): + return self.name == other.name and self.path == other.path + return False + + def to_lsp(self, config: Optional[ClientConfig] = None) -> Dict[str, str]: + return {"name": self.name, "uri": self.uri(config)} + + def uri(self, config: Optional[ClientConfig] = None) -> str: + if config: + return config.map_client_path_to_server_uri(self.path) + else: + return filename_to_uri(self.path) + + def includes_uri(self, uri: str, config: Optional[ClientConfig] = None) -> bool: + return uri.startswith(self.uri(config)) + + def syntax2scope(syntax_path: str) -> Optional[str]: syntax = sublime.syntax_from_path(syntax_path) return syntax.scope if syntax else None diff --git a/plugin/core/views.py b/plugin/core/views.py index 709f34147..1cf006470 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -8,10 +8,12 @@ from .protocol import Point from .protocol import Range from .protocol import Request +from .types import ClientConfig from .typing import Optional, Dict, Any, Iterable, List, Union, Callable, Tuple from .url import filename_to_uri from .url import uri_to_filename import html +import itertools import linecache import mdpopups import os @@ -19,7 +21,6 @@ import sublime import sublime_plugin import tempfile -import itertools DIAGNOSTIC_SEVERITY = [ # Kind CSS class Scope for color Icon resource @@ -155,7 +156,7 @@ def region_to_range(view: sublime.View, region: sublime.Region) -> Range: ) -def location_to_encoded_filename(location: Dict[str, Any]) -> str: +def location_to_encoded_filename(location: Dict[str, Any], config: Optional[ClientConfig] = None) -> str: if "targetUri" in location: uri = location["targetUri"] position = location["targetSelectionRange"]["start"] @@ -163,7 +164,11 @@ def location_to_encoded_filename(location: Dict[str, Any]) -> str: uri = location["uri"] position = location["range"]["start"] # WARNING: Cannot possibly do UTF-16 conversion :) Oh well. - return '{}:{}:{}'.format(uri_to_filename(uri), position['line'] + 1, position['character'] + 1) + return '{}:{}:{}'.format( + config.map_server_uri_to_client_path(uri) if config else uri_to_filename(uri), + position['line'] + 1, + position['character'] + 1 + ) class MissingFilenameError(Exception): @@ -173,18 +178,27 @@ def __init__(self, view_id: int) -> None: self.view_id = view_id -def uri_from_view(view: sublime.View) -> str: +def uri_from_view(view: sublime.View, config: Optional[ClientConfig] = None) -> str: file_name = view.file_name() if file_name: - return filename_to_uri(file_name) + if config: + return config.map_client_path_to_server_uri(file_name) + else: + # DEPRECATED + return filename_to_uri(file_name) raise MissingFilenameError(view.id()) -def text_document_identifier(view_or_file_name: Union[str, sublime.View]) -> Dict[str, Any]: +def text_document_identifier(view_or_file_name: Union[str, sublime.View], + config: Optional[ClientConfig] = None) -> Dict[str, Any]: if isinstance(view_or_file_name, str): - uri = filename_to_uri(view_or_file_name) + if config: + uri = config.map_client_path_to_server_uri(view_or_file_name) + else: + # DEPRECATED + uri = filename_to_uri(view_or_file_name) else: - uri = uri_from_view(view_or_file_name) + uri = uri_from_view(view_or_file_name, config) return {"uri": uri} @@ -200,25 +214,29 @@ def entire_content_range(view: sublime.View) -> Range: return region_to_range(view, entire_content_region(view)) -def text_document_item(view: sublime.View, language_id: str) -> Dict[str, Any]: +def text_document_item(config: ClientConfig, view: sublime.View, language_id: str) -> Dict[str, Any]: return { - "uri": uri_from_view(view), + "uri": uri_from_view(view, config), "languageId": language_id, "version": view.change_count(), "text": entire_content(view) } -def versioned_text_document_identifier(view: sublime.View, version: int) -> Dict[str, Any]: - return {"uri": uri_from_view(view), "version": version} +def versioned_text_document_identifier(config: ClientConfig, view: sublime.View, version: int) -> Dict[str, Any]: + return {"uri": uri_from_view(view, config), "version": version} -def text_document_position_params(view: sublime.View, location: int) -> Dict[str, Any]: - return {"textDocument": text_document_identifier(view), "position": offset_to_point(view, location).to_lsp()} +def text_document_position_params(view: sublime.View, location: int, + config: Optional[ClientConfig] = None) -> Dict[str, Any]: + return { + "textDocument": text_document_identifier(view, config), + "position": offset_to_point(view, location).to_lsp() + } -def did_open_text_document_params(view: sublime.View, language_id: str) -> Dict[str, Any]: - return {"textDocument": text_document_item(view, language_id)} +def did_open_text_document_params(config: ClientConfig, view: sublime.View, language_id: str) -> Dict[str, Any]: + return {"textDocument": text_document_item(config, view, language_id)} def render_text_change(change: sublime.TextChange) -> Dict[str, Any]: @@ -232,10 +250,17 @@ def render_text_change(change: sublime.TextChange) -> Dict[str, Any]: } -def did_change_text_document_params(view: sublime.View, version: int, - changes: Optional[Iterable[sublime.TextChange]] = None) -> Dict[str, Any]: +def did_change_text_document_params( + config: ClientConfig, + view: sublime.View, + version: int, + changes: Optional[Iterable[sublime.TextChange]] = None +) -> Dict[str, Any]: content_changes = [] # type: List[Dict[str, Any]] - result = {"textDocument": versioned_text_document_identifier(view, version), "contentChanges": content_changes} + result = { + "textDocument": versioned_text_document_identifier(config, view, version), + "contentChanges": content_changes + } if changes is None: # TextDocumentSyncKindFull content_changes.append({"text": entire_content(view)}) @@ -246,47 +271,52 @@ def did_change_text_document_params(view: sublime.View, version: int, return result -def will_save_text_document_params(view_or_file_name: Union[str, sublime.View], reason: int) -> Dict[str, Any]: - return {"textDocument": text_document_identifier(view_or_file_name), "reason": reason} +def will_save_text_document_params( + config: ClientConfig, + view_or_file_name: Union[str, sublime.View], + reason: int +) -> Dict[str, Any]: + return {"textDocument": text_document_identifier(view_or_file_name, config), "reason": reason} def did_save_text_document_params( - view: sublime.View, include_text: bool, file_name: Optional[str] = None + config: ClientConfig, view: sublime.View, include_text: bool, file_name: Optional[str] = None ) -> Dict[str, Any]: - identifier = text_document_identifier(file_name if file_name is not None else view) + identifier = text_document_identifier(file_name if file_name is not None else view, config) result = {"textDocument": identifier} # type: Dict[str, Any] if include_text: result["text"] = entire_content(view) return result -def did_close_text_document_params(file_name: str) -> Dict[str, Any]: - return {"textDocument": text_document_identifier(file_name)} +def did_close_text_document_params(config: ClientConfig, file_name: str) -> Dict[str, Any]: + return {"textDocument": text_document_identifier(file_name, config)} -def did_open(view: sublime.View, language_id: str) -> Notification: - return Notification.didOpen(did_open_text_document_params(view, language_id)) +def did_open(config: ClientConfig, view: sublime.View, language_id: str) -> Notification: + return Notification.didOpen(did_open_text_document_params(config, view, language_id)) -def did_change(view: sublime.View, version: int, +def did_change(config: ClientConfig, view: sublime.View, version: int, changes: Optional[Iterable[sublime.TextChange]] = None) -> Notification: - return Notification.didChange(did_change_text_document_params(view, version, changes)) + return Notification.didChange(did_change_text_document_params(config, view, version, changes)) -def will_save(file_name: str, reason: int) -> Notification: - return Notification.willSave(will_save_text_document_params(file_name, reason)) +def will_save(config: ClientConfig, file_name: str, reason: int) -> Notification: + return Notification.willSave(will_save_text_document_params(config, file_name, reason)) -def will_save_wait_until(view: sublime.View, reason: int) -> Request: - return Request.willSaveWaitUntil(will_save_text_document_params(view, reason), view) +def will_save_wait_until(config: ClientConfig, view: sublime.View, reason: int) -> Request: + return Request.willSaveWaitUntil(will_save_text_document_params(config, view, reason), view) -def did_save(view: sublime.View, include_text: bool, file_name: Optional[str] = None) -> Notification: - return Notification.didSave(did_save_text_document_params(view, include_text, file_name)) +def did_save(config: ClientConfig, view: sublime.View, include_text: bool, + file_name: Optional[str] = None) -> Notification: + return Notification.didSave(did_save_text_document_params(config, view, include_text, file_name)) -def did_close(file_name: str) -> Notification: - return Notification.didClose(did_close_text_document_params(file_name)) +def did_close(config: ClientConfig, file_name: str) -> Notification: + return Notification.didClose(did_close_text_document_params(config, file_name)) def formatting_options(settings: sublime.Settings) -> Dict[str, Any]: @@ -307,29 +337,30 @@ def formatting_options(settings: sublime.Settings) -> Dict[str, Any]: } -def text_document_formatting(view: sublime.View) -> Request: +def text_document_formatting(config: ClientConfig, view: sublime.View) -> Request: return Request.formatting({ - "textDocument": text_document_identifier(view), + "textDocument": text_document_identifier(view, config), "options": formatting_options(view.settings()) }, view) -def text_document_range_formatting(view: sublime.View, region: sublime.Region) -> Request: +def text_document_range_formatting(config: ClientConfig, view: sublime.View, region: sublime.Region) -> Request: return Request.rangeFormatting({ - "textDocument": text_document_identifier(view), + "textDocument": text_document_identifier(view, config), "options": formatting_options(view.settings()), "range": region_to_range(view, region).to_lsp() }, view) -def selection_range_params(view: sublime.View) -> Dict[str, Any]: +def selection_range_params(config: ClientConfig, view: sublime.View) -> Dict[str, Any]: return { - "textDocument": text_document_identifier(view), + "textDocument": text_document_identifier(view, config), "positions": [position(view, r.b) for r in view.sel()] } def text_document_code_action_params( + config: ClientConfig, view: sublime.View, file_name: str, range: Range, @@ -337,9 +368,7 @@ def text_document_code_action_params( on_save_actions: Optional[List[str]] = None ) -> Dict: params = { - "textDocument": { - "uri": filename_to_uri(file_name) - }, + "textDocument": text_document_identifier(view, config), "range": range.to_lsp(), "context": { "diagnostics": list(diagnostic.to_lsp() for diagnostic in diagnostics) @@ -532,8 +561,8 @@ def lsp_color_to_phantom(view: sublime.View, color_info: Dict[str, Any]) -> subl return sublime.Phantom(region, lsp_color_to_html(color_info), sublime.LAYOUT_INLINE) -def document_color_params(view: sublime.View) -> Dict[str, Any]: - return {"textDocument": text_document_identifier(view)} +def document_color_params(config: ClientConfig, view: sublime.View) -> Dict[str, Any]: + return {"textDocument": text_document_identifier(view, config)} def format_severity(severity: int) -> str: diff --git a/plugin/core/workspace.py b/plugin/core/workspace.py index 08d753dc6..9c8a8c0f3 100644 --- a/plugin/core/workspace.py +++ b/plugin/core/workspace.py @@ -1,9 +1,9 @@ from .logging import debug -from .protocol import WorkspaceFolder from .types import diff +from .types import WorkspaceFolder from .typing import List, Any, Union -import sublime import os +import sublime def is_subpath_of(file_path: str, potential_subpath: str) -> bool: diff --git a/plugin/documents.py b/plugin/documents.py index a007686db..d2baafabd 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -330,7 +330,7 @@ def _do_signature_help(self, manual: bool) -> None: last_char = previous_non_whitespace_char(self.view, pos) if manual or last_char in triggers: self.purge_changes_async() - params = text_document_position_params(self.view, pos) + params = text_document_position_params(self.view, pos, session.config) assert session session.send_request_async( Request.signatureHelp(params, self.view), lambda resp: self._on_signature_help(resp, pos)) @@ -422,7 +422,8 @@ def _do_color_boxes_async(self) -> None: session = self.session("colorProvider") if session: session.send_request_async( - Request.documentColor(document_color_params(self.view), self.view), self._on_color_boxes) + Request.documentColor( + document_color_params(session.config, self.view), self.view), self._on_color_boxes) def _on_color_boxes(self, response: Any) -> None: color_infos = response if response else [] @@ -448,7 +449,7 @@ def _do_highlights_async(self) -> None: point = self.view.sel()[0].b session = self.session("documentHighlightProvider", point) if session: - params = text_document_position_params(self.view, point) + params = text_document_position_params(self.view, point, session.config) request = Request.documentHighlight(params, self.view) session.send_request_async(request, self._on_highlights) @@ -487,7 +488,7 @@ def _on_query_completions_async(self, resolve: ResolveCompletionsFn, location: i can_resolve_completion_items = bool(session.get_capability('completionProvider.resolveProvider')) config_name = session.config.name session.send_request_async( - Request.complete(text_document_position_params(self.view, location), self.view), + Request.complete(text_document_position_params(self.view, location, session.config), self.view), lambda res: self._on_complete_result(res, resolve, can_resolve_completion_items, config_name), lambda res: self._on_complete_error(res, resolve)) diff --git a/plugin/execute_command.py b/plugin/execute_command.py index 14e4bd46f..8435dd4bd 100644 --- a/plugin/execute_command.py +++ b/plugin/execute_command.py @@ -1,9 +1,10 @@ -import sublime from .core.protocol import Error from .core.protocol import ExecuteCommandParams from .core.registry import LspTextCommand +from .core.types import ClientConfig from .core.typing import List, Optional, Any from .core.views import uri_from_view, offset_to_point, region_to_range, text_document_identifier +import sublime class LspExecuteCommand(LspTextCommand): @@ -19,7 +20,7 @@ def run(self, session = self.session_by_name(session_name) if session_name else self.best_session(self.capability) if session and command_name: if command_args: - self._expand_variables(command_args) + self._expand_variables(session.config, command_args) params = {"command": command_name} # type: ExecuteCommandParams if command_args: params["arguments"] = command_args @@ -38,13 +39,13 @@ def handle_response(response: Any) -> None: session.execute_command(params).then(handle_response) - def _expand_variables(self, command_args: List[Any]) -> None: + def _expand_variables(self, config: ClientConfig, command_args: List[Any]) -> None: region = self.view.sel()[0] for i, arg in enumerate(command_args): if arg in ["$document_id", "${document_id}"]: - command_args[i] = text_document_identifier(self.view) + command_args[i] = text_document_identifier(self.view, config) if arg in ["$file_uri", "${file_uri}"]: - command_args[i] = uri_from_view(self.view) + command_args[i] = uri_from_view(self.view, config) elif arg in ["$selection", "${selection}"]: command_args[i] = self.view.substr(region) elif arg in ["$offset", "${offset}"]: diff --git a/plugin/formatting.py b/plugin/formatting.py index 31ac08a69..a5e2f60ee 100644 --- a/plugin/formatting.py +++ b/plugin/formatting.py @@ -41,7 +41,7 @@ def _handle_next_session_async(self) -> None: def _will_save_wait_until_async(self, session: Session) -> None: session.send_request_async( - will_save_wait_until(self._view, reason=1), # TextDocumentSaveReason.Manual + will_save_wait_until(session.config, self._view, reason=1), # TextDocumentSaveReason.Manual self._on_response, lambda error: self._on_response(None)) @@ -68,7 +68,10 @@ def _format_on_save_async(self) -> None: session = next(sessions_for_view(self._view, 'documentFormattingProvider'), None) if session: session.send_request_async( - text_document_formatting(self._view), self._on_response, lambda error: self._on_response(None)) + text_document_formatting(session.config, self._view), + self._on_response, + lambda error: self._on_response(None) + ) else: self._on_complete() @@ -93,12 +96,12 @@ def run(self, edit: sublime.Edit, event: Optional[dict] = None) -> None: session = self.best_session(self.capability) if session: # Either use the documentFormattingProvider ... - session.send_request(text_document_formatting(self.view), self.on_result) + session.send_request(text_document_formatting(session.config, self.view), self.on_result) else: session = self.best_session(LspFormatDocumentRangeCommand.capability) if session: # ... or use the documentRangeFormattingProvider and format the entire range. - req = text_document_range_formatting(self.view, entire_content_region(self.view)) + req = text_document_range_formatting(session.config, self.view, entire_content_region(self.view)) session.send_request(req, self.on_result) def on_result(self, params: Any) -> None: @@ -120,5 +123,5 @@ def is_enabled(self, event: Optional[dict] = None, point: Optional[int] = None) def run(self, edit: sublime.Edit, event: Optional[dict] = None) -> None: session = self.best_session(self.capability) if session: - req = text_document_range_formatting(self.view, self.view.sel()[0]) + req = text_document_range_formatting(session.config, self.view, self.view.sel()[0]) session.send_request(req, lambda response: apply_response_to_view(response, self.view)) diff --git a/plugin/goto.py b/plugin/goto.py index d97095ede..e494810e4 100644 --- a/plugin/goto.py +++ b/plugin/goto.py @@ -1,11 +1,12 @@ -import sublime from .core.protocol import Request from .core.registry import get_position from .core.registry import LspTextCommand from .core.sessions import method_to_capability +from .core.types import ClientConfig from .core.typing import List, Optional, Any from .core.views import location_to_encoded_filename from .core.views import text_document_position_params +import sublime def open_location(window: sublime.Window, location: str, side_by_side: bool = True) -> None: @@ -47,14 +48,15 @@ def run( ) -> None: session = self.best_session(self.capability) if session: - params = text_document_position_params(self.view, get_position(self.view, event, point)) + config = session.config + params = text_document_position_params(self.view, get_position(self.view, event, point), config) session.send_request( Request(self.method, params, self.view), # It's better to run this on the UI thread so we are guaranteed no AttributeErrors anywhere - lambda response: sublime.set_timeout(lambda: self.handle_response(response, side_by_side)) + lambda response: sublime.set_timeout(lambda: self.handle_response(config, response, side_by_side)) ) - def handle_response(self, response: Any, side_by_side: bool) -> None: + def handle_response(self, config: ClientConfig, response: Any, side_by_side: bool) -> None: if not self.view.is_valid(): return window = self.view.window() @@ -64,9 +66,9 @@ def handle_response(self, response: Any, side_by_side: bool) -> None: if len(self.view.sel()) > 0: self.view.run_command("add_jump_record", {"selection": [(r.a, r.b) for r in self.view.sel()]}) if isinstance(response, dict): - locations = [location_to_encoded_filename(response)] + locations = [location_to_encoded_filename(response, config)] else: - locations = [location_to_encoded_filename(x) for x in response] + locations = [location_to_encoded_filename(x, config) for x in response] if len(locations) == 1: open_location(window, locations[0], side_by_side) elif len(locations) > 1: diff --git a/plugin/hover.py b/plugin/hover.py index cddb5e77c..5b05e83b3 100644 --- a/plugin/hover.py +++ b/plugin/hover.py @@ -89,7 +89,7 @@ def run_async() -> None: def request_symbol_hover(self, point: int) -> None: session = self.best_session('hoverProvider', point) if session: - document_position = text_document_position_params(self.view, point) + document_position = text_document_position_params(self.view, point, session.config) session.send_request( Request.hover(document_position, self.view), lambda response: self.handle_response(response, point)) diff --git a/plugin/references.py b/plugin/references.py index d2d03a579..4df755708 100644 --- a/plugin/references.py +++ b/plugin/references.py @@ -1,21 +1,19 @@ +import linecache import os import sublime -import linecache - from .core.panels import ensure_panel -from .core.protocol import Request, Point +from .core.protocol import Point +from .core.protocol import Request from .core.registry import get_position from .core.registry import LspTextCommand from .core.registry import windows from .core.settings import PLUGIN_NAME from .core.settings import userprefs +from .core.types import ClientConfig from .core.types import PANEL_FILE_REGEX, PANEL_LINE_REGEX -from .core.typing import List, Dict, Optional, Tuple, TypedDict -from .core.url import uri_to_filename +from .core.typing import Any, List, Dict, Optional, Tuple from .core.views import get_line, text_document_position_params -ReferenceDict = TypedDict('ReferenceDict', {'uri': str, 'range': dict}) - def ensure_references_panel(window: sublime.Window) -> 'Optional[sublime.View]': return ensure_panel(window, "references", PANEL_FILE_REGEX, PANEL_LINE_REGEX, @@ -50,12 +48,13 @@ def run(self, edit: sublime.Edit, event: Optional[dict] = None, point: Optional[ if os.path.commonprefix([base_dir, file_path]): self.base_dir = base_dir - document_position = text_document_position_params(self.view, pos) + document_position = text_document_position_params(self.view, pos, session.config) document_position['context'] = {"includeDeclaration": False} request = Request.references(document_position, self.view) - session.send_request(request, lambda response: self.handle_response(response, pos)) + config = session.config + session.send_request(request, lambda response: self.handle_response(config, response, pos)) - def handle_response(self, response: Optional[List[ReferenceDict]], pos: int) -> None: + def handle_response(self, config: ClientConfig, response: Optional[List[Dict[str, Any]]], pos: int) -> None: window = self.view.window() if response is None: @@ -69,7 +68,7 @@ def handle_response(self, response: Optional[List[ReferenceDict]], pos: int) -> window.status_message("No references found") return - references_by_file = self._group_references_by_file(response) + references_by_file = self._group_references_by_file(config, response) if userprefs().show_references_in_quick_panel: self.show_quick_panel(references_by_file) @@ -160,12 +159,15 @@ def get_full_path(self, file_path: str) -> str: return os.path.join(self.base_dir, file_path) return file_path - def _group_references_by_file(self, references: List[ReferenceDict] - ) -> Dict[str, List[Tuple[Point, str]]]: + def _group_references_by_file( + self, + config: ClientConfig, + references: List[Dict[str, Any]] + ) -> Dict[str, List[Tuple[Point, str]]]: """ Return a dictionary that groups references by the file it belongs. """ grouped_references = {} # type: Dict[str, List[Tuple[Point, str]]] for reference in references: - file_path = uri_to_filename(reference["uri"]) + file_path = config.map_server_uri_to_client_path(reference["uri"]) point = Point.from_lsp(reference['range']['start']) # get line of the reference, to showcase its use diff --git a/plugin/rename.py b/plugin/rename.py index 22804fe53..a51082668 100644 --- a/plugin/rename.py +++ b/plugin/rename.py @@ -8,6 +8,7 @@ from .core.registry import get_position from .core.registry import LspTextCommand from .core.registry import windows +from .core.types import ClientConfig from .core.types import PANEL_FILE_REGEX, PANEL_LINE_REGEX from .core.typing import Any, Optional, Dict, List from .core.views import range_to_region, get_line @@ -82,7 +83,7 @@ def run( else: session = self.best_session("{}.prepareProvider".format(self.capability)) if session: - params = text_document_position_params(self.view, pos) + params = text_document_position_params(self.view, pos, session.config) request = Request.prepareRename(params, self.view) self.event = event session.send_request(request, lambda r: self.on_prepare_result(r, pos), self.on_prepare_error) @@ -99,19 +100,20 @@ def run( def _do_rename(self, position: int, new_name: str) -> None: session = self.best_session(self.capability) if session: - params = text_document_position_params(self.view, position) + params = text_document_position_params(self.view, position, session.config) params["newName"] = new_name + config = session.config session.send_request( Request.rename(params, self.view), # This has to run on the main thread due to calling apply_workspace_edit - lambda r: sublime.set_timeout(lambda: self.on_rename_result(r)) + lambda r: sublime.set_timeout(lambda: self.on_rename_result(config, r)) ) - def on_rename_result(self, response: Any) -> None: + def on_rename_result(self, config: ClientConfig, response: Any) -> None: window = self.view.window() if window: if response: - changes = parse_workspace_edit(response) + changes = parse_workspace_edit(config, response) file_count = len(changes.keys()) if file_count > 1: total_changes = sum(map(len, changes.values())) diff --git a/plugin/selection_range.py b/plugin/selection_range.py index af244734a..ca58c8b41 100644 --- a/plugin/selection_range.py +++ b/plugin/selection_range.py @@ -24,7 +24,7 @@ def is_enabled(self, event: Optional[dict] = None, point: Optional[int] = None) def run(self, edit: sublime.Edit, event: Optional[dict] = None) -> None: session = self.best_session(self.capability, get_position(self.view, event)) if session: - params = selection_range_params(self.view) + params = selection_range_params(session.config, self.view) self._regions.extend(self.view.sel()) self._change_count = self.view.change_count() session.send_request(Request(self.method, params), self.on_result, self.on_error) diff --git a/plugin/session_buffer.py b/plugin/session_buffer.py index 3e88b759f..5782548bf 100644 --- a/plugin/session_buffer.py +++ b/plugin/session_buffer.py @@ -98,12 +98,12 @@ def __del__(self) -> None: def _check_did_open(self, view: sublime.View) -> None: if not self.opened and self.should_notify_did_open(): - self.session.send_notification(did_open(view, self.language_id)) + self.session.send_notification(did_open(self.session.config, view, self.language_id)) self.opened = True def _check_did_close(self) -> None: if self.opened and self.should_notify_did_close(): - self.session.send_notification(did_close(self.file_name)) + self.session.send_notification(did_close(self.session.config, self.file_name)) self.opened = False def add_session_view(self, sv: SessionViewProtocol) -> None: @@ -192,7 +192,7 @@ def on_text_changed_async(self, view: sublime.View, change_count: int, def on_revert_async(self, view: sublime.View) -> None: self.pending_changes = None # Don't bother with pending changes - self.session.send_notification(did_change(view, view.change_count(), None)) + self.session.send_notification(did_change(self.session.config, view, view.change_count(), None)) on_reload_async = on_revert_async @@ -207,7 +207,7 @@ def purge_changes_async(self, view: sublime.View) -> None: else: changes = self.pending_changes.changes version = self.pending_changes.version - notification = did_change(view, version, changes) + notification = did_change(self.session.config, view, version, changes) self.session.send_notification(notification) self.pending_changes = None @@ -215,7 +215,7 @@ def on_pre_save_async(self, view: sublime.View, old_file_name: str) -> None: if self.should_notify_will_save(): self.purge_changes_async(view) # TextDocumentSaveReason.Manual - self.session.send_notification(will_save(old_file_name, 1)) + self.session.send_notification(will_save(self.session.config, old_file_name, 1)) def on_post_save_async(self, view: sublime.View) -> None: file_name = view.file_name() @@ -228,7 +228,7 @@ def on_post_save_async(self, view: sublime.View) -> None: if send_did_save: self.purge_changes_async(view) # mypy: expected sublime.View, got ViewLike - self.session.send_notification(did_save(view, include_text, self.file_name)) + self.session.send_notification(did_save(self.session.config, view, include_text, self.file_name)) if self.should_show_diagnostics_panel: mgr = self.session.manager() if mgr: diff --git a/plugin/symbols.py b/plugin/symbols.py index c382842ce..4284206ab 100644 --- a/plugin/symbols.py +++ b/plugin/symbols.py @@ -1,6 +1,7 @@ from .core.protocol import Request, Range from .core.registry import LspTextCommand from .core.sessions import print_to_status_bar +from .core.types import ClientConfig from .core.typing import Any, List, Optional, Tuple, Dict, Generator from .core.views import location_to_encoded_filename from .core.views import range_to_region @@ -82,8 +83,9 @@ def run(self, edit: sublime.Edit) -> None: self.view.settings().set(SUPPRESS_INPUT_SETTING_KEY, True) session = self.best_session(self.capability) if session: + params = {"textDocument": text_document_identifier(self.view, session.config)} session.send_request( - Request.documentSymbols({"textDocument": text_document_identifier(self.view)}, self.view), + Request.documentSymbols(params, self.view), lambda response: sublime.set_timeout(lambda: self.handle_response(response)), lambda error: sublime.set_timeout(lambda: self.handle_response_error(error))) @@ -218,8 +220,12 @@ def run(self, edit: sublime.Edit, symbol_query_input: str = "") -> None: session = self.best_session(self.capability) if session: request = Request.workspaceSymbol({"query": symbol_query_input}) - session.send_request(request, lambda r: self._handle_response( - symbol_query_input, r), self._handle_error) + config = session.config + session.send_request( + request, + lambda r: self._handle_response(config, symbol_query_input, r), + self._handle_error + ) def _format(self, s: Dict[str, Any]) -> str: file_name = os.path.basename(s['location']['uri']) @@ -227,19 +233,22 @@ def _format(self, s: Dict[str, Any]) -> str: name = "{} ({}) - {} -- {}".format(s['name'], symbol_kind, s.get('containerName', ""), file_name) return name - def _open_file(self, symbols: List[Dict[str, Any]], index: int) -> None: + def _open_file(self, config: ClientConfig, symbols: List[Dict[str, Any]], index: int) -> None: if index != -1: symbol = symbols[index] window = self.view.window() if window: - window.open_file(location_to_encoded_filename(symbol['location']), sublime.ENCODED_POSITION) + window.open_file(location_to_encoded_filename(symbol['location'], config), sublime.ENCODED_POSITION) - def _handle_response(self, query: str, response: Optional[List[Dict[str, Any]]]) -> None: + def _handle_response(self, config: ClientConfig, query: str, response: Optional[List[Dict[str, Any]]]) -> None: if response: matches = response window = self.view.window() if window: - window.show_quick_panel(list(map(self._format, matches)), lambda i: self._open_file(matches, i)) + window.show_quick_panel( + list(map(self._format, matches)), + lambda i: self._open_file(config, matches, i) + ) else: sublime.message_dialog("No matches found for query string: '{}'".format(query)) diff --git a/plugin/tooling.py b/plugin/tooling.py index f2d361ae3..ec04a9d60 100644 --- a/plugin/tooling.py +++ b/plugin/tooling.py @@ -99,7 +99,7 @@ class LspTroubleshootServerCommand(sublime_plugin.WindowCommand, TransportCallba def run(self) -> None: window = self.window active_view = window.active_view() - configs = [c for c in windows.lookup(window).get_config_manager().get_configs() if c.enabled] + configs = windows.lookup(window).get_config_manager().get_configs() config_names = [config.name for config in configs] if config_names: window.show_quick_panel(config_names, lambda index: self.on_selected(index, configs, active_view), @@ -117,6 +117,7 @@ def on_selected(self, selected_index: int, configs: List[ClientConfig], def test_run_server_async(self, config: ClientConfig, window: sublime.Window, active_view: Optional[sublime.View], output_sheet: sublime.HtmlSheet) -> None: + config = ClientConfig.from_config(config, {}) server = ServerTestRunner( config, window, lambda output, exit_code: self.update_sheet(config, active_view, output_sheet, output, exit_code)) @@ -140,7 +141,6 @@ def get_contents(self, config: ClientConfig, active_view: Optional[sublime.View] def line(s: str) -> None: lines.append(s) - line('# Troubleshooting: {}'.format(config.name)) line('## Version') diff --git a/tests/pyright-docker/Dockerfile b/tests/pyright-docker/Dockerfile new file mode 100644 index 000000000..bd19a54a2 --- /dev/null +++ b/tests/pyright-docker/Dockerfile @@ -0,0 +1,3 @@ +FROM node:14.15.3-alpine3.10 +RUN npm install -g pyright +CMD ["node", "/usr/local/lib/node_modules/pyright/langserver.index.js", "--stdio"] diff --git a/tests/setup.py b/tests/setup.py index c20b8d1c1..8b974e185 100644 --- a/tests/setup.py +++ b/tests/setup.py @@ -1,9 +1,12 @@ -from LSP.plugin.core.promise import Promise from LSP.plugin.core.logging import debug -from LSP.plugin.core.protocol import Notification, Request +from LSP.plugin.core.promise import Promise +from LSP.plugin.core.protocol import Notification +from LSP.plugin.core.protocol import Request from LSP.plugin.core.registry import windows from LSP.plugin.core.settings import client_configs -from LSP.plugin.core.types import ClientConfig, ClientStates +from LSP.plugin.core.types import ClientConfig +from LSP.plugin.core.types import ClientStates +from LSP.plugin.core.types import PathMap from LSP.plugin.core.typing import Any, Generator, List, Optional, Tuple, Union, Dict from LSP.plugin.documents import DocumentSyncListener from os import environ @@ -44,20 +47,21 @@ def result(self) -> Any: def make_stdio_test_config() -> ClientConfig: + basepath = join(sublime.packages_path(), "LSP", "tests") return ClientConfig( name="TEST", - command=["python3", join("$packages", "LSP", "tests", "server.py")], + command=["python3", join(basepath, "server.py")], selector="text.plain", - enabled=True) + enabled=True, + path_maps=[PathMap(local=basepath, remote="/workspace")] + ) def make_tcp_test_config() -> ClientConfig: - return ClientConfig( - name="TEST", - command=["python3", join("$packages", "LSP", "tests", "server.py"), "--tcp-port", "$port"], - selector="text.plain", - tcp_port=0, # select a free one for me - enabled=True) + config = make_stdio_test_config() + config.command.extend(("--tcp-port", "$port")) + config.tcp_port = 0 # select a free one for me + return config def add_config(config): diff --git a/tests/test_code_actions.py b/tests/test_code_actions.py index 53f583112..acd158899 100644 --- a/tests/test_code_actions.py +++ b/tests/test_code_actions.py @@ -2,28 +2,27 @@ from LSP.plugin.code_actions import CodeActionsByConfigName from LSP.plugin.code_actions import get_matching_kinds from LSP.plugin.core.protocol import Point, Range +from LSP.plugin.core.types import ClientConfig 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.core.views import versioned_text_document_identifier +from LSP.plugin.documents import DocumentSyncListener from setup import TextDocumentTestCase -from test_single_document import TEST_FILE_PATH -import unittest +from test_single_document import REMOTE_URI import sublime - -TEST_FILE_URI = filename_to_uri(TEST_FILE_PATH) +import unittest def edit_to_lsp(edit: Tuple[str, Range]) -> Dict[str, Any]: return {"newText": edit[0], "range": edit[1].to_lsp()} -def create_code_action_edit(view: sublime.View, version: int, edits: List[Tuple[str, Range]]) -> Dict[str, Any]: +def create_code_action_edit(config: ClientConfig, view: sublime.View, version: int, + edits: List[Tuple[str, Range]]) -> Dict[str, Any]: return { "documentChanges": [ { - "textDocument": versioned_text_document_identifier(view, version), + "textDocument": versioned_text_document_identifier(config, view, version), "edits": list(map(edit_to_lsp, edits)) } ] @@ -37,11 +36,11 @@ def create_command(command_name: str, command_args: Optional[List[Any]] = None) return result -def create_test_code_action(view: sublime.View, version: int, edits: List[Tuple[str, Range]], +def create_test_code_action(config: ClientConfig, view: sublime.View, version: int, edits: List[Tuple[str, Range]], kind: str = None) -> Dict[str, Any]: action = { "title": "Fix errors", - "edit": create_code_action_edit(view, version, edits) + "edit": create_code_action_edit(config, view, version, edits) } if kind: action['kind'] = kind @@ -67,7 +66,7 @@ def diagnostic_to_lsp(diagnostic: Tuple[str, Range]) -> Dict: "range": range.to_lsp() } return { - "uri": TEST_FILE_URI, + "uri": REMOTE_URI, "diagnostics": list(map(diagnostic_to_lsp, diagnostics)) } @@ -94,6 +93,7 @@ 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.session.config, self.view, self.view.change_count(), [(';', Range(Point(0, 11), Point(0, 11)))], @@ -121,6 +121,7 @@ def test_applies_in_two_iterations(self) -> Generator: 'textDocument/codeAction', [ create_test_code_action( + self.session.config, self.view, initial_change_count, [(';', Range(Point(0, 11), Point(0, 11)))], @@ -132,6 +133,7 @@ def test_applies_in_two_iterations(self) -> Generator: 'textDocument/codeAction', [ create_test_code_action( + self.session.config, self.view, initial_change_count + 1, [('\nAnd again!', Range(Point(0, 12), Point(0, 12)))], @@ -149,6 +151,7 @@ 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.session.config, self.view, self.view.change_count(), [(';', Range(Point(0, 11), Point(0, 11)))], @@ -173,6 +176,7 @@ 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.session.config, self.view, self.view.change_count(), [(';', Range(Point(0, 11), Point(0, 11)))], @@ -243,8 +247,10 @@ def test_requests_with_diagnostics(self) -> Generator: "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)]) + code_action_a = create_test_code_action( + self.session.config, self.view, self.view.change_count(), [("A", range_a)]) + code_action_b = create_test_code_action( + self.session.config, 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 @@ -265,8 +271,8 @@ def test_requests_with_no_diagnostics(self) -> Generator: yield from self.await_message("textDocument/didChange") range_a = Range(Point(0, 0), Point(0, 1)) range_b = Range(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)]) + code_action1 = create_test_code_action(self.session.config, self.view, 0, [("A", range_a)]) + code_action2 = create_test_code_action(self.session.config, 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 @@ -330,7 +336,7 @@ def test_requests_code_actions_on_newly_published_diagnostics(self) -> Generator 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, [ + code_action = create_test_code_action(self.session.config, self.view, 3, [ ("c", Range(Point(0, 0), Point(0, 1))), ("d", Range(Point(1, 0), Point(1, 1))), ]) @@ -343,7 +349,7 @@ def test_applies_code_action_with_matching_document_version(self) -> Generator: def test_does_not_apply_with_nonmatching_document_version(self) -> Generator: initial_content = 'a\nb' - code_action = create_test_code_action(self.view, 0, [ + code_action = create_test_code_action(self.session.config, self.view, 0, [ ("c", Range(Point(0, 0), Point(0, 1))), ("d", Range(Point(1, 0), Point(1, 1))), ]) @@ -355,7 +361,7 @@ def test_does_not_apply_with_nonmatching_document_version(self) -> Generator: 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, [ + resolved_code_action["edit"] = create_code_action_edit(self.session.config, self.view, 3, [ ("c", Range(Point(0, 0), Point(0, 1))), ("d", Range(Point(1, 0), Point(1, 1))), ]) @@ -374,7 +380,7 @@ def test_runs_command_in_resolved_code_action(self) -> Generator: 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(), [ + code_action = create_test_code_action(self.session.config, self.view, self.view.change_count(), [ ("bye", Range(Point(0, 3), Point(0, 5))), ]) yield from self.await_run_code_action(code_action) diff --git a/tests/test_configs.py b/tests/test_configs.py index 02960932c..4e9c659d3 100644 --- a/tests/test_configs.py +++ b/tests/test_configs.py @@ -89,12 +89,16 @@ def test_path_maps(self): self.assertEqual(uri, "file:///workspace3/bar.ts") uri = config.map_client_path_to_server_uri("/some/path/with/no/mapping.py") self.assertEqual(uri, "file:///some/path/with/no/mapping.py") + uri = config.map_client_path_to_server_uri("/home/user/projects/myproject") + self.assertEqual(uri, "file:///workspace") 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") path = config.map_server_uri_to_client_path("file:///workspace3/bar.ts") self.assertEqual(path, "C:/Documents/bar.ts") + path = config.map_server_uri_to_client_path("file:///workspace") + self.assertEqual(path, "/home/user/projects/myproject") # FIXME: What if the server is running on a Windows VM, # but locally we are running Linux? diff --git a/tests/test_edit.py b/tests/test_edit.py index ee6b939a1..6e9b760ba 100644 --- a/tests/test_edit.py +++ b/tests/test_edit.py @@ -2,6 +2,7 @@ from LSP.plugin.core.url import filename_to_uri from LSP.plugin.edit import temporary_setting from test_protocol import LSP_RANGE +from test_mocks import TEST_CONFIG import sublime import unittest @@ -157,30 +158,30 @@ def test_parse_from_lsp(self): class WorkspaceEditTests(unittest.TestCase): def test_parse_no_changes_from_lsp(self): - edit = parse_workspace_edit(dict()) + edit = parse_workspace_edit(TEST_CONFIG, dict()) self.assertEqual(len(edit), 0) def test_parse_changes_from_lsp(self): - edit = parse_workspace_edit(LSP_EDIT_CHANGES) + edit = parse_workspace_edit(TEST_CONFIG, LSP_EDIT_CHANGES) self.assertIn(FILENAME, edit) self.assertEqual(len(edit), 1) self.assertEqual(len(edit[FILENAME]), 1) def test_parse_document_changes_from_lsp(self): - edit = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES) + edit = parse_workspace_edit(TEST_CONFIG, LSP_EDIT_DOCUMENT_CHANGES) self.assertIn(FILENAME, edit) self.assertEqual(len(edit), 1) self.assertEqual(len(edit[FILENAME]), 1) def test_protocol_violation(self): # This should ignore the None in 'changes' - edit = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES_2) + edit = parse_workspace_edit(TEST_CONFIG, LSP_EDIT_DOCUMENT_CHANGES_2) self.assertIn(FILENAME, edit) self.assertEqual(len(edit), 1) self.assertEqual(len(edit[FILENAME]), 1) def test_no_clobbering_of_previous_edits(self): - edit = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES_3) + edit = parse_workspace_edit(TEST_CONFIG, LSP_EDIT_DOCUMENT_CHANGES_3) self.assertIn(FILENAME, edit) self.assertEqual(len(edit), 1) self.assertEqual(len(edit[FILENAME]), 5) @@ -204,7 +205,7 @@ def test_sorts_in_application_order(self): self.assertEqual(sorted_edits[2][2], 'c') def test_sorts_in_application_order2(self): - edits = parse_workspace_edit(LSP_EDIT_DOCUMENT_CHANGES_3) + edits = parse_workspace_edit(TEST_CONFIG, LSP_EDIT_DOCUMENT_CHANGES_3) sorted_edits = list(reversed(sort_by_application_order(edits[FILENAME]))) self.assertEqual(sorted_edits[0][0], (39, 26)) self.assertEqual(sorted_edits[0][1], (39, 30)) diff --git a/tests/test_session.py b/tests/test_session.py index 755b33495..84490b6dd 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -1,12 +1,12 @@ from LSP.plugin.core.collections import DottedDict from LSP.plugin.core.protocol import Error from LSP.plugin.core.protocol import TextDocumentSyncKindFull, TextDocumentSyncKindNone, TextDocumentSyncKindIncremental -from LSP.plugin.core.protocol import WorkspaceFolder -from LSP.plugin.core.sessions import Logger 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.types import WorkspaceFolder from LSP.plugin.core.typing import Any, Optional, Generator from test_mocks import TEST_CONFIG import sublime diff --git a/tests/test_single_document.py b/tests/test_single_document.py index 618ca0991..0e8c1f70b 100644 --- a/tests/test_single_document.py +++ b/tests/test_single_document.py @@ -1,5 +1,4 @@ from copy import deepcopy -from LSP.plugin.core.url import filename_to_uri from LSP.plugin.hover import _test_contents from setup import TextDocumentTestCase from setup import TIMEOUT_TIME @@ -14,10 +13,10 @@ pass SELFDIR = os.path.dirname(__file__) -TEST_FILE_PATH = os.path.join(SELFDIR, 'testfile.txt') +REMOTE_URI = "file:///workspace/testfile.txt" GOTO_RESPONSE = [ { - 'uri': filename_to_uri(TEST_FILE_PATH), + 'uri': REMOTE_URI, 'range': { 'start': @@ -77,7 +76,7 @@ def test_did_change(self) -> 'Generator': {'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) + 'uri': 'file:///workspace/testfile.txt' } }) @@ -292,7 +291,7 @@ def test_rename(self) -> 'Generator': self.insert_characters("foo\nfoo\nfoo\n") self.set_response("textDocument/rename", { 'changes': { - filename_to_uri(TEST_FILE_PATH): [ + REMOTE_URI: [ { 'range': {'start': {'character': 0, 'line': 0}, 'end': {'character': 3, 'line': 0}}, 'newText': 'bar' diff --git a/tests/test_views.py b/tests/test_views.py index 4ec16113d..6f86104da 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,5 +1,6 @@ from LSP.plugin.core.protocol import Point from LSP.plugin.core.protocol import Range +from LSP.plugin.core.types import ClientConfig from LSP.plugin.core.url import filename_to_uri from LSP.plugin.core.views import did_change from LSP.plugin.core.views import did_open @@ -35,6 +36,7 @@ def setUp(self) -> None: self.mock_file_name = "C:/Windows" if sublime.platform() == "windows" else "/etc" self.view.file_name = MagicMock(return_value=self.mock_file_name) self.view.run_command("insert", {"characters": "hello world\nfoo bar baz"}) + self.config = ClientConfig("foo", "source | text", command=["ls"]) def tearDown(self) -> None: self.view.close() @@ -46,7 +48,7 @@ def test_missing_filename(self) -> None: uri_from_view(self.view) def test_did_open(self) -> None: - self.assertEqual(did_open(self.view, "python").params, { + self.assertEqual(did_open(self.config, self.view, "python").params, { "textDocument": { "uri": filename_to_uri(self.mock_file_name), "languageId": "python", @@ -57,7 +59,7 @@ def test_did_open(self) -> None: def test_did_change_full(self) -> None: version = self.view.change_count() - self.assertEqual(did_change(self.view, version).params, { + self.assertEqual(did_change(self.config, self.view, version).params, { "textDocument": { "uri": filename_to_uri(self.mock_file_name), "version": version @@ -66,28 +68,28 @@ def test_did_change_full(self) -> None: }) def test_will_save(self) -> None: - self.assertEqual(will_save(self.view, 42).params, { + self.assertEqual(will_save(self.config, self.view.file_name() or "", 42).params, { "textDocument": {"uri": filename_to_uri(self.mock_file_name)}, "reason": 42 }) def test_will_save_wait_until(self) -> None: - self.assertEqual(will_save_wait_until(self.view, 1337).params, { + self.assertEqual(will_save_wait_until(self.config, self.view, 1337).params, { "textDocument": {"uri": filename_to_uri(self.mock_file_name)}, "reason": 1337 }) def test_did_save(self) -> None: - self.assertEqual(did_save(self.view, include_text=False).params, { + self.assertEqual(did_save(self.config, self.view, include_text=False).params, { "textDocument": {"uri": filename_to_uri(self.mock_file_name)} }) - self.assertEqual(did_save(self.view, include_text=True).params, { + self.assertEqual(did_save(self.config, self.view, include_text=True).params, { "textDocument": {"uri": filename_to_uri(self.mock_file_name)}, "text": "hello world\nfoo bar baz" }) def test_text_document_position_params(self) -> None: - self.assertEqual(text_document_position_params(self.view, 2), { + self.assertEqual(text_document_position_params(self.view, 2, self.config), { "textDocument": {"uri": filename_to_uri(self.mock_file_name)}, "position": {"line": 0, "character": 2} }) @@ -96,7 +98,7 @@ def test_text_document_formatting(self) -> None: self.view.settings = MagicMock(return_value={ "translate_tabs_to_spaces": False, "tab_size": 1234, "ensure_newline_at_eof_on_save": True}) - self.assertEqual(text_document_formatting(self.view).params, { + self.assertEqual(text_document_formatting(self.config, self.view).params, { "textDocument": {"uri": filename_to_uri(self.mock_file_name)}, "options": { "tabSize": 1234, @@ -109,7 +111,7 @@ def test_text_document_formatting(self) -> None: def test_text_document_range_formatting(self) -> None: self.view.settings = MagicMock(return_value={"tab_size": 4321}) - self.assertEqual(text_document_range_formatting(self.view, sublime.Region(0, 2)).params, { + self.assertEqual(text_document_range_formatting(self.config, self.view, sublime.Region(0, 2)).params, { "textDocument": {"uri": filename_to_uri(self.mock_file_name)}, "options": { "tabSize": 4321, @@ -142,7 +144,7 @@ def test_selection_range_params(self) -> None: self.assertEqual(len(self.view.sel()), 2) self.assertEqual(self.view.substr(self.view.sel()[0]), "hello") self.assertEqual(self.view.substr(self.view.sel()[1]), "world") - self.assertEqual(selection_range_params(self.view), { + self.assertEqual(selection_range_params(self.config, self.view), { "textDocument": {"uri": filename_to_uri(self.mock_file_name)}, "positions": [ {"line": 0, "character": 5}, @@ -277,11 +279,15 @@ def test_text2html_parses_link_in_single_quotes(self) -> None: def test_location_to_encoded_filename(self) -> None: self.assertEqual( location_to_encoded_filename( - {'uri': 'file:///foo/bar', 'range': {'start': {'line': 0, 'character': 5}}}), + {'uri': 'file:///foo/bar', 'range': {'start': {'line': 0, 'character': 5}}}, + self.config + ), '/foo/bar:1:6') self.assertEqual( location_to_encoded_filename( - {'targetUri': 'file:///foo/bar', 'targetSelectionRange': {'start': {'line': 1234, 'character': 4321}}}), + {'targetUri': 'file:///foo/bar', 'targetSelectionRange': {'start': {'line': 1234, 'character': 4321}}}, + self.config + ), '/foo/bar:1235:4322') def test_lsp_color_to_phantom(self) -> None: @@ -311,5 +317,5 @@ def test_lsp_color_to_phantom(self) -> None: def test_document_color_params(self) -> None: self.assertEqual( - document_color_params(self.view), - {"textDocument": {"uri": filename_to_uri(self.view.file_name())}}) + document_color_params(self.config, self.view), + {"textDocument": {"uri": filename_to_uri(self.view.file_name() or "")}}) diff --git a/tests/test_workspace.py b/tests/test_workspace.py index 903c1f22e..98496cc4c 100644 --- a/tests/test_workspace.py +++ b/tests/test_workspace.py @@ -1,8 +1,8 @@ +from LSP.plugin.core.types import WorkspaceFolder from LSP.plugin.core.workspace import sorted_workspace_folders, is_subpath_of -from LSP.plugin.core.protocol import WorkspaceFolder import os -import unittest import tempfile +import unittest class SortedWorkspaceFoldersTest(unittest.TestCase):