Skip to content

Commit

Permalink
feat: integrate lsp_utils
Browse files Browse the repository at this point in the history
  • Loading branch information
rchl committed Jan 3, 2024
1 parent 36871c2 commit b0a15cc
Show file tree
Hide file tree
Showing 35 changed files with 3,902 additions and 5 deletions.
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

0 comments on commit b0a15cc

Please sign in to comment.