Skip to content

Commit

Permalink
Introduce LSP Support
Browse files Browse the repository at this point in the history
This addresses mbj4668#903

Signed-off-by: Siddharth Sharma <[email protected]>
  • Loading branch information
esmasth committed May 10, 2024
1 parent 25f69e8 commit 6a02bce
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 8 deletions.
14 changes: 14 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# EditorConfig is awesome: https://EditorConfig.org

# top-most EditorConfig file
root = true

[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.py]
indent_style = space
indent_size = 4
9 changes: 5 additions & 4 deletions pyang/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,17 +141,18 @@ def add_parsed_module(self, module):

return module

def del_module(self, module):
def del_module(self, module, revision=None):
"""Remove a module from the context"""
rev = util.get_latest_revision(module)
del self.modules[(module.arg, rev)]
if revision is None:
revision = util.get_latest_revision(module)
del self.modules[(module.arg, revision)]

def get_module(self, modulename, revision=None):
"""Return the module if it exists in the context"""
if revision is None and modulename in self.revs:
(revision, _handle) = self._get_latest_rev(self.revs[modulename])
if revision is not None:
if (modulename,revision) in self.modules:
if (modulename, revision) in self.modules:
return self.modules[(modulename, revision)]
else:
return None
Expand Down
4 changes: 4 additions & 0 deletions pyang/lsp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""pyang library to serve Microsoft LSP"""

__version__ = "0.0.1"
__date__ = "2024-05-10"
214 changes: 214 additions & 0 deletions pyang/lsp/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
"""pyang LSP handling"""

from __future__ import absolute_import
import optparse
from pathlib import Path

from pyang import error
from pyang import context
from pyang import plugin
from pyang import syntax

from lsprotocol import types as lsp

from pygls.server import LanguageServer

SERVER_NAME = "pyangls"
SERVER_VERSION = "v0.1"

COUNT_DOWN_START_IN_SECONDS = 10
COUNT_DOWN_SLEEP_IN_SECONDS = 1

SERVER_MODE_IO = "io"
SERVER_MODE_TCP = "tcp"
SERVER_MODE_WS = "ws"
supported_modes = [
SERVER_MODE_IO,
SERVER_MODE_TCP,
SERVER_MODE_WS,
]
default_mode = SERVER_MODE_IO
default_host = "127.0.0.1"
default_port = 2087

class PyangLanguageServer(LanguageServer):
def __init__(self, *args):
self.ctx : context.Context
super().__init__(*args)

pyangls = PyangLanguageServer(SERVER_NAME, SERVER_VERSION)

def _validate(ls: LanguageServer,
params: lsp.DidChangeTextDocumentParams | lsp.DidOpenTextDocumentParams):
ls.show_message_log("Validating YANG...")

text_doc = ls.workspace.get_text_document(params.text_document.uri)
source = text_doc.source

pyangls.ctx.errors = []
modules = []
diagnostics = []
if source:
m = syntax.re_filename.search(Path(text_doc.filename).name)
if m is not None:
name, rev, in_format = m.groups()
module = pyangls.ctx.get_module(name, rev)
if module is not None:
pyangls.ctx.del_module(module)
module = pyangls.ctx.add_module(text_doc.path, source,
in_format, name, rev,
expect_failure_error=False,
primary_module=True)
else:
module = pyangls.ctx.add_module(text_doc.path, source,
primary_module=True)
if module is not None:
modules.append(module)
p : plugin.PyangPlugin
for p in plugin.plugins:
p.pre_validate_ctx(pyangls.ctx, modules)

pyangls.ctx.validate()
module.prune()

diagnostics = build_diagnostics()

ls.publish_diagnostics(text_doc.uri, diagnostics)

def build_diagnostics():
"""Builds lsp diagnostics from pyang context"""
diagnostics = []

for epos, etag, eargs in pyangls.ctx.errors:
msg = error.err_to_str(etag, eargs)
# pyang just stores line context, not keyword/argument context
start_line = epos.line - 1
start_col = 0
end_line = epos.line - 1
end_col = 1
def level_to_severity(level):
if level == 1 or level == 2:
return lsp.DiagnosticSeverity.Error
elif level == 3:
return lsp.DiagnosticSeverity.Warning
elif level == 4:
return lsp.DiagnosticSeverity.Information
else:
return None
d = lsp.Diagnostic(
range=lsp.Range(
start=lsp.Position(line=start_line, character=start_col),
end=lsp.Position(line=end_line, character=end_col),
),
message=msg,
severity=level_to_severity(error.err_level(etag)),
code=etag,
source=SERVER_NAME,
)

diagnostics.append(d)

return diagnostics


@pyangls.feature(
lsp.TEXT_DOCUMENT_DIAGNOSTIC,
lsp.DiagnosticOptions(
identifier="pyangls",
inter_file_dependencies=True,
workspace_diagnostics=True,
),
)
def text_document_diagnostic(
params: lsp.DocumentDiagnosticParams,
) -> lsp.DocumentDiagnosticReport:
"""Returns diagnostic report."""
return lsp.RelatedFullDocumentDiagnosticReport(
items=_validate(pyangls, params),
kind=lsp.DocumentDiagnosticReportKind.Full,
)


@pyangls.feature(lsp.WORKSPACE_DIAGNOSTIC)
def workspace_diagnostic(
params: lsp.WorkspaceDiagnosticParams,
) -> lsp.WorkspaceDiagnosticReport:
"""Returns diagnostic report."""
documents = pyangls.workspace.text_documents.keys()

if len(documents) == 0:
items = []
else:
first = list(documents)[0]
document = pyangls.workspace.get_text_document(first)
items = [
lsp.WorkspaceFullDocumentDiagnosticReport(
uri=document.uri,
version=document.version,
items=_validate(pyangls, params),
kind=lsp.DocumentDiagnosticReportKind.Full,
)
]

return lsp.WorkspaceDiagnosticReport(items=items)


@pyangls.feature(lsp.TEXT_DOCUMENT_DID_CHANGE)
def did_change(ls: LanguageServer, params: lsp.DidChangeTextDocumentParams):
"""Text document did change notification."""

_validate(ls, params)


@pyangls.feature(lsp.TEXT_DOCUMENT_DID_CLOSE)
def did_close(ls: PyangLanguageServer, params: lsp.DidCloseTextDocumentParams):
"""Text document did close notification."""
ls.show_message("Text Document Did Close")


@pyangls.feature(lsp.TEXT_DOCUMENT_DID_OPEN)
async def did_open(ls: LanguageServer, params: lsp.DidOpenTextDocumentParams):
"""Text document did open notification."""
ls.show_message("Text Document Did Open")
_validate(ls, params)


@pyangls.feature(lsp.TEXT_DOCUMENT_INLINE_VALUE)
def inline_value(params: lsp.InlineValueParams):
"""Returns inline value."""
return [lsp.InlineValueText(range=params.range, text="Inline value")]


def add_opts(optparser: optparse.OptionParser):
optlist = [
# use capitalized versions of std options help and version
optparse.make_option("--lsp-mode",
dest="pyangls_mode",
default=default_mode,
metavar="LSP_MODE",
help="Provide LSP Service in this mode" \
"Supported LSP server modes are: " +
', '.join(supported_modes)),
optparse.make_option("--lsp-host",
dest="pyangls_host",
default=default_host,
metavar="LSP_HOST",
help="Bind LSP Server to this address"),
optparse.make_option("--lsp-port",
dest="pyangls_port",
type="int",
default=default_port,
metavar="LSP_PORT",
help="Bind LSP Server to this port"),
]
g = optparser.add_option_group("LSP Server specific options")
g.add_options(optlist)

def start_server(optargs, ctx: context.Context):
pyangls.ctx = ctx
if optargs.pyangls_mode == SERVER_MODE_TCP:
pyangls.start_tcp(optargs.pyangls_host, optargs.pyangls_port)
elif optargs.pyangls_mode == SERVER_MODE_WS:
pyangls.start_ws(optargs.pyangls_host, optargs.pyangls_port)
else:
pyangls.start_io()
11 changes: 9 additions & 2 deletions pyang/scripts/pyang_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import optparse
import io
import shutil
import codecs
from pathlib import Path

import pyang
Expand All @@ -15,8 +14,8 @@
from pyang import hello
from pyang import context
from pyang import repository
from pyang import statements
from pyang import syntax
from pyang.lsp import server as pyangls


def run():
Expand Down Expand Up @@ -131,6 +130,10 @@ def run():
dest="format",
help="Convert to FORMAT. Supported formats " \
"are: " + ', '.join(fmts)),
optparse.make_option("-l", "--lsp",
dest="lsp",
action="store_true",
help="Run as LSP server instead of CLI tool."),
optparse.make_option("-o", "--output",
dest="outfile",
help="Write the output to OUTFILE instead " \
Expand Down Expand Up @@ -218,6 +221,7 @@ def run():
optparser.version = '%prog ' + pyang.__version__
optparser.add_options(optlist)

pyangls.add_opts(optparser)
for p in plugin.plugins:
p.add_opts(optparser)

Expand Down Expand Up @@ -268,6 +272,9 @@ def run():
ctx.strict = o.strict
ctx.max_status = o.max_status

if o.lsp:
pyangls.start_server(o, ctx)

# make a map of features to support, per module
if o.hello:
for mn, rev in hel.yang_modules():
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def run_commands(self):
" Provides a framework for plugins that can convert YANG modules" + \
"to other formats.",
url='https://github.com/mbj4668/pyang',
install_requires = ["lxml"],
install_requires = ["lxml", "pygls"],
license='BSD',
classifiers=[
'Development Status :: 5 - Production/Stable',
Expand All @@ -82,7 +82,7 @@ def run_commands(self):
'json2xml = pyang.scripts.json2xml:main',
]
},
packages=['pyang', 'pyang.plugins', 'pyang.scripts', 'pyang.translators', 'pyang.transforms'],
packages=['pyang', 'pyang.plugins', 'pyang.scripts', 'pyang.translators', 'pyang.transforms', 'pyang.lsp'],
data_files=[
('share/man/man1', man1),
('share/yang/modules/iana', modules_iana),
Expand Down

0 comments on commit 6a02bce

Please sign in to comment.