Skip to content

Commit

Permalink
Merge pull request #79 from swyddfa/develop
Browse files Browse the repository at this point in the history
New Release
  • Loading branch information
alcarney authored Feb 1, 2021
2 parents f33b267 + 0c40d0e commit 56e79d3
Show file tree
Hide file tree
Showing 35 changed files with 1,087 additions and 196 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.coverage
.env
.ipynb_checkpoints
.tox
.vscode-test

Expand All @@ -14,4 +15,4 @@ node_modules
dist

.changes.html
CHANGELOG.md
CHANGELOG.md
7 changes: 6 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ["esbonio.tutorial"]
extensions = ["sphinx.ext.intersphinx", "esbonio.tutorial"]

intersphinx_mapping = {
"python": ("https://docs.python.org/3/", None),
"sphinx": ("https://www.sphinx-doc.org/en/master", None),
}

# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
Expand Down
5 changes: 3 additions & 2 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ Welcome to the documentation
:hidden:
:maxdepth: 2

lsp/*
lsp/features
lsp/contributing
changelog

.. toctree::
Expand All @@ -26,4 +27,4 @@ Welcome to the documentation
:hidden:
:caption: VSCode Extension

code/*
code/*
105 changes: 105 additions & 0 deletions docs/lsp/features.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
Features
========

This page contains a quick overview of the features offered by the Language
Server

Completion
----------

The Language Server can offer auto complete suggestions in a variety of contexts

Directives
^^^^^^^^^^

.. figure:: ../../resources/images/complete-directive-demo.gif
:align: center

Completing directive names

.. note::

Currently the Language Server makes a hardcoded assumption that your
``primary_domain`` is set to ``python`` and has no knowledge that other
domains exist.

Support for additional domains will come in a future release

Directive Options
^^^^^^^^^^^^^^^^^

.. figure:: ../../resources/images/complete-directive-options-demo.gif
:align: center

Completing a directive's options

Roles
^^^^^

.. figure:: ../../resources/images/complete-role-demo.gif
:align: center

Completing role names

.. note::

Currently the Language Server makes a hardcoded assumption that your
``primary_domain`` is set to ``python`` and has no knowledge that other
domains exist.

Support for additional domains will come in a future release

Role Targets
^^^^^^^^^^^^

The lanuguage server is able to offer completions for the targets to a number of
different role types.

.. figure:: ../../resources/images/complete-role-target-demo.gif
:align: center

Completing role targets

Currently supported roles include

.. hlist::
:columns: 3

* :rst:role:`sphinx:doc`
* :rst:role:`sphinx:envvar`
* :rst:role:`sphinx:ref`
* :rst:role:`sphinx:option`
* :rst:role:`sphinx:py:attr`
* :rst:role:`sphinx:py:class`
* :rst:role:`sphinx:py:data`
* :rst:role:`sphinx:py:exc`
* :rst:role:`sphinx:py:func`
* :rst:role:`sphinx:py:meth`
* :rst:role:`sphinx:py:mod`
* :rst:role:`sphinx:py:obj`
* :rst:role:`sphinx:term`
* :rst:role:`sphinx:token`

Inter Sphinx
^^^^^^^^^^^^

The :doc:`intersphinx <sphinx:usage/extensions/intersphinx>` extension that
comes bundled with Sphinx makes it easy to link to other Sphinx projects. If
configured for your project, the language server will offer autocomplete
suggestions when appropriate.

.. figure:: ../../resources/images/complete-intersphinx-demo.gif
:align: center

Completing references to the Python documentation.

Diagnostics
-----------

The language server is able to catch some of the errors Sphinx outputs while
building and publish them as diagnostic messages

.. figure:: ../../resources/images/diagnostic-sphinx-errors-demo.png
:align: center

Example diagnostic messages from Sphinx
2 changes: 1 addition & 1 deletion lib/esbonio/CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Fixes

- Errors encountered when initialising Sphinx are now caught and the language
client is notified of an issue. (`#33 <https://github.com/swyddfa/esbonio/issues/33>`_)
- **Language Server** Fix issue where some malformed ``CompletionItem``s were
- **Language Server** Fix issue where some malformed ``CompletionItem`` objects were
preventing completion suggestions from being shown. (`#54 <https://github.com/swyddfa/esbonio/issues/54>`_)
- **Language Server** Windows paths are now handled correctly (`#60 <https://github.com/swyddfa/esbonio/issues/60>`_)
- **Language Server** Server no longer chooses ``conf.py`` files that
Expand Down
2 changes: 2 additions & 0 deletions lib/esbonio/changes/36.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
**Language Server** Directive option completions are now provided
within a directive's options block
3 changes: 3 additions & 0 deletions lib/esbonio/changes/66.fix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**Language Server** Regex that catches diagnostics from Sphinx's
output can now handle windows paths. Diagnostic reporting now sends a
proper URI
1 change: 1 addition & 0 deletions lib/esbonio/changes/68.fix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**Language Server** Diagnostics are now reported on first startup
2 changes: 2 additions & 0 deletions lib/esbonio/changes/73.fix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
**Language Server** Fix exception that was thrown when trying to find
completions for an unknown role type
2 changes: 2 additions & 0 deletions lib/esbonio/changes/74.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
**Language Server** For projects that use ``interpshinx`` completions
for intersphinx targets are now suggested when available
2 changes: 2 additions & 0 deletions lib/esbonio/changes/77.fix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
**Language Server** The server will not offer completion suggestions outside of
a role target
33 changes: 25 additions & 8 deletions lib/esbonio/esbonio/lsp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from typing import List

from pygls.features import COMPLETION, INITIALIZE, TEXT_DOCUMENT_DID_SAVE
from pygls.features import COMPLETION, INITIALIZE, INITIALIZED, TEXT_DOCUMENT_DID_SAVE
from pygls.server import LanguageServer
from pygls.types import (
CompletionList,
Expand Down Expand Up @@ -35,6 +35,10 @@ def __init__(self, *args, **kwargs):
self.on_init_hooks = []
"""A list of functions to run on initialization"""

self.on_initialized_hooks = []
"""A list of functions to run after receiving the initialized notification from the
client"""

self.on_save_hooks = []
"""A list of hooks to run on document save."""

Expand All @@ -47,15 +51,21 @@ def add_feature(self, feature):
if hasattr(feature, "initialize"):
self.on_init_hooks.append(feature.initialize)

if hasattr(feature, "initialized"):
self.on_initialized_hooks.append(feature.initialized)

if hasattr(feature, "save"):
self.on_save_hooks.append(feature.save)

# TODO: Add support for mutltiple handlers using the same trigger.
if hasattr(feature, "suggest") and hasattr(feature, "suggest_trigger"):
trigger = feature.suggest_trigger
handler = feature.suggest
if hasattr(feature, "suggest") and hasattr(feature, "suggest_triggers"):

for trigger in feature.suggest_triggers:
handler = feature.suggest

self.completion_handlers[trigger] = handler
if trigger in self.completion_handlers:
self.completion_handlers[trigger].append(handler)
else:
self.completion_handlers[trigger] = [handler]

def load_module(self, mod: str):
# TODO: Handle failures.
Expand Down Expand Up @@ -101,6 +111,12 @@ def on_initialize(rst: RstLanguageServer, params: InitializeParams):

rst.logger.info("LSP Server Initialized")

@server.feature(INITIALIZED)
def on_initialized(rst: RstLanguageServer, params):

for initialized_hook in rst.on_initialized_hooks:
initialized_hook()

@server.feature(COMPLETION, trigger_characters=[".", ":", "`", "<"])
def on_completion(rst: RstLanguageServer, params: CompletionParams):
"""Suggest completions based on the current context."""
Expand All @@ -112,10 +128,11 @@ def on_completion(rst: RstLanguageServer, params: CompletionParams):

items = []

for pattern, handler in rst.completion_handlers.items():
for pattern, handlers in rst.completion_handlers.items():
match = pattern.match(line)
if match:
items += handler(match, line, doc)
for handler in handlers:
items += handler(match, doc, pos)

return CompletionList(False, items)

Expand Down
90 changes: 75 additions & 15 deletions lib/esbonio/esbonio/lsp/completion/directives.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
from esbonio.lsp import RstLanguageServer


def to_completion_item(name: str, directive) -> CompletionItem:
"""Convert an rst directive to its CompletionItem representation."""
def resolve_directive(directive):

# 'Core' docutils directives are returned as tuples (modulename, ClassName)
# so its up to us to resolve the reference
Expand All @@ -23,6 +22,13 @@ def to_completion_item(name: str, directive) -> CompletionItem:
module = importlib.import_module(modulename)
directive = getattr(module, cls)

return directive


def directive_to_completion_item(name: str, directive) -> CompletionItem:
"""Convert an rst directive to its CompletionItem representation."""

directive = resolve_directive(directive)
documentation = inspect.getdoc(directive)

# TODO: Give better names to arguments based on what they represent.
Expand All @@ -41,6 +47,23 @@ def to_completion_item(name: str, directive) -> CompletionItem:
)


def options_to_completion_items(directive) -> List[CompletionItem]:
"""Convert a directive's options to a list of completion items."""

directive = resolve_directive(directive)
options = directive.option_spec

if options is None:
return []

return [
CompletionItem(
opt, detail="option", kind=CompletionItemKind.Field, insert_text=f"{opt}:"
)
for opt in options
]


class DirectiveCompletion:
"""A completion handler for directives."""

Expand Down Expand Up @@ -68,24 +91,61 @@ def discover(self):
dirs = {**dirs, **std_directives, **py_directives}

self.directives = {
k: to_completion_item(k, v)
k: directive_to_completion_item(k, v)
for k, v in dirs.items()
if k != "restructuredtext-test-directive"
}
self.rst.logger.debug("Discovered %s directives", len(self.directives))

suggest_trigger = re.compile(
r"""
^\s* # directives may be indented
\.\. # they start with an rst comment
[ ]* # followed by a space
([\w-]+)?$ # with an optional name
""",
re.VERBOSE,
)
self.options = {
k: options_to_completion_items(v)
for k, v in dirs.items()
if k in self.directives
}

self.rst.logger.debug("Discovered %s directives", len(self.directives))

def suggest(self, match, line, doc) -> List[CompletionItem]:
return list(self.directives.values())
suggest_triggers = [
re.compile(
r"""
^\s* # directives may be indented
\.\. # they start with an rst comment
[ ]* # followed by a space
(?P<name>[\w-]+)?$ # with an optional name
""",
re.VERBOSE,
),
re.compile(
r"""
(?P<indent>\s+) # directive options must only be preceeded by whitespace
: # they start with a ':'
(?P<name>[\w-]*) # they have a name
$
""",
re.VERBOSE,
),
]

def suggest(self, match, doc, position) -> List[CompletionItem]:
groups = match.groupdict()

if "indent" not in groups:
return list(self.directives.values())

# Search backwards so that we can determine the context for our completion
indent = groups["indent"]
linum = position.line - 1
line = doc.lines[linum]

while line.startswith(indent):
linum -= 1
line = doc.lines[linum]

# Only offer completions if we're within a directive's option block
match = re.match(r"\s*\.\.[ ]*(?P<name>[\w-]+)::", line)
if not match:
return []

return self.options.get(match.group("name"), [])


def setup(rst: RstLanguageServer):
Expand Down
Loading

0 comments on commit 56e79d3

Please sign in to comment.