@nudojs/lsp 0.3.0 → 0.4.1

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/src/server.ts DELETED
@@ -1,672 +0,0 @@
1
- import {
2
- createConnection,
3
- TextDocuments,
4
- ProposedFeatures,
5
- TextDocumentSyncKind,
6
- type InitializeParams,
7
- type InitializeResult,
8
- type CompletionItem as LspCompletionItem,
9
- CompletionItemKind,
10
- MarkupKind,
11
- type CodeLens,
12
- CodeLensRefreshRequest,
13
- FileChangeType,
14
- type FileEvent,
15
- type InlayHint,
16
- InlayHintKind,
17
- } from "vscode-languageserver/node";
18
- import { TextDocument } from "vscode-languageserver-textdocument";
19
- import { typeValueToString } from "@nudojs/core";
20
- import {
21
- getTypeAtPosition,
22
- getCompletionsAtPosition,
23
- getCasesForFile,
24
- } from "@nudojs/service";
25
- import { parse } from "@nudojs/parser";
26
- import { buildSymbolTable, findDefinition, findReferences, findIdentifierAtPosition } from "./symbols.ts";
27
- import { TOKEN_TYPES, TOKEN_MODIFIERS } from "./semantic-tokens.ts";
28
- import {
29
- analysisCache,
30
- evictModuleGraphCacheEntries,
31
- forgetValidatedFile,
32
- getCachedOrAnalyze,
33
- hasNudoDirectives,
34
- toLspDiagnostic,
35
- uriToFilePath,
36
- validateText,
37
- type ValidateTextDeps,
38
- } from "./validation.ts";
39
- import {
40
- normalizeFilePath,
41
- suggestCase,
42
- trace,
43
- whatIf,
44
- type AgentToolDeps,
45
- type AgentToolResult,
46
- } from "./agent-tools.ts";
47
-
48
- const NUDO_COMMANDS = [
49
- "nudo.whatIf",
50
- "nudo.suggestCase",
51
- "nudo.trace",
52
- "nudo.selectCase",
53
- "nudo.getActiveCases",
54
- ] as const;
55
-
56
- const connection = createConnection(ProposedFeatures.all);
57
- const documents = new TextDocuments(TextDocument);
58
-
59
- const activeCases = new Map<string, Map<string, number>>();
60
-
61
- function getActiveCasesForUri(uri: string): Map<string, number> {
62
- const existing = activeCases.get(uri);
63
- if (existing) return existing;
64
- const map = new Map<string, number>();
65
- activeCases.set(uri, map);
66
- return map;
67
- }
68
-
69
- connection.onInitialize((_params: InitializeParams): InitializeResult => ({
70
- capabilities: {
71
- textDocumentSync: TextDocumentSyncKind.Full,
72
- hoverProvider: true,
73
- completionProvider: {
74
- triggerCharacters: ["."],
75
- resolveProvider: false,
76
- },
77
- codeLensProvider: {
78
- resolveProvider: false,
79
- },
80
- inlayHintProvider: true,
81
- definitionProvider: true,
82
- referencesProvider: true,
83
- renameProvider: true,
84
- codeActionProvider: {
85
- codeActionKinds: ["quickfix"],
86
- },
87
- signatureHelpProvider: {
88
- triggerCharacters: ["(", ","],
89
- },
90
- semanticTokensProvider: {
91
- full: true,
92
- legend: {
93
- tokenTypes: [...TOKEN_TYPES],
94
- tokenModifiers: [...TOKEN_MODIFIERS],
95
- },
96
- },
97
- executeCommandProvider: {
98
- commands: [...NUDO_COMMANDS],
99
- },
100
- diagnosticProvider: {
101
- interFileDependencies: false,
102
- workspaceDiagnostics: false,
103
- },
104
- },
105
- }));
106
-
107
- let debounceTimers = new Map<string, ReturnType<typeof setTimeout>>();
108
-
109
- // 打开即验证:didOpen 不会触发 onDidChangeContent,若不在此主动验证,
110
- // 新打开的文件要等到首次编辑(300ms 防抖后)或客户端 pull 诊断才有结果。
111
- // propagate=true 与编辑防抖路径同语义(含对打开依赖项的一次脏传播)。
112
- documents.onDidOpen((event) => {
113
- nudoFileCache.delete(event.document.uri);
114
- validateDocument(event.document, true).catch(() => {});
115
- });
116
-
117
- documents.onDidChangeContent((change) => {
118
- const uri = change.document.uri;
119
- nudoFileCache.delete(uri);
120
- const existing = debounceTimers.get(uri);
121
- if (existing) clearTimeout(existing);
122
-
123
- debounceTimers.set(
124
- uri,
125
- setTimeout(() => {
126
- debounceTimers.delete(uri);
127
- validateDocument(change.document, true).catch(() => {});
128
- }, 300),
129
- );
130
- });
131
-
132
- documents.onDidClose((event) => {
133
- const timer = debounceTimers.get(event.document.uri);
134
- if (timer) clearTimeout(timer);
135
- debounceTimers.delete(event.document.uri);
136
- nudoFileCache.delete(event.document.uri);
137
- analysisCache.delete(uriToFilePath(event.document.uri));
138
- connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] });
139
- });
140
-
141
- /**
142
- * watched-files 删除事件监听器:接收「被删除且不在打开集」的 uri 列表。
143
- * 缓存逐出等后续逻辑通过 registerWatchedFilesListener 挂到这里。
144
- */
145
- export const watchedFilesListeners: Array<(uris: string[]) => void> = [];
146
-
147
- /** 注册 watched-files 监听器,返回注销函数。 */
148
- export function registerWatchedFilesListener(listener: (uris: string[]) => void): () => void {
149
- watchedFilesListeners.push(listener);
150
- return () => {
151
- const idx = watchedFilesListeners.indexOf(listener);
152
- if (idx >= 0) watchedFilesListeners.splice(idx, 1);
153
- };
154
- }
155
-
156
- /**
157
- * watched-files 事件核心:对 Deleted 且不在打开集的文件清理会话登记项
158
- * (knownFiles/analysisCache 走 forgetValidatedFile,activeCases/nudoFileCache 按 uri),
159
- * 清空其已发布诊断,并把被清理的 uri 列表广播给监听器。
160
- * 打开中的文件跳过——其内容由编辑流负责,外部删除会被编辑器以 didOpen/didChange 覆盖。
161
- */
162
- function handleWatchedFilesChanges(changes: readonly FileEvent[], isOpen: (uri: string) => boolean): string[] {
163
- const gone: string[] = [];
164
- for (const change of changes) {
165
- if (change.type !== FileChangeType.Deleted) continue;
166
- if (isOpen(change.uri)) continue;
167
- gone.push(change.uri);
168
- forgetValidatedFile(uriToFilePath(change.uri));
169
- activeCases.delete(change.uri);
170
- nudoFileCache.delete(change.uri);
171
- connection.sendDiagnostics({ uri: change.uri, diagnostics: [] });
172
- }
173
- if (gone.length > 0) {
174
- // 拷贝后再遍历:监听器内注销自身不应影响本轮广播
175
- for (const listener of [...watchedFilesListeners]) {
176
- try {
177
- listener(gone);
178
- } catch {
179
- // 单个监听器异常不阻断其余监听器的缓存逐出
180
- }
181
- }
182
- }
183
- return gone;
184
- }
185
-
186
- connection.onDidChangeWatchedFiles((event) =>
187
- handleWatchedFilesChanges(event.changes, (uri) => documents.get(uri) !== undefined),
188
- );
189
-
190
- // 模块图边缓存逐出:收到被清理 uri 时逐出 moduleGraphCache 对应 filePath 的条目。
191
- // 会话级常驻注册,无需持有注销函数。
192
- registerWatchedFilesListener(evictModuleGraphCacheEntries);
193
-
194
- function validationDeps(): ValidateTextDeps {
195
- return {
196
- sendDiagnostics: (params) => connection.sendDiagnostics(params),
197
- isNudoUri: (uri) => isNudoFile(uri),
198
- getActiveCases: (uri) => getActiveCasesForUri(uri),
199
- getOpenDocumentByPath: (filePath) =>
200
- documents.all().find((doc) => uriToFilePath(doc.uri) === filePath),
201
- };
202
- }
203
-
204
- function validateDocument(document: TextDocument, propagate = false): Promise<void> {
205
- return validateText(
206
- uriToFilePath(document.uri),
207
- document.uri,
208
- document.getText(),
209
- document.version,
210
- validationDeps(),
211
- propagate,
212
- );
213
- }
214
-
215
- connection.onHover((params) => {
216
- const document = documents.get(params.textDocument.uri);
217
- if (!document) return null;
218
- if (!isNudoFile(params.textDocument.uri)) return null;
219
-
220
- const filePath = uriToFilePath(params.textDocument.uri);
221
- const source = document.getText();
222
- const line = params.position.line + 1;
223
- const column = params.position.character;
224
- const cases = getActiveCasesForUri(params.textDocument.uri);
225
-
226
- try {
227
- const tv = getTypeAtPosition(filePath, source, line, column, cases);
228
- if (!tv) return null;
229
-
230
- return {
231
- contents: {
232
- kind: MarkupKind.Markdown,
233
- value: `\`\`\`nudo\n${typeValueToString(tv)}\n\`\`\``,
234
- },
235
- };
236
- } catch {
237
- return null;
238
- }
239
- });
240
-
241
- connection.onCompletion((params) => {
242
- const document = documents.get(params.textDocument.uri);
243
- if (!document) return [];
244
- if (!isNudoFile(params.textDocument.uri)) return [];
245
-
246
- const filePath = uriToFilePath(params.textDocument.uri);
247
- const source = document.getText();
248
- const line = params.position.line + 1;
249
- const column = params.position.character;
250
-
251
- try {
252
- const items = getCompletionsAtPosition(filePath, source, line, column);
253
- return items.map((item): LspCompletionItem => ({
254
- label: item.label,
255
- kind: item.kind === "method"
256
- ? CompletionItemKind.Method
257
- : item.kind === "property"
258
- ? CompletionItemKind.Property
259
- : CompletionItemKind.Variable,
260
- detail: item.detail,
261
- }));
262
- } catch {
263
- return [];
264
- }
265
- });
266
-
267
- connection.onCodeLens((params) => {
268
- const document = documents.get(params.textDocument.uri);
269
- if (!document) return [];
270
- if (!isNudoFile(params.textDocument.uri)) return [];
271
-
272
- const filePath = uriToFilePath(params.textDocument.uri);
273
- const source = document.getText();
274
- const cases = getActiveCasesForUri(params.textDocument.uri);
275
-
276
- try {
277
- const fnCases = getCasesForFile(filePath, source);
278
- const lenses: CodeLens[] = [];
279
-
280
- for (const fn of fnCases) {
281
- if (fn.cases.length === 0) continue;
282
- const activeIdx = cases.get(fn.functionName) ?? 0;
283
-
284
- for (const c of fn.cases) {
285
- const isActive = c.index === activeIdx;
286
- const title = isActive ? `● case "${c.name}"` : `○ case "${c.name}"`;
287
- lenses.push({
288
- range: {
289
- start: { line: fn.loc.start.line - 1, character: 0 },
290
- end: { line: fn.loc.start.line - 1, character: 0 },
291
- },
292
- command: {
293
- title,
294
- command: "nudo.selectCase",
295
- arguments: [params.textDocument.uri, fn.functionName, c.index, c.name],
296
- },
297
- });
298
- }
299
- }
300
-
301
- return lenses;
302
- } catch {
303
- return [];
304
- }
305
- });
306
-
307
- connection.languages.inlayHint.on((params) => {
308
- const document = documents.get(params.textDocument.uri);
309
- if (!document) return [];
310
- if (!isNudoFile(params.textDocument.uri)) return [];
311
-
312
- const filePath = uriToFilePath(params.textDocument.uri);
313
- const source = document.getText();
314
- const cases = getActiveCasesForUri(params.textDocument.uri);
315
- const lines = source.split("\n");
316
-
317
- try {
318
- const result = getCachedOrAnalyze(filePath, source, document.version, cases);
319
- const hints: InlayHint[] = [];
320
-
321
- for (const hint of result.caseHints) {
322
- const lineIdx = hint.line - 1;
323
- if (lineIdx < 0 || lineIdx >= lines.length) continue;
324
- const lineLen = lines[lineIdx].length;
325
-
326
- hints.push({
327
- position: { line: lineIdx, character: lineLen },
328
- label: ` ${hint.label}`,
329
- kind: InlayHintKind.Type,
330
- paddingLeft: true,
331
- });
332
- }
333
-
334
- return hints;
335
- } catch {
336
- return [];
337
- }
338
- });
339
-
340
- connection.onDefinition((params) => {
341
- const document = documents.get(params.textDocument.uri);
342
- if (!document) return null;
343
- if (!isNudoFile(params.textDocument.uri)) return null;
344
-
345
- const source = document.getText();
346
- const ast = parse(source);
347
- const table = buildSymbolTable(ast, params.textDocument.uri);
348
-
349
- const line = params.position.line + 1;
350
- const column = params.position.character;
351
- const identAtPos = findIdentifierAtPosition(ast, line, column);
352
- if (!identAtPos) return null;
353
-
354
- const def = findDefinition(table, identAtPos);
355
- if (!def) return null;
356
-
357
- return {
358
- uri: params.textDocument.uri,
359
- range: {
360
- start: { line: def.loc.start.line - 1, character: def.loc.start.column },
361
- end: { line: def.loc.end.line - 1, character: def.loc.end.column },
362
- },
363
- };
364
- });
365
-
366
- connection.onReferences((params) => {
367
- const document = documents.get(params.textDocument.uri);
368
- if (!document) return [];
369
- if (!isNudoFile(params.textDocument.uri)) return [];
370
-
371
- const source = document.getText();
372
- const ast = parse(source);
373
- const table = buildSymbolTable(ast, params.textDocument.uri);
374
-
375
- const line = params.position.line + 1;
376
- const column = params.position.character;
377
- const identAtPos = findIdentifierAtPosition(ast, line, column);
378
- if (!identAtPos) return [];
379
-
380
- const refs = findReferences(table, identAtPos);
381
- return refs.map((ref) => ({
382
- uri: params.textDocument.uri,
383
- range: {
384
- start: { line: ref.loc.start.line - 1, character: ref.loc.start.column },
385
- end: { line: ref.loc.end.line - 1, character: ref.loc.end.column },
386
- },
387
- }));
388
- });
389
-
390
- connection.onRenameRequest((params) => {
391
- const document = documents.get(params.textDocument.uri);
392
- if (!document) return null;
393
- if (!isNudoFile(params.textDocument.uri)) return null;
394
-
395
- const source = document.getText();
396
- const ast = parse(source);
397
- const table = buildSymbolTable(ast, params.textDocument.uri);
398
-
399
- const line = params.position.line + 1;
400
- const column = params.position.character;
401
- const identAtPos = findIdentifierAtPosition(ast, line, column);
402
- if (!identAtPos) return null;
403
-
404
- const def = findDefinition(table, identAtPos);
405
- const refs = findReferences(table, identAtPos);
406
-
407
- const edits = [];
408
-
409
- if (def) {
410
- edits.push({
411
- range: {
412
- start: { line: def.loc.start.line - 1, character: def.loc.start.column },
413
- end: { line: def.loc.end.line - 1, character: def.loc.end.column },
414
- },
415
- newText: params.newName,
416
- });
417
- }
418
-
419
- for (const ref of refs) {
420
- edits.push({
421
- range: {
422
- start: { line: ref.loc.start.line - 1, character: ref.loc.start.column },
423
- end: { line: ref.loc.end.line - 1, character: ref.loc.end.column },
424
- },
425
- newText: params.newName,
426
- });
427
- }
428
-
429
- return {
430
- changes: {
431
- [params.textDocument.uri]: edits,
432
- },
433
- };
434
- });
435
-
436
- connection.onCodeAction((params) => {
437
- const document = documents.get(params.textDocument.uri);
438
- if (!document) return [];
439
- if (!isNudoFile(params.textDocument.uri)) return [];
440
-
441
- const actions = [];
442
-
443
- for (const diag of params.context.diagnostics) {
444
- if (diag.code === "nudo-unreachable") {
445
- actions.push({
446
- title: "Remove unreachable code",
447
- kind: "quickfix",
448
- diagnostics: [diag],
449
- edit: {
450
- changes: {
451
- [params.textDocument.uri]: [{
452
- range: diag.range,
453
- newText: "",
454
- }],
455
- },
456
- },
457
- });
458
- }
459
-
460
- if (diag.code === "nudo-assertion-failed") {
461
- actions.push({
462
- title: "Update @nudo:returns to match inferred type",
463
- kind: "quickfix",
464
- diagnostics: [diag],
465
- isPreferred: false,
466
- });
467
- }
468
- }
469
-
470
- return actions;
471
- });
472
-
473
- connection.onSignatureHelp((params) => {
474
- const document = documents.get(params.textDocument.uri);
475
- if (!document) return null;
476
- if (!isNudoFile(params.textDocument.uri)) return null;
477
-
478
- const filePath = uriToFilePath(params.textDocument.uri);
479
- const source = document.getText();
480
- const line = params.position.line + 1;
481
- const column = params.position.character;
482
- const cases = getActiveCasesForUri(params.textDocument.uri);
483
-
484
- try {
485
- const ast = parse(source);
486
- const callInfo = findEnclosingCall(ast, line, column);
487
- if (!callInfo) return null;
488
-
489
- const fnType = getTypeAtPosition(filePath, source, callInfo.calleeLine, callInfo.calleeCol, cases);
490
- if (!fnType || fnType.kind !== "function") return null;
491
-
492
- const paramLabels = fnType.params.map((p) => `${p}: unknown`);
493
- const activeParam = callInfo.currentParamIndex;
494
-
495
- return {
496
- signatures: [{
497
- label: `(${paramLabels.join(", ")}) => unknown`,
498
- parameters: paramLabels.map((label) => ({ label })),
499
- activeParameter: activeParam,
500
- }],
501
- activeSignature: 0,
502
- activeParameter: activeParam,
503
- };
504
- } catch {
505
- return null;
506
- }
507
- });
508
-
509
- function findEnclosingCall(ast: any, line: number, column: number): { calleeLine: number; calleeCol: number; currentParamIndex: number } | null {
510
- let result: any = null;
511
-
512
- function visit(node: any): void {
513
- if (!node || result) return;
514
-
515
- if (node.type === "CallExpression") {
516
- const loc = node.loc;
517
- if (loc && loc.start.line <= line && loc.end.line >= line) {
518
- const calleeLoc = node.callee.loc;
519
- if (calleeLoc) {
520
- let paramIndex = 0;
521
- for (let i = 0; i < node.arguments.length; i++) {
522
- const argLoc = node.arguments[i].loc;
523
- if (argLoc) {
524
- if (argLoc.start.line < line || (argLoc.start.line === line && argLoc.start.column <= column)) {
525
- paramIndex = i + 1;
526
- }
527
- }
528
- }
529
- result = {
530
- calleeLine: calleeLoc.start.line,
531
- calleeCol: calleeLoc.start.column,
532
- currentParamIndex: Math.min(paramIndex, node.arguments.length),
533
- };
534
- }
535
- }
536
- }
537
-
538
- for (const key of Object.keys(node)) {
539
- if (key === "type" || key === "loc" || key === "start" || key === "end") continue;
540
- const child = node[key];
541
- if (Array.isArray(child)) {
542
- for (const item of child) {
543
- if (item && typeof item === "object" && item.type) visit(item);
544
- }
545
- } else if (child && typeof child === "object" && child.type) {
546
- visit(child);
547
- }
548
- }
549
- }
550
-
551
- visit(ast);
552
- return result;
553
- }
554
-
555
- connection.languages.semanticTokens.on((params) => {
556
- const document = documents.get(params.textDocument.uri);
557
- if (!document) return { data: [] };
558
- if (!isNudoFile(params.textDocument.uri)) return { data: [] };
559
-
560
- // For now, return empty tokens - can be enhanced later
561
- return { data: [] };
562
- });
563
-
564
- /** Resolve a `uri`- or `file`-identified target to the uri key used by activeCases/documents. */
565
- function uriForFileOrUri(params: { uri?: string; file?: string }): string {
566
- if (params.uri) return params.uri;
567
- const filePath = normalizeFilePath(params.file ?? "");
568
- const doc = documents.all().find((d) => uriToFilePath(d.uri) === filePath);
569
- return doc ? doc.uri : `file://${filePath}`;
570
- }
571
-
572
- async function handleSelectCase(params: { uri?: string; file?: string; functionName: string; caseIndex: number }) {
573
- const uri = uriForFileOrUri(params);
574
- const cases = getActiveCasesForUri(uri);
575
- cases.set(params.functionName, params.caseIndex);
576
-
577
- const document = documents.get(uri);
578
- if (document) {
579
- await validateDocument(document);
580
- }
581
-
582
- connection.sendRequest(CodeLensRefreshRequest.type).catch(() => {});
583
-
584
- return { success: true };
585
- }
586
-
587
- function handleGetActiveCases(params: { uri?: string; file?: string }) {
588
- const cases = getActiveCasesForUri(uriForFileOrUri(params));
589
- const result: Record<string, number> = {};
590
- for (const [fn, idx] of cases) {
591
- result[fn] = idx;
592
- }
593
- return result;
594
- }
595
-
596
- const agentToolDeps: AgentToolDeps = {
597
- getOpenText: (filePath) => {
598
- const doc = documents.all().find((d) => uriToFilePath(d.uri) === filePath);
599
- return doc ? { text: doc.getText() } : undefined;
600
- },
601
- };
602
-
603
- /**
604
- * One dispatch table shared by the executeCommand commands (`nudo.*`) and the
605
- * custom request aliases (`nudo/…`) — both channels run the same handlers.
606
- */
607
- function dispatchNudoCommand(command: string, arg: Record<string, unknown>) {
608
- switch (command) {
609
- case "nudo.whatIf":
610
- return whatIf(arg as Parameters<typeof whatIf>[0], agentToolDeps);
611
- case "nudo.suggestCase":
612
- return suggestCase(arg as Parameters<typeof suggestCase>[0], agentToolDeps);
613
- case "nudo.trace":
614
- return trace(arg as Parameters<typeof trace>[0], agentToolDeps);
615
- case "nudo.selectCase":
616
- return handleSelectCase(arg as Parameters<typeof handleSelectCase>[0]);
617
- case "nudo.getActiveCases":
618
- return handleGetActiveCases(arg as Parameters<typeof handleGetActiveCases>[0]);
619
- default:
620
- return null;
621
- }
622
- }
623
-
624
- connection.onExecuteCommand((params) =>
625
- dispatchNudoCommand(params.command, (params.arguments?.[0] as Record<string, unknown>) ?? {}),
626
- );
627
-
628
- connection.onRequest("nudo/selectCase", handleSelectCase);
629
-
630
- connection.onRequest("nudo/getActiveCases", handleGetActiveCases);
631
-
632
- /** Request aliases share the command handlers; the pinned return type keeps onRequest overload inference happy. */
633
- function dispatchAgentRequest(command: string, params: Record<string, unknown>): AgentToolResult {
634
- return dispatchNudoCommand(command, params) as AgentToolResult;
635
- }
636
-
637
- connection.onRequest("nudo/whatIf", (params: Record<string, unknown>) => dispatchAgentRequest("nudo.whatIf", params));
638
- connection.onRequest("nudo/suggestCase", (params: Record<string, unknown>) => dispatchAgentRequest("nudo.suggestCase", params));
639
- connection.onRequest("nudo/trace", (params: Record<string, unknown>) => dispatchAgentRequest("nudo.trace", params));
640
-
641
- connection.languages.diagnostics.on((params) => {
642
- const document = documents.get(params.textDocument.uri);
643
- if (!document) return { kind: "full", items: [] };
644
- if (!isNudoFile(params.textDocument.uri)) return { kind: "full", items: [] };
645
-
646
- try {
647
- const filePath = uriToFilePath(document.uri);
648
- const result = getCachedOrAnalyze(filePath, document.getText(), document.version, getActiveCasesForUri(document.uri));
649
- return {
650
- kind: "full",
651
- items: result.diagnostics.map((d) => toLspDiagnostic(d, document.uri)),
652
- };
653
- } catch {
654
- return { kind: "full", items: [] };
655
- }
656
- });
657
-
658
- const nudoFileCache = new Map<string, boolean>();
659
-
660
- function isNudoFile(uri: string): boolean {
661
- if (!uri.endsWith(".js") && !uri.endsWith(".ts") && !uri.endsWith(".mjs")) return false;
662
- const cached = nudoFileCache.get(uri);
663
- if (cached !== undefined) return cached;
664
- const doc = documents.get(uri);
665
- if (!doc) return false;
666
- const result = hasNudoDirectives(doc.getText());
667
- nudoFileCache.set(uri, result);
668
- return result;
669
- }
670
-
671
- documents.listen(connection);
672
- connection.listen();