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

Language Server Spec v1 #90

Merged
merged 11 commits into from
Nov 5, 2019
16 changes: 12 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,21 +182,28 @@ server it will always win vs an auto-configured one.

#### Writing a spec

> See the built-in [specs](./specs) for implementations and some
> [helpers](./specs/utils.py).
> See the built-in [specs](./py_src/jupyter_lsp/specs) for implementations and some
> [helpers](./py_src/jupyter_lsp/specs/utils.py).

A spec is a python function that accepts a single argument, the
`LanguageServerManager`, and returns a dictionary of the form:

```python
{
"python-language-server": { # the name of the implementation
"version": 1, # the version of the spec schema
"argv": ["python", "-m", "pyls"], # a list of command line arguments
"languages": ["python"] # a list of languages it supports
}
}
```

The absolute minimum listing requires `argv` (a list of shell tokens to launch
the server) and `languages` (which languages to respond to), but many number of
other options to enrich the user experience are available in the
[schema](./py_src/jupyter_lsp/schema/schema.json) and are exercised by the
current `entry_points`-based [specs]().

The spec should only be advertised if the command _could actually_ be run:

- its runtime (e.g. `julia`, `nodejs`, `python`, `r`, `ruby`) is installed
Expand All @@ -212,7 +219,7 @@ The spec should only be advertised if the command _could actually_ be run:
guess at where a user's `nodejs` might be found
- some language servers are hard to start purely from the command line
- use a helper script to encapsulate some complexity.
- See the [r spec](./specs/r_languageserver.py) for an example
- See the [r spec](./py_src/jupyter_lsp/specs/r_languageserver.py) for an example

##### Example: making a pip-installable `cool-language-server` spec

Expand Down Expand Up @@ -240,6 +247,7 @@ def cool(app):

