@githolon/dsl 0.50.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dart/example/nomos_secure_key_store.dart +82 -0
- package/dart/lib/src/webview_bridge.dart +282 -0
- package/lsp_server.mjs +43 -0
- package/package.json +5 -2
- package/src/check_package_main.ts +159 -8
- package/src/codegen_dart_client.ts +22 -1
- package/src/codegen_ts.ts +181 -0
- package/src/evolve_preview.ts +466 -0
- package/src/law_check.ts +405 -3
- package/src/law_source_map.ts +146 -0
- package/src/lsp_main.ts +14 -0
- package/src/lsp_server.ts +404 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded QuickJS engine}.
|
|
4
|
+
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `githolon lsp` — THE AUTHOR-TIME GATE AS A LANGUAGE SERVER (architecture/author_time_gate.md,
|
|
8
|
+
* slice 3). A thin `vscode-languageserver` (stdio) wrapper over the SAME static-twin machinery
|
|
9
|
+
* `githolon check` runs: on open/save/change of a `.ts` under a `nomos.package.mjs`'s domain graph,
|
|
10
|
+
* it composes the package IN-PROCESS (cache-busted TS re-import under tsx — no per-keystroke process
|
|
11
|
+
* spawn), runs {@link checkDomainModules}, maps findings to source ranges ({@link LawSourceMap}), and
|
|
12
|
+
* publishes diagnostics (refuse→Error, warn→Warning, note→Information).
|
|
13
|
+
*
|
|
14
|
+
* IN-PROCESS, not subprocess: spawning `check_package.mjs` (tsx+node cold start ≈ 150–300 ms) per
|
|
15
|
+
* keystroke is too slow for red squiggles. The check machinery is pure TS we import directly; the
|
|
16
|
+
* only cost is re-evaluating changed modules, which the `?t=` cache-bust query forces (proven under
|
|
17
|
+
* tsx). Composition is memoized per config keyed by the module files' mtimes so an UNCHANGED package
|
|
18
|
+
* re-checks without re-importing (the cheap warm win).
|
|
19
|
+
*
|
|
20
|
+
* Multi-root: each document resolves to its NEAREST `nomos.package.mjs` (walk up), so a repo with
|
|
21
|
+
* several packages diagnoses each against its own config.
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
createConnection,
|
|
29
|
+
Diagnostic,
|
|
30
|
+
DiagnosticSeverity,
|
|
31
|
+
DidChangeConfigurationNotification,
|
|
32
|
+
StreamMessageReader,
|
|
33
|
+
StreamMessageWriter,
|
|
34
|
+
TextDocuments,
|
|
35
|
+
TextDocumentSyncKind,
|
|
36
|
+
type InitializeParams,
|
|
37
|
+
} from "vscode-languageserver/node.js";
|
|
38
|
+
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
39
|
+
|
|
40
|
+
import { composeDomainModule, type Mod } from "./build_package.js";
|
|
41
|
+
import type { DomainModule } from "./codegen_dart.js";
|
|
42
|
+
import { checkDomainModules, type Finding } from "./law_check.js";
|
|
43
|
+
import { LawSourceMap } from "./law_source_map.js";
|
|
44
|
+
import {
|
|
45
|
+
currentEvolveLaw,
|
|
46
|
+
evolveDiff,
|
|
47
|
+
evolveLawFromDeployJson,
|
|
48
|
+
evolveLawFromLockfile,
|
|
49
|
+
type EvolveLaw,
|
|
50
|
+
type StableIdLockfile,
|
|
51
|
+
} from "./evolve_preview.js";
|
|
52
|
+
|
|
53
|
+
interface DomainConfig {
|
|
54
|
+
key: string;
|
|
55
|
+
name?: string;
|
|
56
|
+
modules: string[];
|
|
57
|
+
}
|
|
58
|
+
interface PackageConfig {
|
|
59
|
+
name: string;
|
|
60
|
+
domains: DomainConfig[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A finding enriched with the resolved source file (absolute) it belongs to. */
|
|
64
|
+
interface LocatedFinding {
|
|
65
|
+
readonly finding: Finding;
|
|
66
|
+
readonly file: string;
|
|
67
|
+
readonly line: number; // 1-based
|
|
68
|
+
readonly column: number; // 1-based
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The result of one package check pass. */
|
|
72
|
+
export interface CheckPassResult {
|
|
73
|
+
/** Findings across the package, each located to a source file. */
|
|
74
|
+
readonly located: LocatedFinding[];
|
|
75
|
+
/** A load/compose error (bad config, module that failed to import) — reported on the config file. */
|
|
76
|
+
readonly error?: string;
|
|
77
|
+
/** The absolute module file paths in the package (so the caller can clear stale diagnostics). */
|
|
78
|
+
readonly files: string[];
|
|
79
|
+
/** The config file path (the anchor for a whole-package load error). */
|
|
80
|
+
readonly configPath: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Walk up from `startDir` to find the nearest `nomos.package.mjs`; `undefined` at the filesystem root. */
|
|
84
|
+
export function findNearestConfig(startDir: string): string | undefined {
|
|
85
|
+
let dir = startDir;
|
|
86
|
+
for (;;) {
|
|
87
|
+
const candidate = path.join(dir, "nomos.package.mjs");
|
|
88
|
+
if (existsSync(candidate)) return candidate;
|
|
89
|
+
const parent = path.dirname(dir);
|
|
90
|
+
if (parent === dir) return undefined;
|
|
91
|
+
dir = parent;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A composed package + its provenance, memoized per config keyed by the module files' mtimes. */
|
|
96
|
+
interface ComposedCache {
|
|
97
|
+
readonly mtimeKey: string;
|
|
98
|
+
readonly modules: DomainModule[];
|
|
99
|
+
readonly domainFiles: Map<string, string[]>;
|
|
100
|
+
}
|
|
101
|
+
const composedCache = new Map<string, ComposedCache>();
|
|
102
|
+
|
|
103
|
+
function mtimeKey(files: readonly string[]): string {
|
|
104
|
+
return files
|
|
105
|
+
.map((f) => {
|
|
106
|
+
try {
|
|
107
|
+
return `${f}:${statSync(f).mtimeMs}`;
|
|
108
|
+
} catch {
|
|
109
|
+
return `${f}:missing`;
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
.join("|");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Compose a package's domain modules IN-PROCESS from its config. Cache-busts each `import()` so an
|
|
117
|
+
* edited module re-evaluates, and memoizes the whole composition by the module files' mtimes so an
|
|
118
|
+
* unchanged package returns instantly. Throws on a bad config / a module that fails to import.
|
|
119
|
+
*/
|
|
120
|
+
async function composePackage(
|
|
121
|
+
configPath: string,
|
|
122
|
+
): Promise<{ modules: DomainModule[]; domainFiles: Map<string, string[]> }> {
|
|
123
|
+
const cfgDir = path.dirname(configPath);
|
|
124
|
+
const cfgModule = (await import(`${pathToFileURL(configPath).href}?t=${Date.now()}`)) as {
|
|
125
|
+
default?: PackageConfig;
|
|
126
|
+
};
|
|
127
|
+
const cfg = cfgModule.default;
|
|
128
|
+
if (!cfg || typeof cfg.name !== "string" || !Array.isArray(cfg.domains) || cfg.domains.length === 0) {
|
|
129
|
+
throw new Error("config must default-export { name, domains: [{ key, modules }, ...] }");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Resolve every module file first so we can compute the mtime cache key before importing.
|
|
133
|
+
const perDomainFiles: { key: string; name: string | undefined; files: string[] }[] = [];
|
|
134
|
+
const allFiles: string[] = [];
|
|
135
|
+
for (const d of cfg.domains) {
|
|
136
|
+
if (typeof d.key !== "string" || !Array.isArray(d.modules) || d.modules.length === 0) {
|
|
137
|
+
throw new Error(`each domain needs { key, modules: [path, ...] }; got ${JSON.stringify(d)}`);
|
|
138
|
+
}
|
|
139
|
+
const files: string[] = [];
|
|
140
|
+
for (const rel of d.modules) {
|
|
141
|
+
const abs = path.resolve(cfgDir, rel);
|
|
142
|
+
if (!existsSync(abs)) throw new Error(`domain '${d.key}': module not found: ${abs}`);
|
|
143
|
+
files.push(abs);
|
|
144
|
+
allFiles.push(abs);
|
|
145
|
+
}
|
|
146
|
+
perDomainFiles.push({ key: d.key, name: d.name, files });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const key = `${statSync(configPath).mtimeMs}::${mtimeKey(allFiles)}`;
|
|
150
|
+
const cached = composedCache.get(configPath);
|
|
151
|
+
if (cached !== undefined && cached.mtimeKey === key) {
|
|
152
|
+
return { modules: cached.modules, domainFiles: cached.domainFiles };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const composed: DomainModule[] = [];
|
|
156
|
+
const domainFiles = new Map<string, string[]>();
|
|
157
|
+
for (const d of perDomainFiles) {
|
|
158
|
+
const mods: Mod[] = [];
|
|
159
|
+
for (const abs of d.files) {
|
|
160
|
+
mods.push((await import(`${pathToFileURL(abs).href}?t=${Date.now()}`)) as Mod);
|
|
161
|
+
}
|
|
162
|
+
domainFiles.set(d.key, d.files);
|
|
163
|
+
composed.push(composeDomainModule({ name: d.name ?? d.key, domain: d.key, modules: mods }));
|
|
164
|
+
}
|
|
165
|
+
composedCache.set(configPath, { mtimeKey: key, modules: composed, domainFiles });
|
|
166
|
+
return { modules: composed, domainFiles };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Load the `--against` prior evolve law from a deploy.json / build dir / lockfile (LSP variant — throws). */
|
|
170
|
+
function loadPriorEvolveLaw(arg: string, cfgDir: string): EvolveLaw | undefined {
|
|
171
|
+
const abs = path.isAbsolute(arg) ? arg : path.resolve(cfgDir, arg);
|
|
172
|
+
if (!existsSync(abs)) return undefined;
|
|
173
|
+
let file = abs;
|
|
174
|
+
if (statSync(abs).isDirectory()) {
|
|
175
|
+
const buildDir = path.join(abs, "build");
|
|
176
|
+
const scan = (d: string): string | undefined => {
|
|
177
|
+
if (!existsSync(d)) return undefined;
|
|
178
|
+
const hit = readdirSync(d).find((f) => f.endsWith(".deploy.json"));
|
|
179
|
+
return hit !== undefined ? path.join(d, hit) : undefined;
|
|
180
|
+
};
|
|
181
|
+
const deploy = scan(abs) ?? scan(buildDir);
|
|
182
|
+
const lock = path.join(abs, "nomos.stable-ids.json");
|
|
183
|
+
if (deploy !== undefined) file = deploy;
|
|
184
|
+
else if (existsSync(lock)) file = lock;
|
|
185
|
+
else return undefined;
|
|
186
|
+
}
|
|
187
|
+
let parsed: unknown;
|
|
188
|
+
try {
|
|
189
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
190
|
+
} catch {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
const p = parsed as Record<string, unknown>;
|
|
194
|
+
if (p.domains !== undefined && p.interface === undefined && p.packageUsda === undefined) {
|
|
195
|
+
return evolveLawFromLockfile(parsed as StableIdLockfile);
|
|
196
|
+
}
|
|
197
|
+
return evolveLawFromDeployJson(parsed as never);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Read the committed `nomos.stable-ids.json` lockfile beside the config (the CURRENT identity). */
|
|
201
|
+
function loadCwdLockfile(cfgDir: string): StableIdLockfile | undefined {
|
|
202
|
+
const lockPath = path.join(cfgDir, "nomos.stable-ids.json");
|
|
203
|
+
if (!existsSync(lockPath)) return undefined;
|
|
204
|
+
try {
|
|
205
|
+
return JSON.parse(readFileSync(lockPath, "utf8")) as StableIdLockfile;
|
|
206
|
+
} catch {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Run the full author-time check for one package config IN-PROCESS and locate every finding. `against`
|
|
213
|
+
* (optional, from `nomos.lsp.against`) folds the evolve-preview findings in. Never throws — a load
|
|
214
|
+
* error is returned as {@link CheckPassResult.error} so the LSP can surface it on the config file.
|
|
215
|
+
*/
|
|
216
|
+
export async function runCheckPass(configPath: string, against?: string): Promise<CheckPassResult> {
|
|
217
|
+
const cfgDir = path.dirname(configPath);
|
|
218
|
+
let modules: DomainModule[];
|
|
219
|
+
let domainFiles: Map<string, string[]>;
|
|
220
|
+
try {
|
|
221
|
+
({ modules, domainFiles } = await composePackage(configPath));
|
|
222
|
+
} catch (e) {
|
|
223
|
+
return { located: [], error: (e as Error).message, files: [], configPath };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const findings: Finding[] = [...checkDomainModules(modules)];
|
|
227
|
+
|
|
228
|
+
if (against !== undefined && against.trim().length > 0) {
|
|
229
|
+
try {
|
|
230
|
+
const prior = loadPriorEvolveLaw(against, cfgDir);
|
|
231
|
+
if (prior !== undefined) {
|
|
232
|
+
const current = currentEvolveLaw(modules, loadCwdLockfile(cfgDir));
|
|
233
|
+
findings.push(...evolveDiff(prior, current).findings);
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
/* evolve preview is best-effort — never block the base check */
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const srcMap = new LawSourceMap(domainFiles);
|
|
241
|
+
const located: LocatedFinding[] = findings.map((f) => {
|
|
242
|
+
const loc = srcMap.locate(f.domain, f.directiveId);
|
|
243
|
+
return {
|
|
244
|
+
finding: f,
|
|
245
|
+
file: loc?.file ?? configPath,
|
|
246
|
+
line: loc?.line ?? 1,
|
|
247
|
+
column: loc?.column ?? 1,
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const files: string[] = [];
|
|
252
|
+
for (const fs of domainFiles.values()) files.push(...fs);
|
|
253
|
+
return { located, files, configPath };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Map a finding level to an LSP diagnostic severity. */
|
|
257
|
+
function severityOf(level: Finding["level"]): DiagnosticSeverity {
|
|
258
|
+
return level === "refuse"
|
|
259
|
+
? DiagnosticSeverity.Error
|
|
260
|
+
: level === "warn"
|
|
261
|
+
? DiagnosticSeverity.Warning
|
|
262
|
+
: DiagnosticSeverity.Information;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Build the diagnostic for a located finding — a range covering the declaration line's token. */
|
|
266
|
+
function diagnosticFor(lf: LocatedFinding): Diagnostic {
|
|
267
|
+
const line = Math.max(0, lf.line - 1);
|
|
268
|
+
const col = Math.max(0, lf.column - 1);
|
|
269
|
+
return {
|
|
270
|
+
severity: severityOf(lf.finding.level),
|
|
271
|
+
range: { start: { line, character: col }, end: { line, character: col + 1 } },
|
|
272
|
+
source: "githolon",
|
|
273
|
+
message: `${lf.finding.what} — fix: ${lf.finding.fix}`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Group located findings by absolute file path into LSP diagnostics. */
|
|
278
|
+
export function groupDiagnostics(located: readonly LocatedFinding[]): Map<string, Diagnostic[]> {
|
|
279
|
+
const byFile = new Map<string, Diagnostic[]>();
|
|
280
|
+
for (const lf of located) {
|
|
281
|
+
const list = byFile.get(lf.file) ?? [];
|
|
282
|
+
list.push(diagnosticFor(lf));
|
|
283
|
+
byFile.set(lf.file, list);
|
|
284
|
+
}
|
|
285
|
+
return byFile;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Start the language server on stdio. Exported so the launcher (`lsp_main.ts`) and tests can drive it;
|
|
290
|
+
* the connection is created from `process.stdin`/`process.stdout` by default.
|
|
291
|
+
*/
|
|
292
|
+
export function startServer(): void {
|
|
293
|
+
// Bind explicitly to stdio (an editor may or may not pass `--stdio`; the streams are unambiguous).
|
|
294
|
+
const connection = createConnection(
|
|
295
|
+
new StreamMessageReader(process.stdin),
|
|
296
|
+
new StreamMessageWriter(process.stdout),
|
|
297
|
+
);
|
|
298
|
+
const documents = new TextDocuments(TextDocument);
|
|
299
|
+
|
|
300
|
+
let against: string | undefined;
|
|
301
|
+
// Per config: the set of files we last published diagnostics for (to clear stale ones on re-check).
|
|
302
|
+
const publishedByConfig = new Map<string, Set<string>>();
|
|
303
|
+
const debounceTimers = new Map<string, NodeJS.Timeout>();
|
|
304
|
+
|
|
305
|
+
connection.onInitialize((params: InitializeParams) => {
|
|
306
|
+
const init = params.initializationOptions as { against?: string } | undefined;
|
|
307
|
+
if (init?.against !== undefined) against = init.against;
|
|
308
|
+
return {
|
|
309
|
+
capabilities: {
|
|
310
|
+
textDocumentSync: TextDocumentSyncKind.Incremental,
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
connection.onInitialized(() => {
|
|
316
|
+
void connection.client
|
|
317
|
+
.register(DidChangeConfigurationNotification.type, undefined)
|
|
318
|
+
.catch(() => {/* client without config support — the initializationOptions value stands */});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
connection.onDidChangeConfiguration((change) => {
|
|
322
|
+
const settings = change.settings as { nomos?: { lsp?: { against?: string } } } | undefined;
|
|
323
|
+
const next = settings?.nomos?.lsp?.against;
|
|
324
|
+
if (next !== undefined) {
|
|
325
|
+
against = next;
|
|
326
|
+
// Re-check every open TS document under a config.
|
|
327
|
+
for (const doc of documents.all()) scheduleCheck(doc.uri, 0);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
/** Run the check for the config that owns `uri` and publish/clear diagnostics across the package. */
|
|
332
|
+
async function checkForUri(uri: string): Promise<void> {
|
|
333
|
+
let fsPath: string;
|
|
334
|
+
try {
|
|
335
|
+
fsPath = fileURLToPath(uri);
|
|
336
|
+
} catch {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (!fsPath.endsWith(".ts")) return;
|
|
340
|
+
const configPath = findNearestConfig(path.dirname(fsPath));
|
|
341
|
+
if (configPath === undefined) return;
|
|
342
|
+
|
|
343
|
+
const result = await runCheckPass(configPath, against);
|
|
344
|
+
|
|
345
|
+
if (result.error !== undefined) {
|
|
346
|
+
// A whole-package load error → one diagnostic on the config file.
|
|
347
|
+
connection.sendDiagnostics({
|
|
348
|
+
uri: pathToFileURL(configPath).href,
|
|
349
|
+
diagnostics: [
|
|
350
|
+
{
|
|
351
|
+
severity: DiagnosticSeverity.Error,
|
|
352
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
|
|
353
|
+
source: "githolon",
|
|
354
|
+
message: `githolon check could not load the package: ${result.error}`,
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
});
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// Clear any prior config-file load error.
|
|
361
|
+
connection.sendDiagnostics({ uri: pathToFileURL(configPath).href, diagnostics: [] });
|
|
362
|
+
|
|
363
|
+
const byFile = groupDiagnostics(result.located);
|
|
364
|
+
const nowPublished = new Set<string>();
|
|
365
|
+
for (const [file, diags] of byFile) {
|
|
366
|
+
connection.sendDiagnostics({ uri: pathToFileURL(file).href, diagnostics: diags });
|
|
367
|
+
nowPublished.add(file);
|
|
368
|
+
}
|
|
369
|
+
// Clear diagnostics for files that had findings last time but are now clean.
|
|
370
|
+
const prev = publishedByConfig.get(configPath) ?? new Set<string>();
|
|
371
|
+
for (const file of prev) {
|
|
372
|
+
if (!nowPublished.has(file)) {
|
|
373
|
+
connection.sendDiagnostics({ uri: pathToFileURL(file).href, diagnostics: [] });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// Also make sure every module file with NO findings is cleared (fresh-open case).
|
|
377
|
+
for (const file of result.files) {
|
|
378
|
+
if (!nowPublished.has(file)) {
|
|
379
|
+
connection.sendDiagnostics({ uri: pathToFileURL(file).href, diagnostics: [] });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
publishedByConfig.set(configPath, nowPublished);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function scheduleCheck(uri: string, delayMs: number): void {
|
|
386
|
+
const existing = debounceTimers.get(uri);
|
|
387
|
+
if (existing !== undefined) clearTimeout(existing);
|
|
388
|
+
debounceTimers.set(
|
|
389
|
+
uri,
|
|
390
|
+
setTimeout(() => {
|
|
391
|
+
debounceTimers.delete(uri);
|
|
392
|
+
void checkForUri(uri).catch((e) => connection.console.error(`githolon lsp: ${String(e)}`));
|
|
393
|
+
}, delayMs),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Open + save are immediate; change is debounced (~500 ms) so we don't re-check every keystroke.
|
|
398
|
+
documents.onDidOpen((e) => scheduleCheck(e.document.uri, 0));
|
|
399
|
+
documents.onDidSave((e) => scheduleCheck(e.document.uri, 0));
|
|
400
|
+
documents.onDidChangeContent((e) => scheduleCheck(e.document.uri, 500));
|
|
401
|
+
|
|
402
|
+
documents.listen(connection);
|
|
403
|
+
connection.listen();
|
|
404
|
+
}
|