manager.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. """ A configurable frontend for stdio-based Language Servers
  2. """
  3. import asyncio
  4. import os
  5. import sys
  6. import traceback
  7. from typing import Dict, Text, Tuple, cast
  8. # See compatibility note on `group` keyword in
  9. # https://docs.python.org/3/library/importlib.metadata.html#entry-points
  10. if sys.version_info < (3, 10): # pragma: no cover
  11. from importlib_metadata import entry_points
  12. else: # pragma: no cover
  13. from importlib.metadata import entry_points
  14. from jupyter_core.paths import jupyter_config_path
  15. from jupyter_server.services.config import ConfigManager
  16. try:
  17. from jupyter_server.transutils import _i18n as _
  18. except ImportError: # pragma: no cover
  19. from jupyter_server.transutils import _
  20. from traitlets import Bool
  21. from traitlets import Dict as Dict_
  22. from traitlets import Instance
  23. from traitlets import List as List_
  24. from traitlets import Unicode, default
  25. from .constants import (
  26. APP_CONFIG_D_SECTIONS,
  27. EP_LISTENER_ALL_V1,
  28. EP_LISTENER_CLIENT_V1,
  29. EP_LISTENER_SERVER_V1,
  30. EP_SPEC_V1,
  31. )
  32. from .schema import LANGUAGE_SERVER_SPEC_MAP
  33. from .session import LanguageServerSession
  34. from .trait_types import LoadableCallable, Schema
  35. from .types import (
  36. KeyedLanguageServerSpecs,
  37. LanguageServerManagerAPI,
  38. MessageScope,
  39. SpecBase,
  40. SpecMaker,
  41. )
  42. class LanguageServerManager(LanguageServerManagerAPI):
  43. """Manage language servers"""
  44. conf_d_language_servers = Schema( # type:ignore[assignment]
  45. validator=LANGUAGE_SERVER_SPEC_MAP,
  46. help=_("extra language server specs, keyed by implementation, from conf.d"),
  47. ) # type: KeyedLanguageServerSpecs
  48. language_servers = Schema( # type:ignore[assignment]
  49. validator=LANGUAGE_SERVER_SPEC_MAP,
  50. help=_("a dict of language server specs, keyed by implementation"),
  51. ).tag(
  52. config=True
  53. ) # type: KeyedLanguageServerSpecs
  54. autodetect: bool = Bool( # type:ignore[assignment]
  55. True, help=_("try to find known language servers in sys.prefix (and elsewhere)")
  56. ).tag(config=True)
  57. sessions: Dict[Tuple[Text], LanguageServerSession] = (
  58. Dict_( # type:ignore[assignment]
  59. trait=Instance(LanguageServerSession),
  60. default_value={},
  61. help="sessions keyed by language server name",
  62. )
  63. )
  64. virtual_documents_dir = Unicode(
  65. help="""Path to virtual documents relative to the content manager root
  66. directory.
  67. Its default value can be set with JP_LSP_VIRTUAL_DIR and fallback to
  68. '.virtual_documents'.
  69. """
  70. ).tag(config=True)
  71. _ready = Bool(
  72. help="""Whether the manager has been initialized""", default_value=False
  73. )
  74. all_listeners = List_( # type:ignore[var-annotated]
  75. trait=LoadableCallable # type:ignore[arg-type]
  76. ).tag(config=True)
  77. server_listeners = List_( # type:ignore[var-annotated]
  78. trait=LoadableCallable # type:ignore[arg-type]
  79. ).tag(config=True)
  80. client_listeners = List_( # type:ignore[var-annotated]
  81. trait=LoadableCallable # type:ignore[arg-type]
  82. ).tag(config=True)
  83. @default("language_servers")
  84. def _default_language_servers(self):
  85. return {}
  86. @default("virtual_documents_dir")
  87. def _default_virtual_documents_dir(self):
  88. return os.getenv("JP_LSP_VIRTUAL_DIR", None) or ".virtual_documents"
  89. @default("conf_d_language_servers")
  90. def _default_conf_d_language_servers(self) -> KeyedLanguageServerSpecs:
  91. language_servers: KeyedLanguageServerSpecs = {}
  92. manager = ConfigManager(read_config_path=jupyter_config_path())
  93. for app in APP_CONFIG_D_SECTIONS:
  94. language_servers.update(
  95. **manager.get(f"jupyter{app}config")
  96. .get(self.__class__.__name__, {})
  97. .get("language_servers", {})
  98. )
  99. return language_servers
  100. def __init__(self, **kwargs: Dict):
  101. """Before starting, perform all necessary configuration"""
  102. self.all_language_servers: KeyedLanguageServerSpecs = {}
  103. self._language_servers_from_config: KeyedLanguageServerSpecs = {}
  104. super().__init__(**kwargs)
  105. def initialize(self, *args, **kwargs):
  106. self.init_language_servers()
  107. self.init_listeners()
  108. self.init_sessions()
  109. self._ready = True
  110. async def ready(self):
  111. while not self._ready: # pragma: no cover
  112. await asyncio.sleep(0.1)
  113. return True
  114. def init_language_servers(self) -> None:
  115. """determine the final language server configuration."""
  116. # copy the language servers before anybody monkeys with them
  117. self._language_servers_from_config = dict(self.language_servers)
  118. self.language_servers = self._collect_language_servers(only_installed=True)
  119. self.all_language_servers = self._collect_language_servers(only_installed=False)
  120. def _collect_language_servers(
  121. self, only_installed: bool
  122. ) -> KeyedLanguageServerSpecs:
  123. language_servers: KeyedLanguageServerSpecs = {}
  124. language_servers_from_config = dict(self._language_servers_from_config)
  125. language_servers_from_config.update(self.conf_d_language_servers)
  126. if self.autodetect:
  127. language_servers.update(
  128. self._autodetect_language_servers(only_installed=only_installed)
  129. )
  130. # restore config
  131. language_servers.update(language_servers_from_config)
  132. # coalesce the servers, allowing a user to opt-out by specifying `[]`
  133. return {key: spec for key, spec in language_servers.items() if spec.get("argv")}
  134. def init_sessions(self):
  135. """create, but do not initialize all sessions"""
  136. sessions = {}
  137. for language_server, spec in self.language_servers.items():
  138. sessions[language_server] = LanguageServerSession(
  139. language_server=language_server, spec=spec, parent=self
  140. )
  141. self.sessions = sessions
  142. def init_listeners(self):
  143. """register traitlets-configured listeners"""
  144. scopes = {
  145. MessageScope.ALL: [self.all_listeners, EP_LISTENER_ALL_V1],
  146. MessageScope.CLIENT: [self.client_listeners, EP_LISTENER_CLIENT_V1],
  147. MessageScope.SERVER: [self.server_listeners, EP_LISTENER_SERVER_V1],
  148. }
  149. for scope, trt_ep in scopes.items():
  150. listeners, entry_point = trt_ep
  151. for ept in entry_points(group=entry_point): # pragma: no cover
  152. try:
  153. listeners.append(ept.load())
  154. except Exception as err:
  155. self.log.warning("Failed to load entry point %s: %s", ept.name, err)
  156. for listener in listeners:
  157. self.__class__.register_message_listener(scope=scope.value)(listener)
  158. def subscribe(self, handler):
  159. """subscribe a handler to session, or sta"""
  160. session = self.sessions.get(handler.language_server)
  161. if session is None:
  162. self.log.error(
  163. "[{}] no session: handler subscription failed".format(
  164. handler.language_server
  165. )
  166. )
  167. return
  168. session.handlers = set([handler]) | session.handlers
  169. async def on_client_message(self, message, handler):
  170. await self.wait_for_listeners(
  171. MessageScope.CLIENT, message, handler.language_server
  172. )
  173. session = self.sessions.get(handler.language_server)
  174. if session is None:
  175. self.log.error(
  176. "[{}] no session: client message dropped".format(
  177. handler.language_server
  178. )
  179. )
  180. return
  181. session.write(message)
  182. async def on_server_message(self, message, session):
  183. language_servers = [
  184. ls_key for ls_key, sess in self.sessions.items() if sess == session
  185. ]
  186. for language_servers in language_servers:
  187. await self.wait_for_listeners(
  188. MessageScope.SERVER, message, language_servers
  189. )
  190. for handler in session.handlers:
  191. handler.write_message(message)
  192. def unsubscribe(self, handler):
  193. session = self.sessions.get(handler.language_server)
  194. if session is None:
  195. self.log.error(
  196. "[{}] no session: handler unsubscription failed".format(
  197. handler.language_server
  198. )
  199. )
  200. return
  201. session.handlers = [h for h in session.handlers if h != handler]
  202. def _autodetect_language_servers(self, only_installed: bool):
  203. _entry_points = None
  204. try:
  205. _entry_points = entry_points(group=EP_SPEC_V1)
  206. except Exception: # pragma: no cover
  207. self.log.exception("Failed to load entry_points")
  208. skipped_servers = []
  209. for ep in _entry_points or []:
  210. try:
  211. spec_finder: SpecMaker = ep.load()
  212. except Exception as err: # pragma: no cover
  213. self.log.warning(
  214. _("Failed to load language server spec finder `{}`: \n{}").format(
  215. ep.name, err
  216. )
  217. )
  218. continue
  219. try:
  220. if only_installed:
  221. if hasattr(spec_finder, "is_installed"):
  222. spec_finder_from_base = cast(SpecBase, spec_finder)
  223. if not spec_finder_from_base.is_installed(self):
  224. skipped_servers.append(ep.name)
  225. continue
  226. specs = spec_finder(self) or {}
  227. except Exception as err: # pragma: no cover
  228. self.log.warning(
  229. _(
  230. "Failed to fetch commands from language server spec finder"
  231. " `{}`:\n{}"
  232. ).format(ep.name, err)
  233. )
  234. traceback.print_exc()
  235. continue
  236. errors = list(LANGUAGE_SERVER_SPEC_MAP.iter_errors(specs))
  237. if errors: # pragma: no cover
  238. self.log.warning(
  239. _(
  240. "Failed to validate commands from language server spec finder"
  241. " `{}`:\n{}"
  242. ).format(ep.name, errors)
  243. )
  244. continue
  245. for key, spec in specs.items():
  246. yield key, spec
  247. if skipped_servers:
  248. self.log.info(
  249. _("Skipped non-installed server(s): {}").format(
  250. ", ".join(skipped_servers)
  251. )
  252. )
  253. # the listener decorator
  254. lsp_message_listener = LanguageServerManager.register_message_listener # noqa