This commit is contained in:
2026-05-30 19:07:01 +02:00
parent 1d6a93be32
commit 4da250ec85
13 changed files with 724 additions and 3 deletions
Executable
+5
View File
@@ -0,0 +1,5 @@
#! /usr/bin/env python
from centvrion.lsp import run
if __name__ == "__main__":
run()
+6
View File
@@ -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()
+210
View File
@@ -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
+21
View File
@@ -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",
}]
+38
View File
@@ -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()),
)
+89
View File
@@ -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)
)
+113
View File
@@ -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()
+3
View File
@@ -0,0 +1,3 @@
node_modules/
out/
*.vsix
+4
View File
@@ -2,3 +2,7 @@
.vscode-test/**
.gitignore
vsc-extension-quickstart.md
src/**
tsconfig.json
node_modules/**
out/**/*.map
+139
View File
@@ -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"
}
}
}
+29 -3
View File
@@ -2,13 +2,17 @@
"name": "centvrion",
"displayName": "centvrion",
"description": "Latin-inspired esoteric programming language with Roman numeral literals",
"version": "0.0.1",
"version": "0.0.2",
"engines": {
"vscode": "^1.68.0"
},
"categories": [
"Programming Languages"
],
"main": "./out/extension.js",
"activationEvents": [
"onLanguage:cent"
],
"contributes": {
"languages": [{
"id": "cent",
@@ -24,6 +28,28 @@
"snippets": [{
"language": "cent",
"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"
}
}
}
+55
View File
@@ -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();
}
+12
View File
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2020",
"outDir": "out",
"lib": ["ES2020"],
"sourceMap": true,
"rootDir": "src",
"strict": true
},
"exclude": ["node_modules", ".vscode-test"]
}