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

Use language server implementation instead of language for URLs #199

Merged
merged 18 commits into from
Feb 22, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 4 additions & 4 deletions docs/EXTENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ from jupyter_lsp import lsp_message_listener
def load_jupyter_server_extension(nbapp):

@lsp_message_listener("all")
async def my_listener(scope, message, languages, manager):
print("received a {} {} message about {}".format(
scope, message["method"], languages
async def my_listener(scope, message, language_server, manager):
print("received a {} {} message from {}".format(
scope, message["method"], language_server
))
```

Expand All @@ -95,5 +95,5 @@ def load_jupyter_server_extension(nbapp):
Fine-grained controls are available as part of the Python API. Pass these as
named arguments to `lsp_message_listener`.

- `languages`: a regular expression of languages
- `language_server`: a regular expression of language servers
- `method`: a regular expression of LSP JSON-RPC method names
32 changes: 18 additions & 14 deletions py_src/jupyter_lsp/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,20 @@ class LanguageServerWebSocketHandler(WebSocketMixin, WebSocketHandler, BaseHandl
""" Setup tornado websocket to route to language server sessions
"""

language = None # type: Optional[Text]
language_server = None # type: Optional[Text]

def open(self, language):
self.language = language
def open(self, language_server):
self.language_server = language_server
self.manager.subscribe(self)
self.log.debug("[{}] Opened a handler".format(self.language))
self.log.debug("[{}] Opened a handler".format(self.language_server))

async def on_message(self, message):
self.log.debug("[{}] Handling a message".format(self.language))
self.log.debug("[{}] Handling a message".format(self.language_server))
await self.manager.on_client_message(message, self)

def on_close(self):
self.manager.unsubscribe(self)
self.log.debug("[{}] Closed a handler".format(self.language))
self.log.debug("[{}] Closed a handler".format(self.language_server))


class LanguageServersHandler(BaseHandler):
Expand All @@ -52,11 +52,11 @@ def get(self):
""" finish with the JSON representations of the sessions
"""
response = {
"version": 1,
"sessions": sorted(
[session.to_json() for session in self.manager.sessions.values()],
key=lambda session: session["spec"]["languages"],
),
"version": 2,
"sessions": {
language_server: session.to_json()
for language_server, session in self.manager.sessions.items()
},
}

errors = list(self.validator.iter_errors(response))
Expand All @@ -71,14 +71,18 @@ def add_handlers(nbapp):
""" Add Language Server routes to the notebook server web application
"""
lsp_url = ujoin(nbapp.base_url, "lsp")
re_langs = "(?P<language>.*)"
re_langservers = "(?P<language_server>.*)"

opts = {"manager": nbapp.language_server_manager}

nbapp.web_app.add_handlers(
".*",
[
(lsp_url, LanguageServersHandler, opts),
(ujoin(lsp_url, re_langs), LanguageServerWebSocketHandler, opts),
(ujoin(lsp_url, "status"), LanguageServersHandler, opts),
(
ujoin(lsp_url, "ws", re_langservers),
LanguageServerWebSocketHandler,
opts,
),
],
)
72 changes: 46 additions & 26 deletions py_src/jupyter_lsp/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class LanguageServerManager(LanguageServerManagerAPI):
sessions = Dict_(
trait=Instance(LanguageServerSession),
default_value={},
help="sessions keyed by languages served",
help="sessions keyed by language server name",
) # type: Dict[Tuple[Text], LanguageServerSession]

all_listeners = List_(trait=LoadableCallable).tag(config=True)
Expand Down Expand Up @@ -80,18 +80,16 @@ def init_language_servers(self) -> None:

# 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")
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 spec in self.language_servers.values():
sessions[tuple(sorted(spec["languages"]))] = LanguageServerSession(
spec=spec, parent=self
for language_server, spec in self.language_servers.items():
sessions[language_server] = LanguageServerSession(
language_server=language_server, spec=spec, parent=self
)
self.sessions = sessions

Expand Down Expand Up @@ -121,37 +119,59 @@ def init_listeners(self):
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)
session = self.sessions.get(handler.language_server)

if sessions:
for session in sessions:
session.handlers = set([handler]) | session.handlers
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])
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

for session in self.sessions_for_handler(handler):
session.write(message)
session.write(message)

async def on_server_message(self, message, session):
await self.wait_for_listeners(
MessageScope.SERVER, message, session.spec["languages"]
)
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):
for session in self.sessions_for_handler(handler):
session.handlers = [h for h in session.handlers if h != 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

def sessions_for_handler(self, handler):
for session in self.sessions.values():
if handler in session.handlers:
yield session
session.handlers = [h for h in session.handlers if h != handler]

def _autodetect_language_servers(self):
entry_points = []
Expand Down
13 changes: 9 additions & 4 deletions py_src/jupyter_lsp/schema/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"current-version": {
"description": "which version of the spec this implements",
"enum": [1],
"enum": [2],
"title": "Spec Schema Version",
"type": "number"
},
Expand Down Expand Up @@ -163,11 +163,16 @@
"servers-response": {
"properties": {
"sessions": {
"description": "a list of servers that are, could be, or were running",
"items": {
"description": "named server sessions that are, could be, or were running",
"patternProperties": {
".*": {
"$ref": "#/definitions/session"
}
},
"additionalProperties": {
"$ref": "#/definitions/session"
},
"type": "array"
"type": "object"
},
"version": {
"$ref": "#/definitions/current-version"
Expand Down
9 changes: 5 additions & 4 deletions py_src/jupyter_lsp/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from tornado.queues import Queue
from tornado.websocket import WebSocketHandler
from traitlets import Bunch, Instance, Set, UseEnum, observe
from traitlets import Bunch, Instance, Set, Unicode, UseEnum, observe
from traitlets.config import LoggingConfigurable

from . import stdio
Expand All @@ -26,6 +26,7 @@ class LanguageServerSession(LoggingConfigurable):
""" Manage a session for a connection to a language server
"""

language_server = Unicode(help="the language server implementation name")
spec = Schema(LANGUAGE_SERVER_SPEC)

# run-time specifics
Expand Down Expand Up @@ -60,9 +61,9 @@ def __init__(self, *args, **kwargs):
atexit.register(self.stop)

def __repr__(self): # pragma: no cover
return "<LanguageServerSession(languages={languages}, argv={argv})>".format(
**self.spec
)
return (
"<LanguageServerSession(" "language_server={language_server}, argv={argv})>"
).format(language_server=self.language_server, **self.spec)

def to_json(self):
return dict(
Expand Down
42 changes: 18 additions & 24 deletions py_src/jupyter_lsp/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,45 +12,39 @@
from jupyter_lsp.handlers import LanguageServersHandler, LanguageServerWebSocketHandler

# these should always be available in a test environment ()
KNOWN_LANGUAGES = [
"bash",
"css",
"dockerfile",
"html",
"ipythongfm",
"javascript",
"json",
"jsx",
"less",
"markdown",
"python",
"scss",
"typescript-jsx",
"typescript",
"yaml",
KNOWN_SERVERS = [
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice thing here is it is now slightly faster...

"bash-language-server",
"vscode-css-languageserver-bin",
"dockerfile-language-server-nodejs",
"vscode-html-languageserver-bin",
"unified-language-server",
"javascript-typescript-langserver",
"vscode-json-languageserver-bin",
"pyls",
"yaml-language-server",
]

CMD_BASED_LANGUAGES = {"Rscript": ["r"]}
CMD_BASED_SERVERS = {"Rscript": ["r-languageserver"]}

KNOWN_LANGUAGES += sum(
[langs for cmd, langs in CMD_BASED_LANGUAGES.items() if shutil.which(cmd)], []
KNOWN_SERVERS += sum(
[langs for cmd, langs in CMD_BASED_SERVERS.items() if shutil.which(cmd)], []
)

KNOWN_UNKNOWN_LANGUAGES = ["cobol"]
KNOWN_UNKNOWN_SERVERS = ["foo-language-server"]


@fixture
def manager() -> LanguageServerManager:
return LanguageServerManager()


@fixture(params=sorted(KNOWN_LANGUAGES))
def known_language(request):
@fixture(params=sorted(KNOWN_SERVERS))
def known_server(request):
return request.param


@fixture(params=sorted(KNOWN_UNKNOWN_LANGUAGES))
def known_unknown_language(request):
@fixture(params=sorted(KNOWN_UNKNOWN_SERVERS))
def known_unknown_server(request):
return request.param


Expand Down
2 changes: 1 addition & 1 deletion py_src/jupyter_lsp/tests/listener.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
async def dummy_listener(scope, message, languages, manager):
async def dummy_listener(scope, message, language_server, manager):
pass
24 changes: 13 additions & 11 deletions py_src/jupyter_lsp/tests/test_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ async def test_listener_bad_traitlets(bad_string, handlers):


@pytest.mark.asyncio
async def test_listeners(known_language, handlers, jsonrpc_init_msg):
async def test_listeners(known_server, handlers, jsonrpc_init_msg):
""" will some listeners listen?
"""
handler, ws_handler = handlers
Expand All @@ -36,7 +36,7 @@ async def test_listeners(known_language, handlers, jsonrpc_init_msg):
assert re.match(
(
"<MessageListener listener=<function dummy_listener at .*?>,"
" method=None, language=None>"
" method=None, language_server=None>"
),
repr(dummy_listener),
)
Expand All @@ -46,39 +46,41 @@ async def test_listeners(known_language, handlers, jsonrpc_init_msg):
all_listened = Queue()

# some client listeners
@lsp_message_listener("client", language=known_language, method="initialize")
async def client_listener(scope, message, languages, manager):
@lsp_message_listener("client", language_server=known_server, method="initialize")
async def client_listener(scope, message, language_server, manager):
await handler_listened.put(message)

@lsp_message_listener("client", method=r"not-a-method")
async def other_client_listener(
scope, message, languages, manager
scope, message, language_server, manager
): # pragma: no cover
await handler_listened.put(message)
raise NotImplementedError("shouldn't get here")

# some server listeners
@lsp_message_listener("server", language=None, method=None)
async def server_listener(scope, message, languages, manager):
@lsp_message_listener("server", language_server=None, method=None)
async def server_listener(scope, message, language_server, manager):
await server_listened.put(message)

@lsp_message_listener("server", language=r"not-a-language")
@lsp_message_listener("server", language_server=r"not-a-language-server")
async def other_server_listener(
scope, message, languages, manager
scope, message, language_server, manager
): # pragma: no cover
await handler_listened.put(message)
raise NotImplementedError("shouldn't get here")

# an all listener
@lsp_message_listener("all")
async def all_listener(scope, message, languages, manager): # pragma: no cover
async def all_listener(
scope, message, language_server, manager
): # pragma: no cover
await all_listened.put(message)

assert len(manager._listeners["server"]) == 2
assert len(manager._listeners["client"]) == 2
assert len(manager._listeners["all"]) == 2

ws_handler.open(known_language)
ws_handler.open(known_server)

await ws_handler.on_message(jsonrpc_init_msg)

Expand Down
Loading