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

feat: integrate lsp_utils #2388

Closed
wants to merge 1 commit 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
17 changes: 17 additions & 0 deletions LSP.sublime-settings
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,23 @@
// "region",
],

// --- LSP API-------------------------------------------------------------------------

// Specifies the type and priority of the Node.js installation that should be used for Node.js-based servers
// using the NpmClientHandler.
// The allowed values are:
// - 'system' - a Node.js runtime found on the PATH
// - 'local' - a Node.js runtime managed by LSP that doesn't affect the system
// The order in which the values are specified determines which one is tried first, with the later one being
// used as a fallback.
// You can also specify just a single value to disable the fallback.
"nodejs_runtime": ["system", "local"],

// Uses Node.js runtime from the Electron package rather than the official distribution. This has the benefit of
// lower memory usage due to it having the pointer compression (https://v8.dev/blog/pointer-compression) enabled.
// Only relevant when using `local` variant of `nodejs_runtime`.
"local_use_electron": false,

// --- Debugging ----------------------------------------------------------------------

// Show verbose debug messages in the sublime console.
Expand Down
21 changes: 21 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Minimal makefile for Sphinx documentation

# You can set these variables from the command line.
SOURCEDIR = api
BUILDDIR = dist

.PHONY: clean

install:
pip3 install sphinx sphinx-rtd-theme sphinx-autodoc-typehints ghp-import mkdocs mkdocs-material mkdocs-redirects
# sed -i -E 's/sublime\.DRAW_[A-Z_]*/0/g' source/modules/LSP/plugin/core/views.py
# sed -i -E 's/sublime\.HIDE_ON_MINIMAP/0/g' source/modules/LSP/plugin/core/views.py

build:
sphinx-build -M html "$(SOURCEDIR)" "$(BUILDDIR)"

deploy:
ghp-import --no-jekyll --push --force html

clean:
rm -rf doctrees html
25 changes: 25 additions & 0 deletions api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from ._client_handler import ClientHandler
from ._client_handler import notification_handler
from ._client_handler import request_handler
from .api_wrapper_interface import ApiWrapperInterface
from .generic_client_handler import GenericClientHandler
from .node_runtime import NodeRuntime
from .npm_client_handler import NpmClientHandler
from .server_npm_resource import ServerNpmResource
from .server_pip_resource import ServerPipResource
from .server_resource_interface import ServerResourceInterface
from .server_resource_interface import ServerStatus

__all__ = [
'ApiWrapperInterface',
'ClientHandler',
'GenericClientHandler',
'NodeRuntime',
'NpmClientHandler',
'ServerResourceInterface',
'ServerStatus',
'ServerNpmResource',
'ServerPipResource',
'notification_handler',
'request_handler',
]
9 changes: 9 additions & 0 deletions api/_client_handler/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .abstract_plugin import ClientHandler
from .api_decorator import notification_handler
from .api_decorator import request_handler

__all__ = [
'ClientHandler',
'notification_handler',
'request_handler',
]
155 changes: 155 additions & 0 deletions api/_client_handler/abstract_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
from .._util import weak_method
from ..api_wrapper_interface import ApiWrapperInterface
from ..server_resource_interface import ServerStatus
from .api_decorator import register_decorated_handlers
from .interface import ClientHandlerInterface
from functools import partial
from LSP.plugin import AbstractPlugin
from LSP.plugin import ClientConfig
from LSP.plugin import Notification
from LSP.plugin import register_plugin
from LSP.plugin import Request
from LSP.plugin import Response
from LSP.plugin import Session
from LSP.plugin import unregister_plugin
from LSP.plugin import WorkspaceFolder
from LSP.plugin.core.rpc import method2attr
from LSP.plugin.core.typing import Any, Callable, Dict, List, Optional, Tuple, TypedDict
from os import path
from weakref import ref
import sublime

__all__ = ['ClientHandler']

LanguagesDict = TypedDict('LanguagesDict', {
'document_selector': Optional[str],
'languageId': Optional[str],
'scopes': Optional[List[str]],
'syntaxes': Optional[List[str]],
}, total=False)
ApiNotificationHandler = Callable[[Any], None]
ApiRequestHandler = Callable[[Any, Callable[[Any], None]], None]


class ApiWrapper(ApiWrapperInterface):
def __init__(self, plugin: 'ref[AbstractPlugin]'):
self.__plugin = plugin

def __session(self) -> Optional[Session]:
plugin = self.__plugin()
return plugin.weaksession() if plugin else None

# --- ApiWrapperInterface -----------------------------------------------------------------------------------------

def on_notification(self, method: str, handler: ApiNotificationHandler) -> None:
def handle_notification(weak_handler: ApiNotificationHandler, params: Any) -> None:
weak_handler(params)

plugin = self.__plugin()
if plugin:
setattr(plugin, method2attr(method), partial(handle_notification, weak_method(handler)))

def on_request(self, method: str, handler: ApiRequestHandler) -> None:
def send_response(request_id: Any, result: Any) -> None:
session = self.__session()
if session:
session.send_response(Response(request_id, result))

def on_response(weak_handler: ApiRequestHandler, params: Any, request_id: Any) -> None:
weak_handler(params, lambda result: send_response(request_id, result))

plugin = self.__plugin()
if plugin:
setattr(plugin, method2attr(method), partial(on_response, weak_method(handler)))

def send_notification(self, method: str, params: Any) -> None:
session = self.__session()
if session:
session.send_notification(Notification(method, params))

