🐐 LSP
This commit is contained in:
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#! /usr/bin/env python
|
||||||
|
from centvrion.lsp import run
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
@@ -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)
|
||||||
|
)
|
||||||
@@ -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