Skip to content

Commit

Permalink
Merge pull request #90 from bollwyvl/add-more-to-spec
Browse files Browse the repository at this point in the history
Language Server Spec v1
  • Loading branch information
krassowski authored Nov 5, 2019
2 parents b86cdb4 + 822d37a commit 44b4d40
Show file tree
Hide file tree
Showing 33 changed files with 1,267 additions and 138 deletions.
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

0 comments on commit 44b4d40

Please sign in to comment.