def send_request(self, method: str, params: Any, handler: Callable[[Any, bool], None]) -> None:
session = self.__session()
if session:
session.send_request(
Request(method, params), lambda result: handler(result, False), lambda result: handler(result, True))
else:
handler(None, True)


class ClientHandler(AbstractPlugin, ClientHandlerInterface):
"""
The base class for creating an LSP plugin.
"""

# --- AbstractPlugin handlers -------------------------------------------------------------------------------------

@classmethod
def name(cls) -> str:
return cls.get_displayed_name()

@classmethod
def configuration(cls) -> Tuple[sublime.Settings, str]:
return cls.read_settings()

@classmethod
def additional_variables(cls) -> Dict[str, str]:
return cls.get_additional_variables()

@classmethod
def needs_update_or_installation(cls) -> bool:
if cls.manages_server():
server = cls.get_server()
return bool(server and server.needs_installation())
return False

@classmethod
def install_or_update(cls) -> None:
server = cls.get_server()
if server:
server.install_or_update()

@classmethod
def can_start(cls, window: sublime.Window, initiating_view: sublime.View,
workspace_folders: List[WorkspaceFolder], configuration: ClientConfig) -> Optional[str]:
if cls.manages_server():
server = cls.get_server()
if not server or server.get_status() == ServerStatus.ERROR:
return "{}: Error installing server dependencies.".format(cls.package_name)
if server.get_status() != ServerStatus.READY:
return "{}: Server installation in progress...".format(cls.package_name)
message = cls.is_allowed_to_start(window, initiating_view, workspace_folders, configuration)
if message:
return message
# Lazily update command after server has initialized if not set manually by the user.
if not configuration.command:
configuration.command = cls.get_command()
return None

@classmethod
def on_pre_start(cls, window: sublime.Window, initiating_view: sublime.View,
workspace_folders: List[WorkspaceFolder], configuration: ClientConfig) -> Optional[str]:
extra_paths = path.pathsep.join(cls.get_additional_paths())
if extra_paths:
original_path = configuration.env.get('PATH') or ''
if isinstance(original_path, list):
original_path = path.pathsep.join(original_path)
configuration.env['PATH'] = path.pathsep.join([extra_paths, original_path])
return None

# --- ClientHandlerInterface --------------------------------------------------------------------------------------

@classmethod
def setup(cls) -> None:
register_plugin(cls)

@classmethod
def cleanup(cls) -> None:
unregister_plugin(cls)

# --- Internals ---------------------------------------------------------------------------------------------------

def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
api = ApiWrapper(ref(self)) # type: ignore
register_decorated_handlers(self, api)
self.on_ready(api)
86 changes: 86 additions & 0 deletions api/_client_handler/api_decorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from ..api_wrapper_interface import ApiWrapperInterface
from .interface import ClientHandlerInterface
from LSP.plugin.core.typing import Any, Callable, List, Optional, TypeVar, Union
import inspect

__all__ = [
"notification_handler",
"request_handler",
"register_decorated_handlers",
]

T = TypeVar('T')
# the first argument is always "self"
NotificationHandler = Callable[[Any, Any], None]
RequestHandler = Callable[[Any, Any, Callable[[Any], None]], None]
MessageMethods = Union[str, List[str]]

_HANDLER_MARKS = {
"notification": "__handle_notification_message_methods",
"request": "__handle_request_message_methods",
}


def notification_handler(notification_methods: MessageMethods) -> Callable[[NotificationHandler], NotificationHandler]:
"""
Marks the decorated function as a "notification" message handler.

On server sending the notification, the decorated function will be called with the `params` argument which contains
the payload.
"""

return _create_handler("notification", notification_methods)


def request_handler(request_methods: MessageMethods) -> Callable[[RequestHandler], RequestHandler]:
"""
Marks the decorated function as a "request" message handler.

On server sending the request, the decorated function will be called with two arguments (`params` and `respond`).
The first argument (`params`) is the payload of the request and the second argument (`respond`) is the function that
must be used to respond to the request. The `respond` function takes any data that should be sent back to the
server.
"""

return _create_handler("request", request_methods)


def _create_handler(client_event: str, message_methods: MessageMethods) -> Callable[[T], T]:
""" Marks the decorated function as a message handler. """

message_methods = [message_methods] if isinstance(message_methods, str) else message_methods

def decorator(func: T) -> T:
setattr(func, _HANDLER_MARKS[client_event], message_methods)
return func

return decorator


def register_decorated_handlers(client_handler: ClientHandlerInterface, api: ApiWrapperInterface) -> None:
"""
Register decorator-style custom message handlers.

This method works as following:

1. Scan through all methods of `client_handler`.
2. If a method is decorated, it will have a "handler mark" attribute which is set by the decorator.
3. Register the method with wanted message methods, which are stored in the "handler mark" attribute.

:param client_handler: The instance of the client handler.
:param api: The API instance for interacting with the server.
"""
for _, func in inspect.getmembers(client_handler, predicate=inspect.isroutine):
for client_event, handler_mark in _HANDLER_MARKS.items():
message_methods = getattr(func, handler_mark, None) # type: Optional[List[str]]
if message_methods is None:
continue

event_registrator = getattr(api, "on_" + client_event, None)
if callable(event_registrator):
for message_method in message_methods:
event_registrator(message_method, func)

# it makes no sense that a handler handles both "notification" and "request"
# so we do early break once we've registered a handler
break
Loading