Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4da250ec85 | |||
| 1d6a93be32 | |||
| 19f8cb5232 |
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#! /usr/bin/env python
|
||||||
|
from centvrion.lsp import run
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
+12
-23
@@ -1287,10 +1287,6 @@ class PerStatement(Node):
|
|||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return type(self) == type(other) and self.data_list == other.data_list and self.variable_name == other.variable_name and self.statements == other.statements
|
return type(self) == type(other) and self.data_list == other.data_list and self.variable_name == other.variable_name and self.statements == other.statements
|
||||||
|
|
||||||
@property
|
|
||||||
def destructure(self):
|
|
||||||
return isinstance(self.variable_name, list)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
test = repr(self.data_list)
|
test = repr(self.data_list)
|
||||||
variable_name = repr(self.variable_name)
|
variable_name = repr(self.variable_name)
|
||||||
@@ -1299,25 +1295,21 @@ class PerStatement(Node):
|
|||||||
return f"Per({dum_string})"
|
return f"Per({dum_string})"
|
||||||
|
|
||||||
def print(self):
|
def print(self):
|
||||||
body = "\n".join(s.print() for s in self.statements)
|
# Re-sugar the parse-time destructure rewrite back into `PER a, b IN ...`
|
||||||
if self.destructure:
|
# so the printed source round-trips through the lexer (which rejects `#`).
|
||||||
var_str = ", ".join(v.print() for v in self.variable_name)
|
if (isinstance(self.variable_name, ID)
|
||||||
|
and self.variable_name.name == "#per_item"
|
||||||
|
and self.statements
|
||||||
|
and isinstance(self.statements[0], DesignaDestructure)
|
||||||
|
and isinstance(self.statements[0].value, ID)
|
||||||
|
and self.statements[0].value.name == "#per_item"):
|
||||||
|
var_str = ", ".join(i.print() for i in self.statements[0].ids)
|
||||||
|
body = "\n".join(s.print() for s in self.statements[1:])
|
||||||
else:
|
else:
|
||||||
var_str = self.variable_name.print()
|
var_str = self.variable_name.print()
|
||||||
|
body = "\n".join(s.print() for s in self.statements)
|
||||||
return f"PER {var_str} IN {self.data_list.print()} FAC {{\n{body}\n}}"
|
return f"PER {var_str} IN {self.data_list.print()} FAC {{\n{body}\n}}"
|
||||||
|
|
||||||
def _assign_loop_var(self, vtable, item):
|
|
||||||
if self.destructure:
|
|
||||||
if not isinstance(item, ValList):
|
|
||||||
raise CentvrionError("Cannot destructure non-array value in PER loop")
|
|
||||||
if len(item.value()) != len(self.variable_name):
|
|
||||||
raise CentvrionError(
|
|
||||||
f"Destructuring mismatch: {len(self.variable_name)} targets, {len(item.value())} values")
|
|
||||||
for id_node, val in zip(self.variable_name, item.value()):
|
|
||||||
vtable[id_node.name] = val
|
|
||||||
else:
|
|
||||||
vtable[self.variable_name.name] = item
|
|
||||||
|
|
||||||
def _eval(self, vtable):
|
def _eval(self, vtable):
|
||||||
vtable, array = self.data_list.eval(vtable)
|
vtable, array = self.data_list.eval(vtable)
|
||||||
if isinstance(array, ValDict):
|
if isinstance(array, ValDict):
|
||||||
@@ -1327,7 +1319,7 @@ class PerStatement(Node):
|
|||||||
raise CentvrionError("PER requires an array or dict")
|
raise CentvrionError("PER requires an array or dict")
|
||||||
last_val = ValNul()
|
last_val = ValNul()
|
||||||
for item in array:
|
for item in array:
|
||||||
self._assign_loop_var(vtable, item)
|
vtable[self.variable_name.name] = item
|
||||||
for statement in self.statements:
|
for statement in self.statements:
|
||||||
vtable, val = statement.eval(vtable)
|
vtable, val = statement.eval(vtable)
|
||||||
if vtable["#break"] or vtable["#continue"] or vtable["#return"] is not None:
|
if vtable["#break"] or vtable["#continue"] or vtable["#return"] is not None:
|
||||||
@@ -1474,9 +1466,6 @@ class BuiltIn(Node):
|
|||||||
)
|
)
|
||||||
print(print_string)
|
print(print_string)
|
||||||
return vtable, ValStr(print_string)
|
return vtable, ValStr(print_string)
|
||||||
case "ERVMPE":
|
|
||||||
vtable["#break"] = True
|
|
||||||
return vtable, ValNul()
|
|
||||||
case "FORTVITVS_NVMERVS":
|
case "FORTVITVS_NVMERVS":
|
||||||
if "FORS" not in vtable["#modules"]:
|
if "FORS" not in vtable["#modules"]:
|
||||||
raise CentvrionError("Cannot use 'FORTVITVS_NVMERVS' without module 'FORS'")
|
raise CentvrionError("Cannot use 'FORTVITVS_NVMERVS' without module 'FORS'")
|
||||||
|
|||||||
@@ -310,11 +310,6 @@ def _emit_builtin(node, ctx):
|
|||||||
else:
|
else:
|
||||||
lines.append(f"CentValue {tmp} = cent_senatus(NULL, 0);")
|
lines.append(f"CentValue {tmp} = cent_senatus(NULL, 0);")
|
||||||
|
|
||||||
case "ERVMPE":
|
|
||||||
# break as expression (side-effecting; result is unused)
|
|
||||||
lines.append("break;")
|
|
||||||
lines.append(f"CentValue {tmp} = cent_null();")
|
|
||||||
|
|
||||||
case "CLAVES":
|
case "CLAVES":
|
||||||
lines.append(f"CentValue {tmp} = cent_dict_keys({param_vars[0]});")
|
lines.append(f"CentValue {tmp} = cent_dict_keys({param_vars[0]});")
|
||||||
|
|
||||||
|
|||||||
@@ -90,43 +90,22 @@ def _emit_stmt_body(node, ctx):
|
|||||||
arr_lines, arr_var = emit_expr(node.data_list, ctx)
|
arr_lines, arr_var = emit_expr(node.data_list, ctx)
|
||||||
i_var = ctx.fresh_tmp()
|
i_var = ctx.fresh_tmp()
|
||||||
body_lines = _emit_body(node.statements, ctx)
|
body_lines = _emit_body(node.statements, ctx)
|
||||||
|
var_name = node.variable_name.name
|
||||||
if node.destructure:
|
lines = arr_lines + [
|
||||||
# Destructuring PER — each element must be a list
|
f"if ({arr_var}.type == CENT_DICT) {{",
|
||||||
elem_var = ctx.fresh_tmp()
|
f" for (int {i_var} = 0; {i_var} < {arr_var}.dval.len; {i_var}++) {{",
|
||||||
assign_lines = [
|
f' cent_scope_set(&_scope, "{var_name}", {arr_var}.dval.keys[{i_var}]);',
|
||||||
f"CentValue {elem_var} = {arr_var}.lval.items[{i_var}];",
|
]
|
||||||
f'if ({elem_var}.type != CENT_LIST) cent_type_error("Cannot destructure non-array value in PER loop");',
|
lines += [f" {l}" for l in body_lines]
|
||||||
f'if ({elem_var}.lval.len != {len(node.variable_name)}) cent_runtime_error("Destructuring mismatch");',
|
lines += [
|
||||||
]
|
" }",
|
||||||
for j, id_node in enumerate(node.variable_name):
|
"} else {",
|
||||||
tmp = ctx.fresh_tmp()
|
f' if ({arr_var}.type != CENT_LIST) cent_type_error("PER requires an array or dict");',
|
||||||
assign_lines.append(f"CentValue {tmp} = cent_list_index({elem_var}, cent_int({j + 1}));")
|
f" for (int {i_var} = 0; {i_var} < {arr_var}.lval.len; {i_var}++) {{",
|
||||||
assign_lines.append(f'cent_scope_set(&_scope, "{id_node.name}", {tmp});')
|
f' cent_scope_set(&_scope, "{var_name}", {arr_var}.lval.items[{i_var}]);',
|
||||||
lines = arr_lines + [
|
]
|
||||||
f'if ({arr_var}.type != CENT_LIST) cent_type_error("PER requires an array");',
|
lines += [f" {l}" for l in body_lines]
|
||||||
f"for (int {i_var} = 0; {i_var} < {arr_var}.lval.len; {i_var}++) {{",
|
lines += [" }", "}"]
|
||||||
]
|
|
||||||
lines += [f" {l}" for l in assign_lines]
|
|
||||||
lines += [f" {l}" for l in body_lines]
|
|
||||||
lines += ["}"]
|
|
||||||
else:
|
|
||||||
var_name = node.variable_name.name
|
|
||||||
lines = arr_lines + [
|
|
||||||
f"if ({arr_var}.type == CENT_DICT) {{",
|
|
||||||
f" for (int {i_var} = 0; {i_var} < {arr_var}.dval.len; {i_var}++) {{",
|
|
||||||
f' cent_scope_set(&_scope, "{var_name}", {arr_var}.dval.keys[{i_var}]);',
|
|
||||||
]
|
|
||||||
lines += [f" {l}" for l in body_lines]
|
|
||||||
lines += [
|
|
||||||
" }",
|
|
||||||
"} else {",
|
|
||||||
f' if ({arr_var}.type != CENT_LIST) cent_type_error("PER requires an array or dict");',
|
|
||||||
f" for (int {i_var} = 0; {i_var} < {arr_var}.lval.len; {i_var}++) {{",
|
|
||||||
f' cent_scope_set(&_scope, "{var_name}", {arr_var}.lval.items[{i_var}]);',
|
|
||||||
]
|
|
||||||
lines += [f" {l}" for l in body_lines]
|
|
||||||
lines += [" }", "}"]
|
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
if isinstance(node, Defini):
|
if isinstance(node, Defini):
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
"""CENTVRION language server package."""
|
||||||
|
|
||||||
|
def run() -> None:
|
||||||
|
"""Start the LSP server over stdio. Blocks until stdin closes."""
|
||||||
|
from centvrion.lsp.server import create_server
|
||||||
|
create_server().start_io()
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
"""Pure analysis helpers over the CENTVRION AST.
|
||||||
|
|
||||||
|
Functions here take source text and zero-based (line, character) LSP positions,
|
||||||
|
and return plain Python values. No pygls types. The server layer adapts these
|
||||||
|
to LSP messages.
|
||||||
|
"""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Iterator, Optional, Union
|
||||||
|
|
||||||
|
from rply.errors import LexingError
|
||||||
|
|
||||||
|
from centvrion import ast_nodes
|
||||||
|
from centvrion.ast_nodes import (
|
||||||
|
Defini, Designa, DesignaDestructure, DesignaIndex, Fvnctio, ID, Invoca,
|
||||||
|
Program,
|
||||||
|
)
|
||||||
|
from centvrion.lexer import Lexer
|
||||||
|
from centvrion.parser import Parser
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ParseFailure:
|
||||||
|
"""Lex or parse error with source position.
|
||||||
|
|
||||||
|
line/character are zero-based (LSP convention). length is the number of
|
||||||
|
characters the squiggle should cover (always >= 1).
|
||||||
|
"""
|
||||||
|
line: int
|
||||||
|
character: int
|
||||||
|
length: int
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
def parse(source: str) -> Union[Program, ParseFailure]:
|
||||||
|
lexer = Lexer().get_lexer()
|
||||||
|
try:
|
||||||
|
tokens = lexer.lex(source + "\n")
|
||||||
|
program = Parser().parse(tokens)
|
||||||
|
except LexingError as e:
|
||||||
|
# rply's colno is sometimes off by one for the failing character, but idx
|
||||||
|
# is reliable, so compute (line, col) from idx.
|
||||||
|
line, character = _idx_to_line_col(source, e.source_pos.idx)
|
||||||
|
bad = source[e.source_pos.idx] if e.source_pos.idx < len(source) else "?"
|
||||||
|
return ParseFailure(
|
||||||
|
line=line,
|
||||||
|
character=character,
|
||||||
|
length=1,
|
||||||
|
message=f"Invalid character {bad!r}",
|
||||||
|
)
|
||||||
|
except SyntaxError as e:
|
||||||
|
line, character = _extract_pos(str(e))
|
||||||
|
return ParseFailure(
|
||||||
|
line=line,
|
||||||
|
character=character,
|
||||||
|
length=1,
|
||||||
|
message=str(e),
|
||||||
|
)
|
||||||
|
if not isinstance(program, Program):
|
||||||
|
return ParseFailure(0, 0, 1, "Parser did not return a Program")
|
||||||
|
return program
|
||||||
|
|
||||||
|
|
||||||
|
def _idx_to_line_col(source: str, idx: int) -> tuple[int, int]:
|
||||||
|
if idx < 0:
|
||||||
|
return 0, 0
|
||||||
|
prefix = source[:idx]
|
||||||
|
line = prefix.count("\n")
|
||||||
|
last_nl = prefix.rfind("\n")
|
||||||
|
character = idx - (last_nl + 1) if last_nl >= 0 else idx
|
||||||
|
return line, character
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_pos(msg: str) -> tuple[int, int]:
|
||||||
|
"""Pull (line, col) out of the parser's 'at line N, column M' error format.
|
||||||
|
|
||||||
|
The CENTVRION parser's error handler (parser.py:476) builds messages like
|
||||||
|
'Unexpected token KEYWORD_X at line 3, column 5'. Falls back to (0, 0).
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
m = re.search(r"line (\d+), column (\d+)", msg)
|
||||||
|
if m:
|
||||||
|
return max(int(m.group(1)) - 1, 0), max(int(m.group(2)) - 1, 0)
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
|
|
||||||
|
def walk(node) -> Iterator:
|
||||||
|
"""Yield every AST node reachable from `node`, including `node` itself.
|
||||||
|
|
||||||
|
Traverses any attribute that is a Node, ast_nodes.Program, list of those, or
|
||||||
|
list of (key, value) pairs. Sufficient for every concrete node in
|
||||||
|
ast_nodes.py.
|
||||||
|
"""
|
||||||
|
if node is None:
|
||||||
|
return
|
||||||
|
yield node
|
||||||
|
for value in vars(node).values():
|
||||||
|
yield from _walk_value(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _walk_value(value):
|
||||||
|
if value is None:
|
||||||
|
return
|
||||||
|
if isinstance(value, (ast_nodes.Node, Program)):
|
||||||
|
yield from walk(value)
|
||||||
|
elif isinstance(value, list):
|
||||||
|
for item in value:
|
||||||
|
if isinstance(item, tuple):
|
||||||
|
for sub in item:
|
||||||
|
yield from _walk_value(sub)
|
||||||
|
else:
|
||||||
|
yield from _walk_value(item)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Definition:
|
||||||
|
"""A symbol definition with its source position (zero-based)."""
|
||||||
|
name: str
|
||||||
|
line: int
|
||||||
|
character: int
|
||||||
|
kind: str # "function" or "variable"
|
||||||
|
|
||||||
|
|
||||||
|
def collect_definitions(program: Program) -> dict[str, Definition]:
|
||||||
|
"""Return name -> Definition for every Defini and Designa-style assignment.
|
||||||
|
|
||||||
|
Functions take precedence over variables when names collide; among
|
||||||
|
variables, the first assignment wins (CENTVRION re-assigns with the same
|
||||||
|
DESIGNA keyword, but go-to-def should land on the introducing site).
|
||||||
|
"""
|
||||||
|
funcs: dict[str, Definition] = {}
|
||||||
|
vars_: dict[str, Definition] = {}
|
||||||
|
for node in walk(program):
|
||||||
|
if isinstance(node, Defini) and node.name.pos is not None:
|
||||||
|
line, character = _to_zero_based(node.name.pos)
|
||||||
|
funcs.setdefault(node.name.name, Definition(
|
||||||
|
name=node.name.name, line=line, character=character, kind="function",
|
||||||
|
))
|
||||||
|
elif isinstance(node, (Designa, DesignaIndex)) and node.id.pos is not None:
|
||||||
|
line, character = _to_zero_based(node.id.pos)
|
||||||
|
vars_.setdefault(node.id.name, Definition(
|
||||||
|
name=node.id.name, line=line, character=character, kind="variable",
|
||||||
|
))
|
||||||
|
elif isinstance(node, DesignaDestructure):
|
||||||
|
for id_node in node.ids:
|
||||||
|
if id_node.pos is not None:
|
||||||
|
line, character = _to_zero_based(id_node.pos)
|
||||||
|
vars_.setdefault(id_node.name, Definition(
|
||||||
|
name=id_node.name, line=line, character=character, kind="variable",
|
||||||
|
))
|
||||||
|
out = dict(vars_)
|
||||||
|
out.update(funcs)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def definition_at(source: str, line: int, character: int) -> Optional[Definition]:
|
||||||
|
"""Resolve an identifier at (line, character) to its definition site.
|
||||||
|
|
||||||
|
Re-lexes (so this still works while the file fails to parse for unrelated
|
||||||
|
reasons), finds an ID token under the cursor, then looks it up against the
|
||||||
|
AST's definition table. Returns None if no identifier is at that position,
|
||||||
|
if parsing fails outright, or if the name has no definition.
|
||||||
|
"""
|
||||||
|
token = token_at(source, line, character)
|
||||||
|
if token is None or token.gettokentype() != "ID":
|
||||||
|
return None
|
||||||
|
result = parse(source)
|
||||||
|
if isinstance(result, ParseFailure):
|
||||||
|
return None
|
||||||
|
return collect_definitions(result).get(token.getstr())
|
||||||
|
|
||||||
|
|
||||||
|
_WORD_TOKENS = ("ID", "DATA_NUMERAL", "BUILTIN", "MODULE")
|
||||||
|
|
||||||
|
|
||||||
|
def token_at(source: str, line: int, character: int):
|
||||||
|
"""Return the rply Token under the cursor at (line, character), or None.
|
||||||
|
|
||||||
|
Cursor position N (0-based) sits between characters N-1 and N, so a token
|
||||||
|
covering character columns [start, end) matches for any cursor in
|
||||||
|
[start, end] (end-inclusive). When two tokens touch the cursor at their
|
||||||
|
shared boundary (e.g. cursor between `digits` and `)`), prefer the
|
||||||
|
identifier-like one so go-to-def behaves the way users expect from other
|
||||||
|
editors. Tolerant of LexingError: works on whatever tokens were produced
|
||||||
|
before the failure.
|
||||||
|
"""
|
||||||
|
lexer = Lexer().get_lexer()
|
||||||
|
try:
|
||||||
|
tokens = list(lexer.lex(source + "\n"))
|
||||||
|
except LexingError:
|
||||||
|
return None
|
||||||
|
target_line = line + 1
|
||||||
|
target_col = character + 1
|
||||||
|
candidates = []
|
||||||
|
for tok in tokens:
|
||||||
|
sp = tok.source_pos
|
||||||
|
if sp is None or sp.lineno != target_line:
|
||||||
|
continue
|
||||||
|
start = sp.colno
|
||||||
|
end = start + len(tok.getstr())
|
||||||
|
if start <= target_col <= end:
|
||||||
|
candidates.append(tok)
|
||||||
|
for tok in candidates:
|
||||||
|
if tok.gettokentype() in _WORD_TOKENS:
|
||||||
|
return tok
|
||||||
|
return candidates[0] if candidates else None
|
||||||
|
|
||||||
|
|
||||||
|
def _to_zero_based(pos: tuple[int, int]) -> tuple[int, int]:
|
||||||
|
"""rply (lineno, colno) is 1-based; LSP wants 0-based (line, character)."""
|
||||||
|
return pos[0] - 1, pos[1] - 1
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
"""Convert CENTVRION parse failures into LSP-style diagnostic dicts.
|
||||||
|
|
||||||
|
Returns a list of plain dicts so the server layer can map them to
|
||||||
|
lsprotocol types without this module importing pygls. Runtime errors
|
||||||
|
are explicitly out of scope: we only catch lex and parse failures.
|
||||||
|
"""
|
||||||
|
from centvrion.lsp.analysis import ParseFailure, parse
|
||||||
|
|
||||||
|
|
||||||
|
def build_diagnostics(source: str) -> list[dict]:
|
||||||
|
result = parse(source)
|
||||||
|
if not isinstance(result, ParseFailure):
|
||||||
|
return []
|
||||||
|
return [{
|
||||||
|
"line": result.line,
|
||||||
|
"character": result.character,
|
||||||
|
"length": result.length,
|
||||||
|
"message": result.message,
|
||||||
|
"severity": 1, # Error
|
||||||
|
"source": "centvrion",
|
||||||
|
}]
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""Hover information for the LSP server.
|
||||||
|
|
||||||
|
Currently only handles Roman numeral literals: hovering a DATA_NUMERAL token
|
||||||
|
shows its decimal value. The MAGNVM / SVBNVLLA module gating is bypassed for
|
||||||
|
display purposes; if the user has written it, we'll show the decimal.
|
||||||
|
"""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from centvrion.ast_nodes import num_to_int
|
||||||
|
from centvrion.errors import CentvrionError
|
||||||
|
|
||||||
|
from centvrion.lsp.analysis import token_at
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HoverInfo:
|
||||||
|
text: str
|
||||||
|
line: int
|
||||||
|
start_character: int
|
||||||
|
end_character: int
|
||||||
|
|
||||||
|
|
||||||
|
def hover_at(source: str, line: int, character: int) -> Optional[HoverInfo]:
|
||||||
|
token = token_at(source, line, character)
|
||||||
|
if token is None or token.gettokentype() != "DATA_NUMERAL":
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
value = num_to_int(token.getstr(), m=True, s=True)
|
||||||
|
except CentvrionError:
|
||||||
|
return None
|
||||||
|
start_col = token.source_pos.colno - 1
|
||||||
|
return HoverInfo(
|
||||||
|
text=f"`{token.getstr()}` = **{value}**",
|
||||||
|
line=line,
|
||||||
|
start_character=start_col,
|
||||||
|
end_character=start_col + len(token.getstr()),
|
||||||
|
)
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
"""pygls language server for CENTVRION.
|
||||||
|
|
||||||
|
Thin adapter: protocol handlers convert LSP params to source + position,
|
||||||
|
delegate to the pure analysis/hover/diagnostics modules, and convert results
|
||||||
|
back to lsprotocol types.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import lsprotocol.types as t
|
||||||
|
from pygls.lsp.server import LanguageServer
|
||||||
|
|
||||||
|
from centvrion.lsp import analysis, diagnostics, hover
|
||||||
|
|
||||||
|
|
||||||
|
_DEBOUNCE_SECONDS = 0.15
|
||||||
|
_pending: dict[str, asyncio.Task] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def create_server() -> LanguageServer:
|
||||||
|
ls = LanguageServer("centvrion-lsp", "0.1.0")
|
||||||
|
|
||||||
|
@ls.feature(t.TEXT_DOCUMENT_DID_OPEN)
|
||||||
|
def did_open(params: t.DidOpenTextDocumentParams):
|
||||||
|
_publish(ls, params.text_document.uri)
|
||||||
|
|
||||||
|
@ls.feature(t.TEXT_DOCUMENT_DID_CHANGE)
|
||||||
|
def did_change(params: t.DidChangeTextDocumentParams):
|
||||||
|
uri = params.text_document.uri
|
||||||
|
task = _pending.pop(uri, None)
|
||||||
|
if task is not None and not task.done():
|
||||||
|
task.cancel()
|
||||||
|
_pending[uri] = asyncio.get_event_loop().create_task(_debounced_publish(ls, uri))
|
||||||
|
|
||||||
|
@ls.feature(t.TEXT_DOCUMENT_HOVER)
|
||||||
|
def on_hover(params: t.HoverParams):
|
||||||
|
doc = ls.workspace.get_text_document(params.text_document.uri)
|
||||||
|
info = hover.hover_at(doc.source, params.position.line, params.position.character)
|
||||||
|
if info is None:
|
||||||
|
return None
|
||||||
|
return t.Hover(
|
||||||
|
contents=t.MarkupContent(kind=t.MarkupKind.Markdown, value=info.text),
|
||||||
|
range=t.Range(
|
||||||
|
start=t.Position(line=info.line, character=info.start_character),
|
||||||
|
end=t.Position(line=info.line, character=info.end_character),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@ls.feature(t.TEXT_DOCUMENT_DEFINITION)
|
||||||
|
def on_definition(params: t.DefinitionParams):
|
||||||
|
doc = ls.workspace.get_text_document(params.text_document.uri)
|
||||||
|
defn = analysis.definition_at(doc.source, params.position.line, params.position.character)
|
||||||
|
if defn is None:
|
||||||
|
return None
|
||||||
|
return t.Location(
|
||||||
|
uri=params.text_document.uri,
|
||||||
|
range=t.Range(
|
||||||
|
start=t.Position(line=defn.line, character=defn.character),
|
||||||
|
end=t.Position(line=defn.line, character=defn.character + len(defn.name)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return ls
|
||||||
|
|
||||||
|
|
||||||
|
async def _debounced_publish(ls: LanguageServer, uri: str) -> None:
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(_DEBOUNCE_SECONDS)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
_publish(ls, uri)
|
||||||
|
|
||||||
|
|
||||||
|
def _publish(ls: LanguageServer, uri: str) -> None:
|
||||||
|
doc = ls.workspace.get_text_document(uri)
|
||||||
|
diags = [
|
||||||
|
t.Diagnostic(
|
||||||
|
range=t.Range(
|
||||||
|
start=t.Position(line=d["line"], character=d["character"]),
|
||||||
|
end=t.Position(line=d["line"], character=d["character"] + d["length"]),
|
||||||
|
),
|
||||||
|
message=d["message"],
|
||||||
|
severity=t.DiagnosticSeverity(d["severity"]),
|
||||||
|
source=d["source"],
|
||||||
|
)
|
||||||
|
for d in diagnostics.build_diagnostics(doc.source)
|
||||||
|
]
|
||||||
|
ls.text_document_publish_diagnostics(
|
||||||
|
t.PublishDiagnosticsParams(uri=uri, diagnostics=diags)
|
||||||
|
)
|
||||||
+5
-1
@@ -284,7 +284,11 @@ class Parser():
|
|||||||
|
|
||||||
@self.pg.production('per_statement : KEYWORD_PER id SYMBOL_COMMA id_list_rest KEYWORD_IN expression KEYWORD_FAC SYMBOL_LCURL statements SYMBOL_RCURL')
|
@self.pg.production('per_statement : KEYWORD_PER id SYMBOL_COMMA id_list_rest KEYWORD_IN expression KEYWORD_FAC SYMBOL_LCURL statements SYMBOL_RCURL')
|
||||||
def per_destructure(tokens):
|
def per_destructure(tokens):
|
||||||
return _at(ast_nodes.PerStatement(tokens[5], [tokens[1]] + tokens[3], tokens[8]), tokens[0])
|
ids = [tokens[1]] + tokens[3]
|
||||||
|
sentinel = ast_nodes.ID("#per_item")
|
||||||
|
destructure = _at(ast_nodes.DesignaDestructure(ids, sentinel), tokens[0])
|
||||||
|
body = [destructure] + tokens[8]
|
||||||
|
return _at(ast_nodes.PerStatement(tokens[5], sentinel, body), tokens[0])
|
||||||
|
|
||||||
@self.pg.production('per_statement : KEYWORD_PER id KEYWORD_IN expression KEYWORD_FAC SYMBOL_LCURL statements SYMBOL_RCURL')
|
@self.pg.production('per_statement : KEYWORD_PER id KEYWORD_IN expression KEYWORD_FAC SYMBOL_LCURL statements SYMBOL_RCURL')
|
||||||
def per(tokens):
|
def per(tokens):
|
||||||
|
|||||||
@@ -138,15 +138,21 @@ control_tests = [
|
|||||||
("PER a, b IN [[I, II], [III, IV]] FAC { DIC(a + b) }",
|
("PER a, b IN [[I, II], [III, IV]] FAC { DIC(a + b) }",
|
||||||
Program([], [PerStatement(
|
Program([], [PerStatement(
|
||||||
DataArray([DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("III"), Numeral("IV")])]),
|
DataArray([DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("III"), Numeral("IV")])]),
|
||||||
[ID("a"), ID("b")],
|
ID("#per_item"),
|
||||||
[ExpressionStatement(BuiltIn("DIC", [BinOp(ID("a"), ID("b"), "SYMBOL_PLUS")]))])]),
|
[
|
||||||
|
DesignaDestructure([ID("a"), ID("b")], ID("#per_item")),
|
||||||
|
ExpressionStatement(BuiltIn("DIC", [BinOp(ID("a"), ID("b"), "SYMBOL_PLUS")])),
|
||||||
|
])]),
|
||||||
ValStr("VII"), "III\nVII\n"),
|
ValStr("VII"), "III\nVII\n"),
|
||||||
# PER destructuring: three variables
|
# PER destructuring: three variables
|
||||||
("PER a, b, c IN [[I, II, III]] FAC { DIC(a + b + c) }",
|
("PER a, b, c IN [[I, II, III]] FAC { DIC(a + b + c) }",
|
||||||
Program([], [PerStatement(
|
Program([], [PerStatement(
|
||||||
DataArray([DataArray([Numeral("I"), Numeral("II"), Numeral("III")])]),
|
DataArray([DataArray([Numeral("I"), Numeral("II"), Numeral("III")])]),
|
||||||
[ID("a"), ID("b"), ID("c")],
|
ID("#per_item"),
|
||||||
[ExpressionStatement(BuiltIn("DIC", [BinOp(BinOp(ID("a"), ID("b"), "SYMBOL_PLUS"), ID("c"), "SYMBOL_PLUS")]))])]),
|
[
|
||||||
|
DesignaDestructure([ID("a"), ID("b"), ID("c")], ID("#per_item")),
|
||||||
|
ExpressionStatement(BuiltIn("DIC", [BinOp(BinOp(ID("a"), ID("b"), "SYMBOL_PLUS"), ID("c"), "SYMBOL_PLUS")])),
|
||||||
|
])]),
|
||||||
ValStr("VI"), "VI\n"),
|
ValStr("VI"), "VI\n"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -287,8 +293,9 @@ loop_edge_tests = [
|
|||||||
Designa(ID("r"), Numeral("I")),
|
Designa(ID("r"), Numeral("I")),
|
||||||
PerStatement(
|
PerStatement(
|
||||||
DataArray([DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("III"), Numeral("IV")]), DataArray([Numeral("V"), Numeral("VI")])]),
|
DataArray([DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("III"), Numeral("IV")]), DataArray([Numeral("V"), Numeral("VI")])]),
|
||||||
[ID("a"), ID("b")],
|
ID("#per_item"),
|
||||||
[SiStatement(BinOp(ID("a"), Numeral("III"), "KEYWORD_EST"), [Erumpe()], None),
|
[DesignaDestructure([ID("a"), ID("b")], ID("#per_item")),
|
||||||
|
SiStatement(BinOp(ID("a"), Numeral("III"), "KEYWORD_EST"), [Erumpe()], None),
|
||||||
Designa(ID("r"), BinOp(BinOp(ID("r"), ID("a"), "SYMBOL_PLUS"), ID("b"), "SYMBOL_PLUS"))],
|
Designa(ID("r"), BinOp(BinOp(ID("r"), ID("a"), "SYMBOL_PLUS"), ID("b"), "SYMBOL_PLUS"))],
|
||||||
),
|
),
|
||||||
ExpressionStatement(ID("r")),
|
ExpressionStatement(ID("r")),
|
||||||
@@ -300,8 +307,9 @@ loop_edge_tests = [
|
|||||||
Defini(ID("f"), [],
|
Defini(ID("f"), [],
|
||||||
[PerStatement(
|
[PerStatement(
|
||||||
DataArray([DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("III"), Numeral("IV")])]),
|
DataArray([DataArray([Numeral("I"), Numeral("II")]), DataArray([Numeral("III"), Numeral("IV")])]),
|
||||||
[ID("a"), ID("b")],
|
ID("#per_item"),
|
||||||
[SiStatement(BinOp(ID("a"), Numeral("III"), "KEYWORD_EST"), [Redi([ID("b")])], None)],
|
[DesignaDestructure([ID("a"), ID("b")], ID("#per_item")),
|
||||||
|
SiStatement(BinOp(ID("a"), Numeral("III"), "KEYWORD_EST"), [Redi([ID("b")])], None)],
|
||||||
)]),
|
)]),
|
||||||
ExpressionStatement(Invoca(ID("f"), [])),
|
ExpressionStatement(Invoca(ID("f"), [])),
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ error_tests = [
|
|||||||
("SI [] TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: empty list
|
("SI [] TVNC { DESIGNA r VT I }", CentvrionError), # non-bool SI condition: empty list
|
||||||
("DESIGNA x VT I\nDVM x FAC {\nDESIGNA x VT x + I\n}", CentvrionError), # non-bool DVM condition: int
|
("DESIGNA x VT I\nDVM x FAC {\nDESIGNA x VT x + I\n}", CentvrionError), # non-bool DVM condition: int
|
||||||
("NON I", CentvrionError), # NON on integer
|
("NON I", CentvrionError), # NON on integer
|
||||||
|
("ERVMPE()", SyntaxError), # ERVMPE is statement-only
|
||||||
|
("CONTINVA()", SyntaxError), # CONTINVA is statement-only
|
||||||
("DESIGNA z VT I - I\nNON z", CentvrionError), # NON on zero integer
|
("DESIGNA z VT I - I\nNON z", CentvrionError), # NON on zero integer
|
||||||
('NON "hello"', CentvrionError), # NON on string
|
('NON "hello"', CentvrionError), # NON on string
|
||||||
("DESIGNA a, b VT III", CentvrionError), # destructure non-array
|
("DESIGNA a, b VT III", CentvrionError), # destructure non-array
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from parameterized import parameterized
|
||||||
|
|
||||||
|
from centvrion.lsp.analysis import (
|
||||||
|
Definition, ParseFailure, collect_definitions, definition_at, parse,
|
||||||
|
)
|
||||||
|
from centvrion.lsp.diagnostics import build_diagnostics
|
||||||
|
from centvrion.lsp.hover import hover_at
|
||||||
|
|
||||||
|
|
||||||
|
# --- Diagnostics ---
|
||||||
|
# (source, expected_diagnostic_count, expected_first_position_or_None)
|
||||||
|
# Positions are 0-based (line, character).
|
||||||
|
|
||||||
|
diagnostic_tests = [
|
||||||
|
("DESIGNA foo VT X", 0, None),
|
||||||
|
("DIC(\"hello\")", 0, None),
|
||||||
|
("DEFINI greet (x) VT {\n DIC(x)\n}", 0, None),
|
||||||
|
("DESIGNA foo VT @@@", 1, (0, 15)),
|
||||||
|
("DESIGNA foo VT", 1, (0, 14)),
|
||||||
|
("DIC(\"unclosed", 1, (0, 4)),
|
||||||
|
]
|
||||||
|
|
||||||
|
class TestDiagnostics(unittest.TestCase):
|
||||||
|
@parameterized.expand(diagnostic_tests)
|
||||||
|
def test_diagnostics(self, source, expected_count, expected_pos):
|
||||||
|
diags = build_diagnostics(source)
|
||||||
|
self.assertEqual(len(diags), expected_count)
|
||||||
|
if expected_pos is not None:
|
||||||
|
self.assertEqual((diags[0]["line"], diags[0]["character"]), expected_pos)
|
||||||
|
self.assertEqual(diags[0]["severity"], 1)
|
||||||
|
self.assertEqual(diags[0]["source"], "centvrion")
|
||||||
|
|
||||||
|
def test_parse_returns_program_on_success(self):
|
||||||
|
result = parse("DIC(\"hi\")")
|
||||||
|
self.assertNotIsInstance(result, ParseFailure)
|
||||||
|
|
||||||
|
def test_parse_returns_failure_on_error(self):
|
||||||
|
result = parse("DESIGNA")
|
||||||
|
self.assertIsInstance(result, ParseFailure)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Hover ---
|
||||||
|
# (source, line, char, expected_text_substring_or_None)
|
||||||
|
|
||||||
|
hover_tests = [
|
||||||
|
("DIC(X)", 0, 4, "10"),
|
||||||
|
("DIC(III)", 0, 4, "3"),
|
||||||
|
("DIC(MMXXV)", 0, 4, "2025"),
|
||||||
|
("DIC(IV)", 0, 4, "4"),
|
||||||
|
("DIC(X)", 0, 0, None),
|
||||||
|
("DESIGNA foo VT X", 0, 8, None),
|
||||||
|
("DESIGNA foo VT X\nDIC(@@@", 0, 15, "10"),
|
||||||
|
]
|
||||||
|
|
||||||
|
class TestHover(unittest.TestCase):
|
||||||
|
@parameterized.expand(hover_tests)
|
||||||
|
def test_hover(self, source, line, char, expected):
|
||||||
|
info = hover_at(source, line, char)
|
||||||
|
if expected is None:
|
||||||
|
self.assertIsNone(info)
|
||||||
|
else:
|
||||||
|
self.assertIsNotNone(info)
|
||||||
|
self.assertIn(expected, info.text)
|
||||||
|
|
||||||
|
def test_hover_range_covers_numeral(self):
|
||||||
|
info = hover_at("DIC(MMXXV)", 0, 4)
|
||||||
|
self.assertEqual(info.line, 0)
|
||||||
|
self.assertEqual(info.start_character, 4)
|
||||||
|
self.assertEqual(info.end_character, 9)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Definitions / go-to-def ---
|
||||||
|
|
||||||
|
definition_tests = [
|
||||||
|
# variable use in DIC -> DESIGNA site
|
||||||
|
("DESIGNA foo VT X\nDIC(foo)", 1, 4, ("foo", 0, 8, "variable")),
|
||||||
|
# function call -> DEFINI site
|
||||||
|
("DEFINI greet (x) VT {\n DIC(x)\n}\nINVOCA greet (V)", 3, 8, ("greet", 0, 7, "function")),
|
||||||
|
# parameter inside body -> not a definition site we track; returns None
|
||||||
|
("DEFINI greet (x) VT {\n DIC(x)\n}", 1, 6, None),
|
||||||
|
# undefined identifier
|
||||||
|
("DIC(missing)", 0, 4, None),
|
||||||
|
# destructuring assignment
|
||||||
|
("DESIGNA a, b VT [I, II]\nDIC(a)\nDIC(b)", 1, 4, ("a", 0, 8, "variable")),
|
||||||
|
("DESIGNA a, b VT [I, II]\nDIC(a)\nDIC(b)", 2, 4, ("b", 0, 11, "variable")),
|
||||||
|
# cursor right after identifier (typical F12 position): the identifier wins
|
||||||
|
# over the adjacent punctuation, matching standard editor behavior.
|
||||||
|
("DESIGNA foo VT X\nDIC(foo)", 1, 7, ("foo", 0, 8, "variable")),
|
||||||
|
]
|
||||||
|
|
||||||
|
class TestDefinition(unittest.TestCase):
|
||||||
|
@parameterized.expand(definition_tests)
|
||||||
|
def test_definition(self, source, line, char, expected):
|
||||||
|
result = definition_at(source, line, char)
|
||||||
|
if expected is None:
|
||||||
|
self.assertIsNone(result)
|
||||||
|
else:
|
||||||
|
name, exp_line, exp_char, kind = expected
|
||||||
|
self.assertEqual(
|
||||||
|
result,
|
||||||
|
Definition(name=name, line=exp_line, character=exp_char, kind=kind),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_functions_shadow_variables_with_same_name(self):
|
||||||
|
src = "DESIGNA bar VT X\nDEFINI bar () VT {\n DIC(I)\n}\nINVOCA bar ()"
|
||||||
|
defs = collect_definitions(parse(src))
|
||||||
|
self.assertEqual(defs["bar"].kind, "function")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
out/
|
||||||
|
*.vsix
|
||||||
@@ -2,3 +2,7 @@
|
|||||||
.vscode-test/**
|
.vscode-test/**
|
||||||
.gitignore
|
.gitignore
|
||||||
vsc-extension-quickstart.md
|
vsc-extension-quickstart.md
|
||||||
|
src/**
|
||||||
|
tsconfig.json
|
||||||
|
node_modules/**
|
||||||
|
out/**/*.map
|
||||||
|
|||||||
Generated
+139
@@ -0,0 +1,139 @@
|
|||||||
|
{
|
||||||
|
"name": "centvrion",
|
||||||
|
"version": "0.0.2",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "centvrion",
|
||||||
|
"version": "0.0.2",
|
||||||
|
"dependencies": {
|
||||||
|
"vscode-languageclient": "^9.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"@types/vscode": "^1.68.0",
|
||||||
|
"typescript": "^5.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"vscode": "^1.68.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.19.41",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
|
||||||
|
"integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/vscode": {
|
||||||
|
"version": "1.120.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.120.0.tgz",
|
||||||
|
"integrity": "sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/balanced-match": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/brace-expansion": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minimatch": {
|
||||||
|
"version": "5.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
|
||||||
|
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "7.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
|
||||||
|
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/vscode-jsonrpc": {
|
||||||
|
"version": "8.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
|
||||||
|
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-languageclient": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"minimatch": "^5.1.0",
|
||||||
|
"semver": "^7.3.7",
|
||||||
|
"vscode-languageserver-protocol": "3.17.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"vscode": "^1.82.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-languageserver-protocol": {
|
||||||
|
"version": "3.17.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
|
||||||
|
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"vscode-jsonrpc": "8.2.0",
|
||||||
|
"vscode-languageserver-types": "3.17.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-languageserver-types": {
|
||||||
|
"version": "3.17.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
|
||||||
|
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,13 +2,17 @@
|
|||||||
"name": "centvrion",
|
"name": "centvrion",
|
||||||
"displayName": "centvrion",
|
"displayName": "centvrion",
|
||||||
"description": "Latin-inspired esoteric programming language with Roman numeral literals",
|
"description": "Latin-inspired esoteric programming language with Roman numeral literals",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.68.0"
|
"vscode": "^1.68.0"
|
||||||
},
|
},
|
||||||
"categories": [
|
"categories": [
|
||||||
"Programming Languages"
|
"Programming Languages"
|
||||||
],
|
],
|
||||||
|
"main": "./out/extension.js",
|
||||||
|
"activationEvents": [
|
||||||
|
"onLanguage:cent"
|
||||||
|
],
|
||||||
"contributes": {
|
"contributes": {
|
||||||
"languages": [{
|
"languages": [{
|
||||||
"id": "cent",
|
"id": "cent",
|
||||||
@@ -24,6 +28,28 @@
|
|||||||
"snippets": [{
|
"snippets": [{
|
||||||
"language": "cent",
|
"language": "cent",
|
||||||
"path": "./snippets/cent.json"
|
"path": "./snippets/cent.json"
|
||||||
}]
|
}],
|
||||||
|
"configuration": {
|
||||||
|
"title": "CENTVRION",
|
||||||
|
"properties": {
|
||||||
|
"centvrion.languageServer.path": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "centvrion-lsp",
|
||||||
|
"description": "Path to the centvrion-lsp executable (or just the name if on PATH)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"compile": "tsc -p ./",
|
||||||
|
"watch": "tsc -watch -p ./"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vscode-languageclient": "^9.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"@types/vscode": "^1.68.0",
|
||||||
|
"typescript": "^5.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import { workspace, ExtensionContext } from "vscode";
|
||||||
|
import {
|
||||||
|
LanguageClient,
|
||||||
|
LanguageClientOptions,
|
||||||
|
ServerOptions,
|
||||||
|
TransportKind,
|
||||||
|
} from "vscode-languageclient/node";
|
||||||
|
|
||||||
|
let client: LanguageClient | undefined;
|
||||||
|
|
||||||
|
const DEFAULT_COMMAND = "centvrion-lsp";
|
||||||
|
|
||||||
|
function resolveServerCommand(): string {
|
||||||
|
const configured = workspace
|
||||||
|
.getConfiguration("centvrion")
|
||||||
|
.get<string>("languageServer.path", DEFAULT_COMMAND);
|
||||||
|
if (configured !== DEFAULT_COMMAND) {
|
||||||
|
return configured;
|
||||||
|
}
|
||||||
|
for (const folder of workspace.workspaceFolders ?? []) {
|
||||||
|
const candidate = path.join(folder.uri.fsPath, "centvrion-lsp");
|
||||||
|
if (fs.existsSync(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return configured;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function activate(_context: ExtensionContext) {
|
||||||
|
const command = resolveServerCommand();
|
||||||
|
|
||||||
|
const serverOptions: ServerOptions = {
|
||||||
|
command,
|
||||||
|
transport: TransportKind.stdio,
|
||||||
|
};
|
||||||
|
|
||||||
|
const clientOptions: LanguageClientOptions = {
|
||||||
|
documentSelector: [{ scheme: "file", language: "cent" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
client = new LanguageClient(
|
||||||
|
"centvrion",
|
||||||
|
"CENTVRION Language Server",
|
||||||
|
serverOptions,
|
||||||
|
clientOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
client.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deactivate(): Thenable<void> | undefined {
|
||||||
|
return client?.stop();
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "ES2020",
|
||||||
|
"outDir": "out",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"sourceMap": true,
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", ".vscode-test"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user