class LanguageServerManager(LanguageServerManagerAPI): """Manage language servers""" conf_d_language_servers = Schema( validator=LANGUAGE_SERVER_SPEC_MAP, help=_( "extra language server specs, keyed by implementation, from conf.d" ), ) # type: KeyedLanguageServerSpecs language_servers = Schema( validator=LANGUAGE_SERVER_SPEC_MAP, help=_("a dict of language server specs, keyed by implementation"), ).tag(config=True) # type: KeyedLanguageServerSpecs autodetect = Bool( True, help=_( "try to find known language servers in sys.prefix (and elsewhere)") ).tag(config=True) # type: bool sessions = Dict_( trait=Instance(LanguageServerSession), default_value={}, help="sessions keyed by language server name", ) # type: Dict[Tuple[Text], LanguageServerSession] 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) all_listeners = List_(trait=LoadableCallable).tag(config=True) server_listeners = List_(trait=LoadableCallable).tag(config=True) client_listeners = List_(trait=LoadableCallable).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", ".virtual_documents") @default("conf_d_language_servers") def _default_conf_d_language_servers(self): language_servers = {} # type: 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): """Before starting, perform all necessary configuration""" self.all_language_servers: KeyedLanguageServerSpecs = {} self._language_servers_from_config = {} super().__init__(**kwargs) def initialize(self, *args, **kwargs): self.init_language_servers() self.init_listeners() self.init_sessions() 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 ep_name, ept in entrypoints.get_group_named( entry_point).items(): # pragma: no cover try: listeners.append(ept.load()) except Exception as err: self.log.warning("Failed to load entry point %s: %s", ep_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 = {} try: entry_points = entrypoints.get_group_named(EP_SPEC_V1) except Exception: # pragma: no cover self.log.exception("Failed to load entry_points") skipped_servers = [] for ep_name, ep in entry_points.items(): try: spec_finder = ep.load() # type: SpecMaker 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)))
class LanguageServerManager(LanguageServerManagerAPI): """ Manage language servers """ language_servers = Schema( validator=LANGUAGE_SERVER_SPEC_MAP, help=_("a dict of language server specs, keyed by implementation"), ).tag(config=True) # type: KeyedLanguageServerSpecs autodetect = Bool( True, help=_( "try to find known language servers in sys.prefix (and elsewhere)") ).tag(config=True) # type: bool sessions = Dict_( trait=Instance(LanguageServerSession), default_value={}, help="sessions keyed by languages served", ) # type: Dict[Tuple[Text], LanguageServerSession] all_listeners = List_(trait=LoadableCallable).tag(config=True) server_listeners = List_(trait=LoadableCallable).tag(config=True) client_listeners = List_(trait=LoadableCallable).tag(config=True) @default("language_servers") def _default_language_servers(self): return {} def __init__(self, **kwargs): """ Before starting, perform all necessary configuration """ super().__init__(**kwargs) def initialize(self, *args, **kwargs): self.init_language_servers() self.init_listeners() self.init_sessions() def init_language_servers(self) -> None: """ determine the final language server configuration. """ language_servers = {} # type: KeyedLanguageServerSpecs # copy the language servers before anybody monkeys with them language_servers_from_config = dict(self.language_servers) if self.autodetect: language_servers.update(self._autodetect_language_servers()) # restore config language_servers.update(language_servers_from_config) # coalesce the servers, allowing a user to opt-out by specifying `[]` self.language_servers = { key: spec for key, spec in language_servers.items() if spec.get("argv") and spec.get("languages") } def init_sessions(self): """ create, but do not initialize all sessions """ sessions = {} for spec in self.language_servers.values(): sessions[tuple(sorted(spec["languages"]))] = LanguageServerSession( 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 pkg_resources.iter_entry_points( entry_point): # pragma: no cover try: listeners.append(entry_point.load()) except Exception as err: self.log.warning("Failed to load entry point %s: %s", ept, 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 """ sessions = [] for languages, candidate_session in self.sessions.items(): if handler.language in languages: sessions.append(candidate_session) if sessions: for session in sessions: session.handlers = set([handler]) | session.handlers async def on_client_message(self, message, handler): await self.wait_for_listeners(MessageScope.CLIENT, message, [handler.language]) for session in self.sessions_for_handler(handler): session.write(message) async def on_server_message(self, message, session): await self.wait_for_listeners(MessageScope.SERVER, message, session.spec["languages"]) for handler in session.handlers: handler.write_message(message) def unsubscribe(self, handler): for session in self.sessions_for_handler(handler): session.handlers = [h for h in session.handlers if h != handler] def sessions_for_handler(self, handler): for session in self.sessions.values(): if handler in session.handlers: yield session def _autodetect_language_servers(self): entry_points = [] try: entry_points = list(pkg_resources.iter_entry_points(EP_SPEC_V1)) except Exception: # pragma: no cover self.log.exception("Failed to load entry_points") for ep in entry_points: try: spec_finder = ep.load() # type: SpecMaker except Exception as err: # pragma: no cover self.log.warn( _("Failed to load language server spec finder `{}`: \n{}"). format(ep.name, err)) continue try: specs = spec_finder(self) except Exception as err: # pragma: no cover self.log.warning( _("Failed to fetch commands from language server spec finder" "`{}`:\n{}").format(ep.name, err)) 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
class LanguageServerManagerAPI(LoggingConfigurable, HasListeners): """Public API that can be used for python-based spec finders and listeners""" nodejs = Unicode(help=_("path to nodejs executable")).tag(config=True) node_roots = List_( [], help=_("absolute paths in which to seek node_modules")).tag( config=True) extra_node_roots = List_( [], help=_("additional absolute paths to seek node_modules first")).tag( config=True) def find_node_module(self, *path_frag): """look through the node_module roots to find the given node module""" all_roots = self.extra_node_roots + self.node_roots found = None for candidate_root in all_roots: candidate = pathlib.Path(candidate_root, "node_modules", *path_frag) self.log.debug("Checking for %s", candidate) if candidate.exists(): found = str(candidate) break if found is None: # pragma: no cover self.log.debug("{} not found in node_modules of {}".format( pathlib.Path(*path_frag), all_roots)) return found @default("nodejs") def _default_nodejs(self): return (shutil.which("node") or shutil.which("nodejs") or shutil.which("nodejs.exe")) @default("node_roots") def _default_node_roots(self): """get the "usual suspects" for where `node_modules` may be found - where this was launch (usually the same as NotebookApp.notebook_dir) - the JupyterLab staging folder (if available) - wherever conda puts it - wherever some other conventions put it """ # check where the server was started first roots = [pathlib.Path.cwd()] # try jupyterlab staging next try: from jupyterlab import commands roots += [pathlib.Path(commands.get_app_dir()) / "staging"] except ImportError: # pragma: no cover pass # conda puts stuff in $PREFIX/lib on POSIX systems roots += [pathlib.Path(sys.prefix) / "lib"] # ... but right in %PREFIX% on nt roots += [pathlib.Path(sys.prefix)] return roots