Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Path mapping Part II #1527

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions plugin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__
Expand All @@ -21,14 +21,14 @@
'ClientConfig',
'css',
'DottedDict',
'filename_to_uri',
'filename_to_uri', # DEPRECATED: Use ClientConfig.map_client_path_to_server_uri instead
'Notification',
'register_plugin',
'Request',
'Response',
'Session',
'SessionBufferProtocol',
'unregister_plugin',
'uri_to_filename',
'uri_to_filename', # DEPRECATED: Use ClientConfig.map_server_uri_to_client_path instead
'WorkspaceFolder',
]
5 changes: 3 additions & 2 deletions plugin/code_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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:
Expand Down
10 changes: 6 additions & 4 deletions plugin/core/edit.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand All @@ -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


Expand Down
6 changes: 3 additions & 3 deletions plugin/core/open.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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":
Expand Down
38 changes: 0 additions & 38 deletions plugin/core/protocol.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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())
35 changes: 19 additions & 16 deletions plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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('_')]
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand All @@ -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"""
Expand Down Expand Up @@ -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))

Expand Down
1 change: 1 addition & 0 deletions plugin/core/transports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
61 changes: 55 additions & 6 deletions plugin/core/types.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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


Expand All @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
Loading