Skip to content

Commit

Permalink
Provide intersphinx completion suggestions (#76)
Browse files Browse the repository at this point in the history
In the case where the intersphinx extension is enabled and configured. The language server will now offer completions for targets in intersphinx'd projects

Also fixed a bug where role target suggestions were being offered outside of a role

Closes #74 
Closes #77
  • Loading branch information
alcarney authored Jan 29, 2021
1 parent 068f082 commit 556aa29
Show file tree
Hide file tree
Showing 21 changed files with 728 additions and 175 deletions.
5 changes: 4 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@
# ones.
extensions = ["sphinx.ext.intersphinx", "esbonio.tutorial"]

intersphinx_mapping = {"sphinx": ("https://www.sphinx-doc.org/en/master", None)}
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/*
26 changes: 24 additions & 2 deletions docs/lsp/01-features.rst → docs/lsp/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,6 @@ different role types.

Completing role targets



Currently supported roles include

.. hlist::
Expand All @@ -73,3 +71,27 @@ Currently supported roles include
* :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: 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
18 changes: 11 additions & 7 deletions lib/esbonio/esbonio/lsp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,15 @@ def add_feature(self, feature):
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"):

self.completion_handlers[trigger] = handler
for trigger in feature.suggest_triggers:
handler = feature.suggest

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 @@ -125,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, line, doc)

return CompletionList(False, items)

Expand Down
20 changes: 11 additions & 9 deletions lib/esbonio/esbonio/lsp/completion/directives.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,17 @@ def discover(self):
}
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,
)
suggest_triggers = [
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,
)
]

def suggest(self, match, line, doc) -> List[CompletionItem]:
return list(self.directives.values())
Expand Down
213 changes: 193 additions & 20 deletions lib/esbonio/esbonio/lsp/completion/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@
from esbonio.lsp import RstLanguageServer


def namespace_to_completion_item(namespace: str) -> CompletionItem:
return CompletionItem(
namespace,
detail="intersphinx namespace",
kind=CompletionItemKind.Module,
)


def role_to_completion_item(name, role) -> CompletionItem:
return CompletionItem(
name,
Expand All @@ -27,6 +35,14 @@ def role_to_completion_item(name, role) -> CompletionItem:
"function": CompletionItemKind.Function,
"method": CompletionItemKind.Method,
"module": CompletionItemKind.Module,
"py:attribute": CompletionItemKind.Field,
"py:class": CompletionItemKind.Class,
"py:function": CompletionItemKind.Function,
"py:method": CompletionItemKind.Method,
"py:module": CompletionItemKind.Module,
"std:doc": CompletionItemKind.File,
"std:envvar": CompletionItemKind.Variable,
"std:term": CompletionItemKind.Text,
"term": CompletionItemKind.Text,
}

Expand All @@ -36,6 +52,21 @@ def target_to_completion_item(name, display, type_) -> CompletionItem:
return CompletionItem(name, kind=kind, detail=str(display), insert_text=name)


def intersphinx_target_to_completion_item(label, item, type_) -> CompletionItem:
kind = TARGET_KINDS.get(type_, CompletionItemKind.Reference)
source, version, _, display = item

if display == "-":
display = label

if version:
version = f" v{version}"

detail = f"{display} - {source}{version}"

return CompletionItem(label, kind=kind, detail=detail, insert_text=label)


class RoleCompletion:
"""Completion handler for roles."""

Expand Down Expand Up @@ -72,16 +103,18 @@ def discover(self):
self.roles = {k: role_to_completion_item(k, v) for k, v in rs.items()}
self.rst.logger.debug("Discovered %s roles", len(self.roles))

suggest_trigger = re.compile(
r"""
(^|.*[ ]) # roles must be preceeded by a space, or start the line
: # roles start with the ':' character
(?!:) # make sure the next character is not ':'
[\w-]* # match the role name
$ # ensure pattern only matches incomplete roles
""",
re.MULTILINE | re.VERBOSE,
)
suggest_triggers = [
re.compile(
r"""
(^|.*[ ]) # roles must be preceeded by a space, or start the line
: # roles start with the ':' character
(?!:) # make sure the next character is not ':'
[\w-]* # match the role name
$ # ensure pattern only matches incomplete roles
""",
re.MULTILINE | re.VERBOSE,
)
]

def suggest(self, match, line, doc) -> List[CompletionItem]:
return list(self.roles.values())
Expand Down Expand Up @@ -139,16 +172,33 @@ def initialize(self):
def save(self, params: DidSaveTextDocumentParams):
self.discover_targets()

suggest_trigger = re.compile(
r"""
(^|.*[ ]) # roles must be preveeded by a space, or start the line
: # roles start with the ':' character
(?P<name>[\w-]+) # capture the role name, suggestions will change based on it
: # the role name ends with a ':'
` # the target begins with a '`'`
""",
re.MULTILINE | re.VERBOSE,
)
suggest_triggers = [
re.compile(
r"""
(^|.*[ ]) # roles must be preceeded by a space, or start the line
: # roles start with the ':' character
(?P<name>[\w-]+) # capture the role name, suggestions will change based on it
: # the role name ends with a ':'
` # the target begins with a '`'
(?P<target>[^<:`]*) # match "plain link" targets
$
""",
re.MULTILINE | re.VERBOSE,
),
re.compile(
r"""
(^|.*[ ]) # roles must be preceeded by a space, or start the line
: # roles start with the ':' character
(?P<name>[\w-]+) # capture the role name, suggestions will change based on it
: # the role name ends with a ':'
` # the target begins with a '`'`
.*< # the actual target name starts after a '<'
(?P<target>[^`:]*) # match "aliased" targets
$
""",
re.MULTILINE | re.VERBOSE,
),
]

def suggest(self, match, line, doc) -> List[CompletionItem]:
# TODO: Detect if we're in an angle bracket e.g. :ref:`More Info <|` in that
Expand Down Expand Up @@ -195,9 +245,132 @@ def discover_targets(self):
self.targets = {**build_target_map(py), **build_target_map(std)}


class InterSphinxNamespaceCompletion:
"""Completion handler for intersphinx namespaces."""

def __init__(self, rst: RstLanguageServer):
self.rst = rst
self.namespaces = {}

def initialize(self):

if self.rst.app and hasattr(self.rst.app.env, "intersphinx_named_inventory"):
inv = self.rst.app.env.intersphinx_named_inventory
self.namespaces = {v: namespace_to_completion_item(v) for v in inv.keys()}

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

suggest_triggers = RoleTargetCompletion.suggest_triggers

def suggest(self, match, line, doc) -> List[CompletionItem]:
return list(self.namespaces.values())


def build_target_type_map(domain: Domain) -> Dict[str, List[str]]:

types = {}

for name, obj in domain.object_types.items():
for role in obj.roles:
objs = types.get(role, None)

if objs is None:
objs = []

objs.append(f"{domain.name}:{name}")
types[role] = objs

return types


class InterSphinxTargetCompletion:
"""Completion handler for intersphinx targets"""

def __init__(self, rst: RstLanguageServer):
self.rst = rst
self.targets = {}
self.target_types = {}

def initialize(self):

if self.rst.app and hasattr(self.rst.app.env, "intersphinx_named_inventory"):
inv = self.rst.app.env.intersphinx_named_inventory
domains = self.rst.app.env.domains

for domain in domains.values():
self.target_types.update(build_target_type_map(domain))

for namespace, types in inv.items():
self.targets[namespace] = {
type_: {
label: intersphinx_target_to_completion_item(label, item, type_)
for label, item in items.items()
}
for type_, items in types.items()
}

suggest_triggers = [
re.compile(
r"""
(^|.*[ ]) # roles must be preceeded by a space, or start the line
: # roles start with the ':' character
(?P<name>[\w-]+) # capture the role name, suggestions will change based on it
: # the role name ends with a ':'
` # the target begins with a '`'
(?P<namespace>[^<:]*) # match "plain link" targets
: # namespaces end with a ':'
$
""",
re.MULTILINE | re.VERBOSE,
),
re.compile(
r"""
(^|.*[ ]) # roles must be preceeded by a space, or start the line
: # roles start with the ':' character
(?P<name>[\w-]+) # capture the role name, suggestions will change based on it
: # the role name ends with a ':'
` # the target begins with a '`'`
.*< # the actual target name starts after a '<'
(?P<namespace>[^:]*) # match "aliased" targets
: # namespaces end with a ':'
$
""",
re.MULTILINE | re.VERBOSE,
),
]

def suggest(self, match, line, doc) -> List[CompletionItem]:
# TODO: Detect if we're in an angle bracket e.g. :ref:`More Info <|` in that
# situation, add the closing '>' to the completion item insert text.

namespace = match.group("namespace")
rolename = match.group("name")

types = self.target_types.get(rolename, None)
if types is None:
return []

namespace = self.targets.get(namespace, None)
if namespace is None:
return []

targets = []
for type_ in types:
items = namespace.get(type_, {})
targets += items.values()

return targets


def setup(rst: RstLanguageServer):
role_completion = RoleCompletion(rst)
role_target_completion = RoleTargetCompletion(rst)
intersphinx_namespaces = InterSphinxNamespaceCompletion(rst)
intersphinx_targets = InterSphinxTargetCompletion(rst)

rst.add_feature(role_completion)
rst.add_feature(role_target_completion)
rst.add_feature(intersphinx_namespaces)
rst.add_feature(intersphinx_targets)
Loading

0 comments on commit 556aa29

Please sign in to comment.