more capabilities
This commit is contained in:
parent
97025786ad
commit
3a166388e4
396
skillls/main.py
396
skillls/main.py
|
@ -1,15 +1,18 @@
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from logging import INFO, basicConfig, getLogger
|
from dataclasses import dataclass, field
|
||||||
from re import fullmatch
|
from logging import INFO, basicConfig, debug, error, getLogger, info, warning
|
||||||
import re
|
from re import findall, finditer, fullmatch, match as rematch
|
||||||
from time import time
|
from time import time
|
||||||
from lsprotocol.types import (
|
from lsprotocol.types import (
|
||||||
|
INLAY_HINT_RESOLVE,
|
||||||
TEXT_DOCUMENT_DID_CHANGE,
|
TEXT_DOCUMENT_DID_CHANGE,
|
||||||
TEXT_DOCUMENT_DID_OPEN,
|
TEXT_DOCUMENT_DID_OPEN,
|
||||||
TEXT_DOCUMENT_DID_SAVE,
|
TEXT_DOCUMENT_DID_SAVE,
|
||||||
TEXT_DOCUMENT_DOCUMENT_SYMBOL,
|
TEXT_DOCUMENT_DOCUMENT_SYMBOL,
|
||||||
TEXT_DOCUMENT_HOVER,
|
TEXT_DOCUMENT_HOVER,
|
||||||
TEXT_DOCUMENT_INLAY_HINT,
|
TEXT_DOCUMENT_INLAY_HINT,
|
||||||
|
WORKSPACE_INLAY_HINT_REFRESH,
|
||||||
|
WORKSPACE_SEMANTIC_TOKENS_REFRESH,
|
||||||
CompletionItem,
|
CompletionItem,
|
||||||
Diagnostic,
|
Diagnostic,
|
||||||
DiagnosticSeverity,
|
DiagnosticSeverity,
|
||||||
|
@ -21,10 +24,12 @@ from lsprotocol.types import (
|
||||||
Hover,
|
Hover,
|
||||||
HoverParams,
|
HoverParams,
|
||||||
InlayHint,
|
InlayHint,
|
||||||
|
InlayHintKind,
|
||||||
InlayHintParams,
|
InlayHintParams,
|
||||||
MessageType,
|
MessageType,
|
||||||
Position,
|
Position,
|
||||||
Range,
|
Range,
|
||||||
|
SymbolKind,
|
||||||
)
|
)
|
||||||
|
|
||||||
from pygls.server import LanguageServer
|
from pygls.server import LanguageServer
|
||||||
|
@ -37,13 +42,85 @@ from .cache import Cache
|
||||||
|
|
||||||
URI = str
|
URI = str
|
||||||
|
|
||||||
basicConfig(filename="skillls.log", level=INFO)
|
basicConfig(filename="skillls.log", filemode="w", level=INFO)
|
||||||
cache: Cache[str, CompletionItem] = Cache()
|
cache: Cache[str, CompletionItem] = Cache()
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
|
||||||
|
def in_range(what: Position, area: Range) -> bool:
|
||||||
|
return (what >= area.start) and (what <= area.end)
|
||||||
|
|
||||||
|
|
||||||
|
def find_end(start: Position, lines: list[str]) -> Position:
|
||||||
|
count = 0
|
||||||
|
in_str: bool = False
|
||||||
|
last = ""
|
||||||
|
for row, line in enumerate(lines[start.line :]):
|
||||||
|
if row == 0:
|
||||||
|
line = line[start.character :]
|
||||||
|
row += start.character
|
||||||
|
for col, char in enumerate(line[start.character :] if row == 0 else line):
|
||||||
|
match char:
|
||||||
|
case "(":
|
||||||
|
if not in_str:
|
||||||
|
count += 1
|
||||||
|
case ")":
|
||||||
|
if not in_str:
|
||||||
|
if count > 0:
|
||||||
|
count -= 1
|
||||||
|
if count == 0:
|
||||||
|
return Position(start.line + row, col)
|
||||||
|
case '"':
|
||||||
|
if not (in_str and last == "\\"):
|
||||||
|
in_str = not in_str
|
||||||
|
case _:
|
||||||
|
last = char
|
||||||
|
|
||||||
|
last = char
|
||||||
|
|
||||||
|
error(f"did not fin end for start at {start}")
|
||||||
|
return Position(len(lines), len(lines[-1]))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Environment:
|
||||||
|
range: Range
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class LetEnvironment(Environment):
|
||||||
|
locals: set[str] = field(default_factory=set)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# @dataclass(frozen=True)
|
||||||
|
# class ProcEnvironment(Environment):
|
||||||
|
# name: str
|
||||||
|
# args: tuple[DocumentSymbol, ...]
|
||||||
|
# kwargs: tuple[DocumentSymbol, ...]
|
||||||
|
# rest: DocumentSymbol | None = None
|
||||||
|
#
|
||||||
|
# @property
|
||||||
|
# def locals(self) -> tuple[DocumentSymbol, ...]:
|
||||||
|
# ret = [*self.args, *self.kwargs]
|
||||||
|
# if self.rest:
|
||||||
|
# ret.append(self.rest)
|
||||||
|
#
|
||||||
|
# return tuple(ret)
|
||||||
|
|
||||||
|
|
||||||
class SkillLanguageServer(LanguageServer):
|
class SkillLanguageServer(LanguageServer):
|
||||||
|
lets: list[DocumentSymbol] = []
|
||||||
|
procs: list[DocumentSymbol] = []
|
||||||
|
defs: list[DocumentSymbol] = []
|
||||||
|
globals: list[DocumentSymbol] = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def envs(self) -> tuple[DocumentSymbol, ...]:
|
||||||
|
return (
|
||||||
|
*self.procs,
|
||||||
|
*self.lets,
|
||||||
|
)
|
||||||
|
|
||||||
def _diagnose_parens(self, doc: TextDocument) -> Generator[Diagnostic, None, None]:
|
def _diagnose_parens(self, doc: TextDocument) -> Generator[Diagnostic, None, None]:
|
||||||
open: list[tuple[int, int]] = []
|
open: list[tuple[int, int]] = []
|
||||||
in_str: bool = False
|
in_str: bool = False
|
||||||
|
@ -87,26 +164,37 @@ class SkillLanguageServer(LanguageServer):
|
||||||
|
|
||||||
def _diagnose_cisms(self, doc: TextDocument) -> Generator[Diagnostic, None, None]:
|
def _diagnose_cisms(self, doc: TextDocument) -> Generator[Diagnostic, None, None]:
|
||||||
for row, line in enumerate(doc.lines):
|
for row, line in enumerate(doc.lines):
|
||||||
for col, char in enumerate(line):
|
for m in finditer(
|
||||||
if col > 0:
|
r"(?P<proc>procedure\s+)?([a-zA-Z_][a-zA-Z_0-9]+)\(", line
|
||||||
if fullmatch("\\w", line[col - 1]) and char == "(":
|
):
|
||||||
if m := re.match(r"([a-zA-Z_][a-zA-Z_0-9]*)$", line[:col]):
|
if not m.group("proc"):
|
||||||
tok = m.group(1)
|
yield Diagnostic(
|
||||||
r = Range(
|
Range(Position(row, m.start()), Position(row, m.end())),
|
||||||
Position(row, col - len(tok)),
|
f"change `{m.group(2)}(` to `( {m.group(2)}`",
|
||||||
Position(row, col + 1),
|
DiagnosticSeverity.Hint,
|
||||||
)
|
)
|
||||||
else:
|
# for col, char in enumerate(line):
|
||||||
tok = "<token>"
|
# if col > 0:
|
||||||
r = Range(
|
# if fullmatch(r"\w", line[col - 1]) and char == "(":
|
||||||
Position(row, col - 1),
|
# if m := rematch(r"([a-zA-Z_][a-zA-Z_0-9]*)$", line[:col]):
|
||||||
Position(row, col + 1),
|
# tok = m.group(1)
|
||||||
)
|
# r = Range(
|
||||||
yield Diagnostic(
|
# Position(row, col - len(tok)),
|
||||||
r,
|
# Position(row, col + 1),
|
||||||
f"change `{tok}(` to `( {tok}` [cism]",
|
# )
|
||||||
DiagnosticSeverity.Hint,
|
# else:
|
||||||
)
|
# tok = "<token>"
|
||||||
|
# r = Range(
|
||||||
|
# Position(row, col - 1),
|
||||||
|
# Position(row, col + 1),
|
||||||
|
# )
|
||||||
|
# yield Diagnostic(
|
||||||
|
# r,
|
||||||
|
# f"change `{tok}(` to `( {tok}` [cism]",
|
||||||
|
# DiagnosticSeverity.Hint,
|
||||||
|
# )
|
||||||
|
|
||||||
|
# def _diagnose_vars(self, doc: TextDocument) -> Generator[Diagnostic, None, None]:
|
||||||
|
|
||||||
def diagnose(self, doc: TextDocument) -> None:
|
def diagnose(self, doc: TextDocument) -> None:
|
||||||
diags: list[Diagnostic] = []
|
diags: list[Diagnostic] = []
|
||||||
|
@ -116,27 +204,265 @@ class SkillLanguageServer(LanguageServer):
|
||||||
|
|
||||||
self.publish_diagnostics(doc.uri, diags)
|
self.publish_diagnostics(doc.uri, diags)
|
||||||
|
|
||||||
|
def parse(self, doc: TextDocument) -> None:
|
||||||
|
self.lets = []
|
||||||
|
self._parse_let(doc.lines)
|
||||||
|
self.procs = []
|
||||||
|
self._parse_proc(doc.lines, doc.uri)
|
||||||
|
self.globals = []
|
||||||
|
self._parse_assigns(doc.lines)
|
||||||
|
|
||||||
server = SkillLanguageServer("skillls", "v0.2")
|
def _parse_assigns(self, lines: list[str]) -> None:
|
||||||
|
for row, line in enumerate(lines):
|
||||||
|
for found in finditer(r"([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s+", line):
|
||||||
|
token = found.group(1)
|
||||||
|
token_range = Range(
|
||||||
|
Position(row, found.start()),
|
||||||
|
Position(row, found.start() + len(token)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if any(
|
||||||
|
in_range(token_range.start, let.range)
|
||||||
|
and (token in (child.name for child in (let.children or [])))
|
||||||
|
for let in self.lets
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.globals.append(
|
||||||
|
DocumentSymbol(
|
||||||
|
token, SymbolKind.Variable, token_range, token_range
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_let(self, lines: list[str]) -> None:
|
||||||
|
active_let: DocumentSymbol
|
||||||
|
for row, line in enumerate(lines):
|
||||||
|
for found in finditer(r"(\(\s*let\s+|\blet\(\s+)\((.*)\)", line):
|
||||||
|
start = Position(row, found.start())
|
||||||
|
end = find_end(start, lines)
|
||||||
|
children: list[DocumentSymbol] = []
|
||||||
|
active_let = DocumentSymbol(
|
||||||
|
"let",
|
||||||
|
SymbolKind.Namespace,
|
||||||
|
Range(start, end),
|
||||||
|
Range(start, end),
|
||||||
|
children=children,
|
||||||
|
)
|
||||||
|
self.lets.append(active_let)
|
||||||
|
|
||||||
|
offset = len(found.group(1)) + 3
|
||||||
|
for local_var in finditer(
|
||||||
|
r"([a-zA-Z_][a-zA-Z0-9_]*|\([a-zA-Z_][a-zA-Z0-9_]*\s+.+\))",
|
||||||
|
found.group(2),
|
||||||
|
):
|
||||||
|
if local_var.group(1).startswith("("):
|
||||||
|
if m := fullmatch(
|
||||||
|
r"\(([a-zA-Z_][a-zA-Z0-9_]*)\s+.+\)",
|
||||||
|
local_var.group(1),
|
||||||
|
):
|
||||||
|
children.append(
|
||||||
|
DocumentSymbol(
|
||||||
|
m.group(1),
|
||||||
|
SymbolKind.Variable,
|
||||||
|
Range(
|
||||||
|
Position(row, offset + local_var.start() + 1),
|
||||||
|
Position(
|
||||||
|
row,
|
||||||
|
offset
|
||||||
|
+ local_var.start()
|
||||||
|
+ 1
|
||||||
|
+ len(m.string),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Range(
|
||||||
|
Position(row, offset + local_var.start() + 1),
|
||||||
|
Position(
|
||||||
|
row,
|
||||||
|
offset
|
||||||
|
+ local_var.start()
|
||||||
|
+ 1
|
||||||
|
+ len(m.group(1)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
assert isinstance(active_let.children, list)
|
||||||
|
active_let.children.append(
|
||||||
|
DocumentSymbol(
|
||||||
|
local_var.group(1),
|
||||||
|
SymbolKind.Variable,
|
||||||
|
Range(
|
||||||
|
Position(row, offset + local_var.start()),
|
||||||
|
Position(row, offset + local_var.end()),
|
||||||
|
),
|
||||||
|
Range(
|
||||||
|
Position(row, offset + local_var.start()),
|
||||||
|
Position(row, offset + local_var.end()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_proc(self, lines: list[str], uri: str) -> None:
|
||||||
|
for row, line in enumerate(lines):
|
||||||
|
for found in finditer(
|
||||||
|
r"(\(\s*procedure|\bprocedure\()(\s+)([a-zA-Z_][a-zA-Z0-9_]*)\((.*)\)",
|
||||||
|
line,
|
||||||
|
):
|
||||||
|
start = Position(row, found.start())
|
||||||
|
end = find_end(start, lines)
|
||||||
|
if "@option" in found.group(4) and "@key" in found.group(4):
|
||||||
|
self.publish_diagnostics(
|
||||||
|
uri,
|
||||||
|
[
|
||||||
|
Diagnostic(
|
||||||
|
Range(start, Position(row, len(line))),
|
||||||
|
"`@key` and `@option` used in same definition",
|
||||||
|
severity=DiagnosticSeverity.Error,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
args: list[DocumentSymbol] = []
|
||||||
|
kwargs: list[DocumentSymbol] = []
|
||||||
|
rest: list[DocumentSymbol] = []
|
||||||
|
params_start = found.end() - len(found.group(4))
|
||||||
|
|
||||||
|
for part in finditer(
|
||||||
|
r"(@(option|key)(\s\(\w+\s+.+\))+|@rest \w+|(\w+\s*))",
|
||||||
|
found.group(4),
|
||||||
|
):
|
||||||
|
if part.group(1).startswith("@rest"):
|
||||||
|
rest_var_name = part.group(1).split()[1]
|
||||||
|
rest_var_range = Range(
|
||||||
|
Position(
|
||||||
|
row,
|
||||||
|
params_start + part.end() - len(rest_var_name),
|
||||||
|
),
|
||||||
|
Position(row, params_start + part.end()),
|
||||||
|
)
|
||||||
|
rest.append(
|
||||||
|
DocumentSymbol(
|
||||||
|
rest_var_name,
|
||||||
|
kind=SymbolKind.Variable,
|
||||||
|
range=rest_var_range,
|
||||||
|
selection_range=rest_var_range,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif part.group(1).startswith("@"):
|
||||||
|
for kwarg in finditer(r"(\((\w+)\s+[^\)]+\))", part.group(1)):
|
||||||
|
kwargs.append(
|
||||||
|
DocumentSymbol(
|
||||||
|
kwarg.group(2),
|
||||||
|
kind=SymbolKind.Variable,
|
||||||
|
range=Range(
|
||||||
|
Position(
|
||||||
|
row,
|
||||||
|
params_start + part.start() + kwarg.start(),
|
||||||
|
),
|
||||||
|
Position(
|
||||||
|
row,
|
||||||
|
params_start + part.start() + kwarg.end(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
selection_range=Range(
|
||||||
|
Position(
|
||||||
|
row,
|
||||||
|
params_start + part.start() + kwarg.start(),
|
||||||
|
),
|
||||||
|
Position(
|
||||||
|
row,
|
||||||
|
params_start
|
||||||
|
+ part.start()
|
||||||
|
+ kwarg.start()
|
||||||
|
+ len(kwarg.group(2)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
for arg in finditer(r"(\w+)", part.group(1)):
|
||||||
|
arg_range = Range(
|
||||||
|
Position(
|
||||||
|
row,
|
||||||
|
params_start + part.start() + arg.start() - 1,
|
||||||
|
),
|
||||||
|
Position(
|
||||||
|
row,
|
||||||
|
params_start + part.start() + arg.end() - 1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
args.append(
|
||||||
|
DocumentSymbol(
|
||||||
|
arg.group(1),
|
||||||
|
kind=SymbolKind.Variable,
|
||||||
|
range=arg_range,
|
||||||
|
selection_range=arg_range,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.procs.append(
|
||||||
|
DocumentSymbol(
|
||||||
|
found.group(3),
|
||||||
|
kind=SymbolKind.Function,
|
||||||
|
range=Range(start, end),
|
||||||
|
selection_range=Range(start, Position(row, len(line))),
|
||||||
|
children=args + rest + kwargs,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _hint_let(self) -> Generator[InlayHint, None, None]:
|
||||||
|
for let in self.lets:
|
||||||
|
if let.children:
|
||||||
|
for child in let.children:
|
||||||
|
yield InlayHint(child.selection_range.end, "|l")
|
||||||
|
|
||||||
|
def _hint_proc(self) -> Generator[InlayHint, None, None]:
|
||||||
|
for proc in self.procs:
|
||||||
|
warning(proc)
|
||||||
|
if proc.children:
|
||||||
|
for child in proc.children:
|
||||||
|
yield InlayHint(child.selection_range.end, "|l")
|
||||||
|
|
||||||
|
def _hint_globals(self) -> Generator[InlayHint, None, None]:
|
||||||
|
for glbl in self.globals:
|
||||||
|
yield InlayHint(glbl.selection_range.end, "|g")
|
||||||
|
|
||||||
|
def hint(self, doc: TextDocument, area: Range) -> list[InlayHint]:
|
||||||
|
hints: list[InlayHint] = []
|
||||||
|
hints.extend(self._hint_proc())
|
||||||
|
hints.extend(self._hint_let())
|
||||||
|
hints.extend(self._hint_globals())
|
||||||
|
|
||||||
|
return hints
|
||||||
|
|
||||||
|
|
||||||
|
server = SkillLanguageServer("skillls", "v0.3")
|
||||||
|
|
||||||
|
|
||||||
|
@server.feature(TEXT_DOCUMENT_DID_SAVE)
|
||||||
@server.feature(TEXT_DOCUMENT_DID_OPEN)
|
@server.feature(TEXT_DOCUMENT_DID_OPEN)
|
||||||
def on_open(ls: SkillLanguageServer, params: DidOpenTextDocumentParams) -> None:
|
|
||||||
doc = server.workspace.get_text_document(params.text_document.uri)
|
|
||||||
ls.diagnose(doc)
|
|
||||||
|
|
||||||
|
|
||||||
@server.feature(TEXT_DOCUMENT_DID_CHANGE)
|
@server.feature(TEXT_DOCUMENT_DID_CHANGE)
|
||||||
def on_save(ls: SkillLanguageServer, params: DidChangeTextDocumentParams) -> None:
|
def on_open(ls: SkillLanguageServer, params: DidSaveTextDocumentParams) -> None:
|
||||||
doc = server.workspace.get_text_document(params.text_document.uri)
|
doc = server.workspace.get_text_document(params.text_document.uri)
|
||||||
ls.diagnose(doc)
|
if not ls.diagnose(doc):
|
||||||
|
ls.parse(doc)
|
||||||
|
ls.lsp.send_request_async(WORKSPACE_INLAY_HINT_REFRESH)
|
||||||
|
|
||||||
|
|
||||||
@server.feature(TEXT_DOCUMENT_INLAY_HINT)
|
@server.feature(TEXT_DOCUMENT_INLAY_HINT)
|
||||||
def inlay_hints(ls: SkillLanguageServer, params: InlayHintParams) -> list[InlayHint]:
|
def inlay_hints(ls: SkillLanguageServer, params: InlayHintParams) -> list[InlayHint]:
|
||||||
hints: list[InlayHint] = []
|
doc = server.workspace.get_text_document(params.text_document.uri)
|
||||||
|
return ls.hint(doc, params.range)
|
||||||
|
|
||||||
return hints
|
|
||||||
|
@server.feature(TEXT_DOCUMENT_DOCUMENT_SYMBOL)
|
||||||
|
def doc_symbols(
|
||||||
|
ls: SkillLanguageServer,
|
||||||
|
params: DocumentSymbolParams,
|
||||||
|
) -> list[DocumentSymbol]:
|
||||||
|
return ls.procs + ls.lets + ls.defs + ls.globals
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
Loading…
Reference in New Issue