You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			315 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			315 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
""" A configurable frontend for stdio-based Language Servers
 | 
						|
"""
 | 
						|
 | 
						|
import asyncio
 | 
						|
import os
 | 
						|
import sys
 | 
						|
import traceback
 | 
						|
from typing import Dict, Text, Tuple, cast
 | 
						|
 | 
						|
# See compatibility note on `group` keyword in
 | 
						|
# https://docs.python.org/3/library/importlib.metadata.html#entry-points
 | 
						|
if sys.version_info < (3, 10):  # pragma: no cover
 | 
						|
    from importlib_metadata import entry_points
 | 
						|
else:  # pragma: no cover
 | 
						|
    from importlib.metadata import entry_points
 | 
						|
 | 
						|
from jupyter_core.paths import jupyter_config_path
 | 
						|
from jupyter_server.services.config import ConfigManager
 | 
						|
 | 
						|
try:
 | 
						|
    from jupyter_server.transutils import _i18n as _
 | 
						|
except ImportError:  # pragma: no cover
 | 
						|
    from jupyter_server.transutils import _
 | 
						|
 | 
						|
from traitlets import Bool
 | 
						|
from traitlets import Dict as Dict_
 | 
						|
from traitlets import Instance
 | 
						|
from traitlets import List as List_
 | 
						|
from traitlets import Unicode, default
 | 
						|
 | 
						|
from .constants import (
 | 
						|
    APP_CONFIG_D_SECTIONS,
 | 
						|
    EP_LISTENER_ALL_V1,
 | 
						|
    EP_LISTENER_CLIENT_V1,
 | 
						|
    EP_LISTENER_SERVER_V1,
 | 
						|
    EP_SPEC_V1,
 | 
						|
)
 | 
						|
from .schema import LANGUAGE_SERVER_SPEC_MAP
 | 
						|
from .session import LanguageServerSession
 | 
						|
from .trait_types import LoadableCallable, Schema
 | 
						|
from .types import (
 | 
						|
    KeyedLanguageServerSpecs,
 | 
						|
    LanguageServerManagerAPI,
 | 
						|
    MessageScope,
 | 
						|
    SpecBase,
 | 
						|
    SpecMaker,
 | 
						|
)
 | 
						|
 | 
						|
 | 
						|