return {
"cool-language-server": {
"version": 1,
"argv": [cool_language_server],
"languages": ["cool"]
}
Expand All @@ -255,7 +263,7 @@ setuptools.setup(
name="jupyter-lsp-my-cool-language-server",
py_modules=["jupyter_lsp_my_cool_language_server"],
entry_points={
"jupyter_lsp_spec_v0": [
"jupyter_lsp_spec_v1": [
"cool-language-server":
"jupyter_lsp_my_cool_language_server:cool"
]
Expand Down
40 changes: 26 additions & 14 deletions LANGUAGESERVERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,36 @@ Use a package manager to install a [language server][lsp-implementations]
(also [this list][langserver]) from the tables below: these implementations are
tested to work with `jupyter-lsp`.

| language | `npm install (-g)`, or `yarn/jlpm add (-g)` |
| ------------------------- | :-----------------------------------------: |
| bash | `bash-language-server` |
| css<br/>less<br/>sass | `vscode-css-languageserver-bin` |
| docker | `dockerfile-language-server-nodejs` |
| html | `vscode-html-languageserver-bin` |
| javascript<br/>typescript | `javascript-typescript-langserver` |
| json | `vscode-json-languageserver-bin` |
| markdown | `unified-language-server` |
| yaml | `yaml-language-server` |
| language | `jupyter labextension link`<br/>`npm install (-g)`<br/>`yarn/jlpm add (-g)` |
| ------------------------- | :-------------------------------------------------------------------------: |
| bash | [bash-language-server][] |
| css<br/>less<br/>sass | [vscode-css-languageserver-bin][] |
| docker | [dockerfile-language-server-nodejs][] |
| html | [vscode-html-languageserver-bin][] |
| javascript<br/>typescript | [javascript-typescript-langserver][] |
| json | [vscode-json-languageserver-bin][] |
| yaml | [yaml-language-server][] |

| language | `conda install -c conda-forge` | language-specific package manager |
| -------- | :----------------------------: | :-----------------------------------------------: |
| python | `python-language-server` | `pip install python-language-server` |
| r | `r-languageserver` | `Rscript -e 'install.packages("languageserver")'` |
| python | [python-language-server][] | `pip install python-language-server` |
| r | [r-languageserver][] | `Rscript -e 'install.packages("languageserver")'` |

[language-server]: https://microsoft.github.io/language-server-protocol/specification
[langserver]: https://langserver.org
[jupyter-server-proxy]: https://github.com/jupyterhub/jupyter-server-proxy
[lsp-implementations]: https://microsoft.github.io/language-server-protocol/implementors/servers
[jupyter-lsp]: https://github.com/krassowski/jupyterlab-lsp.git
[jupyterlab]: https://github.com/jupyterlab/jupyterlab
[bash-language-server]: https://github.com/mads-hartmann/https://github.com/mads-hartmann
[vscode-css-languageserver-bin]: https://github.com/vscode-langservers/vscode-css-languageserver-bin
[vscode-html-languageserver-bin]: https://github.com/vscode-langservers/vscode-html-languageserver-bin
[dockerfile-language-server-nodejs]: https://github.com/rcjsuen/dockerfile-language-server-nodejs
[javascript-typescript-langserver]: https://github.com/sourcegraph/javascript-typescript-langserver
[python-language-server]: https://github.com/palantir/python-language-server
[vscode-json-languageserver-bin]: https://github.com/vscode-langservers/vscode-json-languageserver-bin
[yaml-language-server]: https://github.com/redhat-developer/yaml-language-server
[r-languageserver]: https://github.com/REditorSupport/languageserver

Don't see an implementation for the language server you need? You can
[bring your own language server](#adding-custom-language-servers).
Expand All @@ -52,7 +60,11 @@ jupyter --paths
```

They will be merged from bottom to top, and the directory where you launch your
`notebook` server wins, making it easy to check in to version control.
`notebook` server wins, making it easy to check in to version control. The absolute
minimum listing requires `argv` (a list of shell tokens to launch the server)
and `languages` (which languages to respond to), but many number of other options
to enrich the user experience are available in the
[schema](./py_src/jupyter_lsp/schema/schema.json).

```python
# ./jupyter_notebook_config.json ---------- unique! -----------
Expand Down Expand Up @@ -134,4 +146,4 @@ The order is, roughly:
> default: []

Additional places `jupyter-lsp` will look for `node_modules`. These will be checked
before `node_roots`
_before_ `node_roots`.
2 changes: 1 addition & 1 deletion py_src/jupyter_lsp/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"""

# the current entry_point to use for python-based spec finders
EP_SPEC_V0 = "jupyter_lsp_spec_v0"
EP_SPEC_V1 = "jupyter_lsp_spec_v1"
29 changes: 20 additions & 9 deletions py_src/jupyter_lsp/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from tornado.ioloop import IOLoop

from .manager import LanguageServerManager
from .schema import SERVERS_RESPONSE


class BaseHandler(IPythonHandler):
Expand Down Expand Up @@ -47,18 +48,28 @@ class LanguageServersHandler(BaseHandler):
Response should conform to schema in schema/servers.schema.json
"""

validator = SERVERS_RESPONSE

def initialize(self, *args, **kwargs):
super().initialize(*args, **kwargs)

def get(self):
""" finish with the JSON representations of the sessions
"""
self.finish(
{
"version": 0,
"sessions": sorted(
[session.to_json() for session in self.manager.sessions.values()],
key=lambda session: session["languages"],
),
}
)
response = {
"version": 1,
"sessions": sorted(
[session.to_json() for session in self.manager.sessions.values()],
key=lambda session: session["spec"]["languages"],
),
}

errors = list(self.validator.iter_errors(response))

if errors: # pragma: no cover
self.log.warn("{} validation errors: {}", len(errors), errors)

self.finish(response)


def add_handlers(nbapp):
Expand Down
36 changes: 27 additions & 9 deletions py_src/jupyter_lsp/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@

import pkg_resources
from notebook.transutils import _
from traitlets import Bool, Dict as Dict_, Instance
from traitlets import Bool, Dict as Dict_, Instance, default

from .constants import EP_SPEC_V0
from .constants import EP_SPEC_V1
from .schema import LANGUAGE_SERVER_SPEC_MAP
from .session import LanguageServerSession
from .trait_types import Schema
from .types import KeyedLanguageServerSpecs, LanguageServerManagerAPI, SpecMaker


class LanguageServerManager(LanguageServerManagerAPI):
""" Manage language servers
"""

language_servers = Dict_(
trait=Dict_,
default_value=[],
language_servers = Schema(
validator=LANGUAGE_SERVER_SPEC_MAP,
help=_("a dict of language server specs, keyed by implementation"),
).tag(
config=True
Expand All @@ -35,6 +36,10 @@ class LanguageServerManager(LanguageServerManagerAPI):
help="sessions keyed by languages served",
) # type: typing.Dict[typing.Tuple[typing.Text], LanguageServerSession]

@default("language_servers")
def _default_language_servers(self):
return {}

def __init__(self, **kwargs):
""" Before starting, perform all necessary configuration
"""
Expand Down Expand Up @@ -71,7 +76,7 @@ def init_sessions(self):
sessions = {}
for spec in self.language_servers.values():
sessions[tuple(sorted(spec["languages"]))] = LanguageServerSession(
argv=spec["argv"], languages=spec["languages"]
spec=spec
)
self.sessions = sessions

Expand Down Expand Up @@ -104,7 +109,7 @@ def _autodetect_language_servers(self):
entry_points = []

try:
entry_points = list(pkg_resources.iter_entry_points(EP_SPEC_V0))
entry_points = list(pkg_resources.iter_entry_points(EP_SPEC_V1))
except Exception: # pragma: no cover
self.log.exception("Failed to load entry_points")

Expand All @@ -120,8 +125,7 @@ def _autodetect_language_servers(self):
continue

try:
for key, spec in spec_finder(self).items():
yield key, spec
specs = spec_finder(self)
except Exception as err: # pragma: no cover
self.log.warning(
_(
Expand All @@ -130,3 +134,17 @@ def _autodetect_language_servers(self):
).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
20 changes: 15 additions & 5 deletions py_src/jupyter_lsp/schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,21 @@
import jsonschema

HERE = pathlib.Path(__file__).parent
SCHEMA_FILE = HERE / "schema.json"
SCHEMA = json.loads(SCHEMA_FILE.read_text())
SPEC_VERSION = SCHEMA["definitions"]["current-version"]["enum"][0]


def servers_schema() -> jsonschema.validators.Draft7Validator:
""" return a JSON Schema Draft 7 validator for the server status API
def make_validator(key):
""" make a JSON Schema (Draft 7) validator
"""
return jsonschema.validators.Draft7Validator(
json.loads((HERE / "servers.schema.json").read_text())
)
schema = {"$ref": "#/definitions/{}".format(key)}
schema.update(SCHEMA)
return jsonschema.validators.Draft7Validator(schema)


SERVERS_RESPONSE = make_validator("servers-response")

LANGUAGE_SERVER_SPEC = make_validator("language-server-spec")

LANGUAGE_SERVER_SPEC_MAP = make_validator("language-server-specs-implementation-map")
Loading