class LanguageServerManager(LanguageServerManagerAPI):
 | 
						|
    """Manage language servers"""
 | 
						|
 | 
						|
    conf_d_language_servers = Schema(  # type:ignore[assignment]
 | 
						|
        validator=LANGUAGE_SERVER_SPEC_MAP,
 | 
						|
        help=_("extra language server specs, keyed by implementation, from conf.d"),
 | 
						|
    )  # type: KeyedLanguageServerSpecs
 | 
						|
 | 
						|
    language_servers = Schema(  # type:ignore[assignment]
 | 
						|
        validator=LANGUAGE_SERVER_SPEC_MAP,
 | 
						|
        help=_("a dict of language server specs, keyed by implementation"),
 | 
						|
    ).tag(
 | 
						|
        config=True
 | 
						|
    )  # type: KeyedLanguageServerSpecs
 | 
						|
 | 
						|
    autodetect: bool = Bool(  # type:ignore[assignment]
 | 
						|
        True, help=_("try to find known language servers in sys.prefix (and elsewhere)")
 | 
						|
    ).tag(config=True)
 | 
						|
 | 
						|
    sessions: Dict[Tuple[Text], LanguageServerSession] = (
 | 
						|
        Dict_(  # type:ignore[assignment]
 | 
						|
            trait=Instance(LanguageServerSession),
 | 
						|
            default_value={},
 | 
						|
            help="sessions keyed by language server name",
 | 
						|
        )
 | 
						|
    )
 | 
						|
 | 
						|
    virtual_documents_dir = Unicode(
 | 
						|
        help="""Path to virtual documents relative to the content manager root
 | 
						|
        directory.
 | 
						|
 | 
						|
        Its default value can be set with JP_LSP_VIRTUAL_DIR and fallback to
 | 
						|
        '.virtual_documents'.
 | 
						|
        """
 | 
						|
    ).tag(config=True)
 | 
						|
 | 
						|
    _ready = Bool(
 | 
						|
        help="""Whether the manager has been initialized""", default_value=False
 | 
						|
    )
 | 
						|
 | 
						|
    all_listeners = List_(  # type:ignore[var-annotated]
 | 
						|
        trait=LoadableCallable  # type:ignore[arg-type]
 | 
						|
    ).tag(config=True)
 | 
						|
    server_listeners = List_(  # type:ignore[var-annotated]
 | 
						|
        trait=LoadableCallable  # type:ignore[arg-type]
 | 
						|
    ).tag(config=True)
 | 
						|
    client_listeners = List_(  # type:ignore[var-annotated]
 | 
						|
        trait=LoadableCallable  # type:ignore[arg-type]
 | 
						|
    ).tag(config=True)
 | 
						|
 | 
						|
    @default("language_servers")
 | 
						|
    def _default_language_servers(self):
 | 
						|
        return {}
 | 
						|
 | 
						|
    @default("virtual_documents_dir")
 | 
						|
    def _default_virtual_documents_dir(self):
 | 
						|
        return os.getenv("JP_LSP_VIRTUAL_DIR", None) or ".virtual_documents"
 | 
						|
 | 
						|
    @default("conf_d_language_servers")
 | 
						|
    def _default_conf_d_language_servers(self) -> KeyedLanguageServerSpecs:
 | 
						|
        language_servers: KeyedLanguageServerSpecs = {}
 | 
						|
 | 
						|
        manager = ConfigManager(read_config_path=jupyter_config_path())
 | 
						|
 | 
						|
        for app in APP_CONFIG_D_SECTIONS:
 | 
						|
            language_servers.update(
 | 
						|
                **manager.get(f"jupyter{app}config")
 | 
						|
                .get(self.__class__.__name__, {})
 | 
						|
                .get("language_servers", {})
 | 
						|
            )
 | 
						|
 | 
						|
        return language_servers
 | 
						|
 | 
						|
    def __init__(self, **kwargs: Dict):
 | 
						|
        """Before starting, perform all necessary configuration"""
 | 
						|
        self.all_language_servers: KeyedLanguageServerSpecs = {}
 | 
						|
        self._language_servers_from_config: KeyedLanguageServerSpecs = {}
 | 
						|
        super().__init__(**kwargs)
 | 
						|
 | 
						|
    def initialize(self, *args, **kwargs):
 | 
						|
        self.init_language_servers()
 | 
						|
        self.init_listeners()
 | 
						|
        self.init_sessions()
 | 
						|
        self._ready = True
 | 
						|
 | 
						|
    async def ready(self):
 | 
						|
        while not self._ready:  # pragma: no cover
 | 
						|
            await asyncio.sleep(0.1)
 | 
						|
        return True
 | 
						|
 | 
						|
    def init_language_servers(self) -> None:
 | 
						|
        """determine the final language server configuration."""
 | 
						|
        # copy the language servers before anybody monkeys with them
 | 
						|
        self._language_servers_from_config = dict(self.language_servers)
 | 
						|
        self.language_servers = self._collect_language_servers(only_installed=True)
 | 
						|
        self.all_language_servers = self._collect_language_servers(only_installed=False)
 | 
						|
 | 
						|
    def _collect_language_servers(
 | 
						|
        self, only_installed: bool
 | 
						|
    ) -> KeyedLanguageServerSpecs:
 | 
						|
        language_servers: KeyedLanguageServerSpecs = {}
 | 
						|
 | 
						|
        language_servers_from_config = dict(self._language_servers_from_config)
 | 
						|
        language_servers_from_config.update(self.conf_d_language_servers)
 | 
						|
 | 
						|
        if self.autodetect:
 | 
						|
            language_servers.update(
 | 
						|
                self._autodetect_language_servers(only_installed=only_installed)
 | 
						|
            )
 | 
						|
 | 
						|
        # restore config
 | 
						|
        language_servers.update(language_servers_from_config)
 | 
						|
 | 
						|
        # coalesce the servers, allowing a user to opt-out by specifying `[]`
 | 
						|
        return {key: spec for key, spec in language_servers.items() if spec.get("argv")}
 | 
						|
 | 
						|
    def init_sessions(self):
 | 
						|
        """create, but do not initialize all sessions"""
 | 
						|
        sessions = {}
 | 
						|
        for language_server, spec in self.language_servers.items():
 | 
						|
            sessions[language_server] = LanguageServerSession(
 | 
						|
                language_server=language_server, spec=spec, parent=self
 | 
						|
            )
 | 
						|
        self.sessions = sessions
 | 
						|
 | 
						|
    def init_listeners(self):
 | 
						|
        """register traitlets-configured listeners"""
 | 
						|
 | 
						|
        scopes = {
 | 
						|
            MessageScope.ALL: [self.all_listeners, EP_LISTENER_ALL_V1],
 | 
						|
            MessageScope.CLIENT: [self.client_listeners, EP_LISTENER_CLIENT_V1],
 | 
						|
            MessageScope.SERVER: [self.server_listeners, EP_LISTENER_SERVER_V1],
 | 
						|
        }
 | 
						|
        for scope, trt_ep in scopes.items():
 | 
						|
            listeners, entry_point = trt_ep
 | 
						|
 | 
						|
            for ept in entry_points(group=entry_point):  # pragma: no cover
 | 
						|
                try:
 | 
						|
                    listeners.append(ept.load())
 | 
						|
                except Exception as err:
 | 
						|
                    self.log.warning("Failed to load entry point %s: %s", ept.name, err)
 | 
						|
 | 
						|
            for listener in listeners:
 | 
						|
                self.__class__.register_message_listener(scope=scope.value)(listener)
 | 
						|
 | 
						|
    def subscribe(self, handler):
 | 
						|
        """subscribe a handler to session, or sta"""
 | 
						|
        session = self.sessions.get(handler.language_server)
 | 
						|
 | 
						|
        if session is None:
 | 
						|
            self.log.error(
 | 
						|
                "[{}] no session: handler subscription failed".format(
 | 
						|
                    handler.language_server
 | 
						|
                )
 | 
						|
            )
 | 
						|
            return
 | 
						|
 | 
						|
        session.handlers = set([handler]) | session.handlers
 | 
						|
 | 
						|
    async def on_client_message(self, message, handler):
 | 
						|
        await self.wait_for_listeners(
 | 
						|
            MessageScope.CLIENT, message, handler.language_server
 | 
						|
        )
 | 
						|
        session = self.sessions.get(handler.language_server)
 | 
						|
 | 
						|
        if session is None:
 | 
						|
            self.log.error(
 | 
						|
                "[{}] no session: client message dropped".format(
 | 
						|
                    handler.language_server
 | 
						|
                )
 | 
						|
            )
 | 
						|
            return
 | 
						|
 | 
						|
        session.write(message)
 | 
						|
 | 
						|
    async def on_server_message(self, message, session):
 | 
						|
        language_servers = [
 | 
						|
            ls_key for ls_key, sess in self.sessions.items() if sess == session
 | 
						|
        ]
 | 
						|
 | 
						|
        for language_servers in language_servers:
 | 
						|
            await self.wait_for_listeners(
 | 
						|
                MessageScope.SERVER, message, language_servers
 | 
						|
            )
 | 
						|
 | 
						|
        for handler in session.handlers:
 | 
						|
            handler.write_message(message)
 | 
						|
 | 
						|
    def unsubscribe(self, handler):
 | 
						|
        session = self.sessions.get(handler.language_server)
 | 
						|
 | 
						|
        if session is None:
 | 
						|
            self.log.error(
 | 
						|
                "[{}] no session: handler unsubscription failed".format(
 | 
						|
                    handler.language_server
 | 
						|
                )
 | 
						|
            )
 | 
						|
            return
 | 
						|
 | 
						|
        session.handlers = [h for h in session.handlers if h != handler]
 | 
						|
 | 
						|
    def _autodetect_language_servers(self, only_installed: bool):
 | 
						|
        _entry_points = None
 | 
						|
 | 
						|
        try:
 | 
						|
            _entry_points = entry_points(group=EP_SPEC_V1)
 | 
						|
        except Exception:  # pragma: no cover
 | 
						|
            self.log.exception("Failed to load entry_points")
 | 
						|
 | 
						|
        skipped_servers = []
 | 
						|
 | 
						|
        for ep in _entry_points or []:
 | 
						|
            try:
 | 
						|
                spec_finder: SpecMaker = ep.load()
 | 
						|
            except Exception as err:  # pragma: no cover
 | 
						|
                self.log.warning(
 | 
						|
                    _("Failed to load language server spec finder `{}`: \n{}").format(
 | 
						|
                        ep.name, err
 | 
						|
                    )
 | 
						|
                )
 | 
						|
                continue
 | 
						|
 | 
						|
            try:
 | 
						|
                if only_installed:
 | 
						|
                    if hasattr(spec_finder, "is_installed"):
 | 
						|
                        spec_finder_from_base = cast(SpecBase, spec_finder)
 | 
						|
                        if not spec_finder_from_base.is_installed(self):
 | 
						|
                            skipped_servers.append(ep.name)
 | 
						|
                            continue
 | 
						|
                specs = spec_finder(self) or {}
 | 
						|
            except Exception as err:  # pragma: no cover
 | 
						|
                self.log.warning(
 | 
						|
                    _(
 | 
						|
                        "Failed to fetch commands from language server spec finder"
 | 
						|
                        " `{}`:\n{}"
 | 
						|
                    ).format(ep.name, err)
 | 
						|
                )
 | 
						|
                traceback.print_exc()
 | 
						|
 | 
						|
                continue
 | 
						|
 | 
						|
            errors = list(LANGUAGE_SERVER_SPEC_MAP.iter_errors(specs))
 | 
						|
 | 
						|
            if errors:  # pragma: no cover
 | 
						|
                self.log.warning(
 | 
						|
                    _(
 | 
						|
                        "Failed to validate commands from language server spec finder"
 | 
						|
                        " `{}`:\n{}"
 | 
						|
                    ).format(ep.name, errors)
 | 
						|
                )
 | 
						|
                continue
 | 
						|
 | 
						|
            for key, spec in specs.items():
 | 
						|
                yield key, spec
 | 
						|
 | 
						|
        if skipped_servers:
 | 
						|
            self.log.info(
 | 
						|
                _("Skipped non-installed server(s): {}").format(
 | 
						|
                    ", ".join(skipped_servers)
 | 
						|
                )
 | 
						|
            )
 | 
						|
 | 
						|
 | 
						|
# the listener decorator
 | 
						|
lsp_message_listener = LanguageServerManager.register_message_listener  # noqa
 |