@nudojs/lsp 0.1.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/CHANGELOG.md +13 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +254 -0
- package/package.json +15 -0
- package/src/server.ts +302 -0
- package/tsconfig.json +12 -0
package/CHANGELOG.md
ADDED
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import {
|
|
3
|
+
createConnection,
|
|
4
|
+
TextDocuments,
|
|
5
|
+
ProposedFeatures,
|
|
6
|
+
TextDocumentSyncKind,
|
|
7
|
+
DiagnosticSeverity,
|
|
8
|
+
DiagnosticTag,
|
|
9
|
+
CompletionItemKind,
|
|
10
|
+
MarkupKind,
|
|
11
|
+
CodeLensRefreshRequest,
|
|
12
|
+
InlayHintKind
|
|
13
|
+
} from "vscode-languageserver/node";
|
|
14
|
+
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
15
|
+
import { typeValueToString } from "@nudojs/core";
|
|
16
|
+
import {
|
|
17
|
+
analyzeFile,
|
|
18
|
+
getTypeAtPosition,
|
|
19
|
+
getCompletionsAtPosition,
|
|
20
|
+
getCasesForFile
|
|
21
|
+
} from "@nudojs/service";
|
|
22
|
+
var connection = createConnection(ProposedFeatures.all);
|
|
23
|
+
var documents = new TextDocuments(TextDocument);
|
|
24
|
+
var activeCases = /* @__PURE__ */ new Map();
|
|
25
|
+
function getActiveCasesForUri(uri) {
|
|
26
|
+
const existing = activeCases.get(uri);
|
|
27
|
+
if (existing) return existing;
|
|
28
|
+
const map = /* @__PURE__ */ new Map();
|
|
29
|
+
activeCases.set(uri, map);
|
|
30
|
+
return map;
|
|
31
|
+
}
|
|
32
|
+
var severityMap = {
|
|
33
|
+
error: DiagnosticSeverity.Error,
|
|
34
|
+
warning: DiagnosticSeverity.Warning,
|
|
35
|
+
info: DiagnosticSeverity.Information
|
|
36
|
+
};
|
|
37
|
+
connection.onInitialize((_params) => ({
|
|
38
|
+
capabilities: {
|
|
39
|
+
textDocumentSync: TextDocumentSyncKind.Full,
|
|
40
|
+
hoverProvider: true,
|
|
41
|
+
completionProvider: {
|
|
42
|
+
triggerCharacters: ["."],
|
|
43
|
+
resolveProvider: false
|
|
44
|
+
},
|
|
45
|
+
codeLensProvider: {
|
|
46
|
+
resolveProvider: false
|
|
47
|
+
},
|
|
48
|
+
inlayHintProvider: true
|
|
49
|
+
}
|
|
50
|
+
}));
|
|
51
|
+
var debounceTimers = /* @__PURE__ */ new Map();
|
|
52
|
+
documents.onDidChangeContent((change) => {
|
|
53
|
+
const uri = change.document.uri;
|
|
54
|
+
nudoFileCache.delete(uri);
|
|
55
|
+
const existing = debounceTimers.get(uri);
|
|
56
|
+
if (existing) clearTimeout(existing);
|
|
57
|
+
debounceTimers.set(
|
|
58
|
+
uri,
|
|
59
|
+
setTimeout(() => {
|
|
60
|
+
debounceTimers.delete(uri);
|
|
61
|
+
validateDocument(change.document);
|
|
62
|
+
}, 300)
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
documents.onDidClose((event) => {
|
|
66
|
+
const timer = debounceTimers.get(event.document.uri);
|
|
67
|
+
if (timer) clearTimeout(timer);
|
|
68
|
+
debounceTimers.delete(event.document.uri);
|
|
69
|
+
nudoFileCache.delete(event.document.uri);
|
|
70
|
+
connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] });
|
|
71
|
+
});
|
|
72
|
+
function validateDocument(document) {
|
|
73
|
+
const uri = document.uri;
|
|
74
|
+
if (!isNudoFile(uri)) {
|
|
75
|
+
connection.sendDiagnostics({ uri, diagnostics: [] });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const filePath = uriToFilePath(uri);
|
|
79
|
+
const source = document.getText();
|
|
80
|
+
const cases = getActiveCasesForUri(uri);
|
|
81
|
+
try {
|
|
82
|
+
const result = analyzeFile(filePath, source, cases);
|
|
83
|
+
const diagnostics = result.diagnostics.map((d) => {
|
|
84
|
+
const diag = {
|
|
85
|
+
severity: severityMap[d.severity],
|
|
86
|
+
range: {
|
|
87
|
+
start: { line: d.range.start.line - 1, character: d.range.start.column },
|
|
88
|
+
end: { line: d.range.end.line - 1, character: d.range.end.column }
|
|
89
|
+
},
|
|
90
|
+
message: d.message,
|
|
91
|
+
source: "nudo"
|
|
92
|
+
};
|
|
93
|
+
if (d.tags?.includes("unnecessary")) {
|
|
94
|
+
diag.tags = [DiagnosticTag.Unnecessary];
|
|
95
|
+
}
|
|
96
|
+
return diag;
|
|
97
|
+
});
|
|
98
|
+
connection.sendDiagnostics({ uri, diagnostics });
|
|
99
|
+
} catch (err) {
|
|
100
|
+
connection.sendDiagnostics({
|
|
101
|
+
uri,
|
|
102
|
+
diagnostics: [
|
|
103
|
+
{
|
|
104
|
+
severity: DiagnosticSeverity.Error,
|
|
105
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
|
|
106
|
+
message: `Analysis error: ${err.message}`,
|
|
107
|
+
source: "nudo"
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
connection.onHover((params) => {
|
|
114
|
+
const document = documents.get(params.textDocument.uri);
|
|
115
|
+
if (!document) return null;
|
|
116
|
+
if (!isNudoFile(params.textDocument.uri)) return null;
|
|
117
|
+
const filePath = uriToFilePath(params.textDocument.uri);
|
|
118
|
+
const source = document.getText();
|
|
119
|
+
const line = params.position.line + 1;
|
|
120
|
+
const column = params.position.character;
|
|
121
|
+
const cases = getActiveCasesForUri(params.textDocument.uri);
|
|
122
|
+
try {
|
|
123
|
+
const tv = getTypeAtPosition(filePath, source, line, column, cases);
|
|
124
|
+
if (!tv) return null;
|
|
125
|
+
return {
|
|
126
|
+
contents: {
|
|
127
|
+
kind: MarkupKind.Markdown,
|
|
128
|
+
value: `\`\`\`nudo
|
|
129
|
+
${typeValueToString(tv)}
|
|
130
|
+
\`\`\``
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
connection.onCompletion((params) => {
|
|
138
|
+
const document = documents.get(params.textDocument.uri);
|
|
139
|
+
if (!document) return [];
|
|
140
|
+
if (!isNudoFile(params.textDocument.uri)) return [];
|
|
141
|
+
const filePath = uriToFilePath(params.textDocument.uri);
|
|
142
|
+
const source = document.getText();
|
|
143
|
+
const line = params.position.line + 1;
|
|
144
|
+
const column = params.position.character;
|
|
145
|
+
try {
|
|
146
|
+
const items = getCompletionsAtPosition(filePath, source, line, column);
|
|
147
|
+
return items.map((item) => ({
|
|
148
|
+
label: item.label,
|
|
149
|
+
kind: item.kind === "method" ? CompletionItemKind.Method : item.kind === "property" ? CompletionItemKind.Property : CompletionItemKind.Variable,
|
|
150
|
+
detail: item.detail
|
|
151
|
+
}));
|
|
152
|
+
} catch {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
connection.onCodeLens((params) => {
|
|
157
|
+
const document = documents.get(params.textDocument.uri);
|
|
158
|
+
if (!document) return [];
|
|
159
|
+
if (!isNudoFile(params.textDocument.uri)) return [];
|
|
160
|
+
const filePath = uriToFilePath(params.textDocument.uri);
|
|
161
|
+
const source = document.getText();
|
|
162
|
+
const cases = getActiveCasesForUri(params.textDocument.uri);
|
|
163
|
+
try {
|
|
164
|
+
const fnCases = getCasesForFile(filePath, source);
|
|
165
|
+
const lenses = [];
|
|
166
|
+
for (const fn of fnCases) {
|
|
167
|
+
if (fn.cases.length === 0) continue;
|
|
168
|
+
const activeIdx = cases.get(fn.functionName) ?? 0;
|
|
169
|
+
for (const c of fn.cases) {
|
|
170
|
+
const isActive = c.index === activeIdx;
|
|
171
|
+
const title = isActive ? `\u25CF case "${c.name}"` : `\u25CB case "${c.name}"`;
|
|
172
|
+
lenses.push({
|
|
173
|
+
range: {
|
|
174
|
+
start: { line: fn.loc.start.line - 1, character: 0 },
|
|
175
|
+
end: { line: fn.loc.start.line - 1, character: 0 }
|
|
176
|
+
},
|
|
177
|
+
command: {
|
|
178
|
+
title,
|
|
179
|
+
command: "nudo.selectCase",
|
|
180
|
+
arguments: [params.textDocument.uri, fn.functionName, c.index, c.name]
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return lenses;
|
|
186
|
+
} catch {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
connection.languages.inlayHint.on((params) => {
|
|
191
|
+
const document = documents.get(params.textDocument.uri);
|
|
192
|
+
if (!document) return [];
|
|
193
|
+
if (!isNudoFile(params.textDocument.uri)) return [];
|
|
194
|
+
const filePath = uriToFilePath(params.textDocument.uri);
|
|
195
|
+
const source = document.getText();
|
|
196
|
+
const cases = getActiveCasesForUri(params.textDocument.uri);
|
|
197
|
+
const lines = source.split("\n");
|
|
198
|
+
try {
|
|
199
|
+
const result = analyzeFile(filePath, source, cases);
|
|
200
|
+
const hints = [];
|
|
201
|
+
for (const hint of result.caseHints) {
|
|
202
|
+
const lineIdx = hint.line - 1;
|
|
203
|
+
if (lineIdx < 0 || lineIdx >= lines.length) continue;
|
|
204
|
+
const lineLen = lines[lineIdx].length;
|
|
205
|
+
hints.push({
|
|
206
|
+
position: { line: lineIdx, character: lineLen },
|
|
207
|
+
label: ` ${hint.label}`,
|
|
208
|
+
kind: InlayHintKind.Type,
|
|
209
|
+
paddingLeft: true
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return hints;
|
|
213
|
+
} catch {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
connection.onRequest("nudo/selectCase", (params) => {
|
|
218
|
+
const cases = getActiveCasesForUri(params.uri);
|
|
219
|
+
cases.set(params.functionName, params.caseIndex);
|
|
220
|
+
const document = documents.get(params.uri);
|
|
221
|
+
if (document) {
|
|
222
|
+
validateDocument(document);
|
|
223
|
+
}
|
|
224
|
+
connection.sendRequest(CodeLensRefreshRequest.type).catch(() => {
|
|
225
|
+
});
|
|
226
|
+
return { success: true };
|
|
227
|
+
});
|
|
228
|
+
connection.onRequest("nudo/getActiveCases", (params) => {
|
|
229
|
+
const cases = getActiveCasesForUri(params.uri);
|
|
230
|
+
const result = {};
|
|
231
|
+
for (const [fn, idx] of cases) {
|
|
232
|
+
result[fn] = idx;
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
235
|
+
});
|
|
236
|
+
var nudoFileCache = /* @__PURE__ */ new Map();
|
|
237
|
+
function isNudoFile(uri) {
|
|
238
|
+
if (!uri.endsWith(".js") && !uri.endsWith(".ts") && !uri.endsWith(".mjs")) return false;
|
|
239
|
+
const cached = nudoFileCache.get(uri);
|
|
240
|
+
if (cached !== void 0) return cached;
|
|
241
|
+
const doc = documents.get(uri);
|
|
242
|
+
if (!doc) return false;
|
|
243
|
+
const result = hasNudoDirectives(doc.getText());
|
|
244
|
+
nudoFileCache.set(uri, result);
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
function hasNudoDirectives(source) {
|
|
248
|
+
return /@nudo:(case|mock|pure|skip|sample|returns)\b/.test(source);
|
|
249
|
+
}
|
|
250
|
+
function uriToFilePath(uri) {
|
|
251
|
+
return uri.startsWith("file://") ? decodeURIComponent(uri.slice(7)) : uri;
|
|
252
|
+
}
|
|
253
|
+
documents.listen(connection);
|
|
254
|
+
connection.listen();
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nudojs/lsp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/server.ts",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"vscode-languageserver": "^9.0.0",
|
|
8
|
+
"vscode-languageserver-textdocument": "^1.0.0",
|
|
9
|
+
"@nudojs/service": "0.1.0",
|
|
10
|
+
"@nudojs/core": "0.1.0"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup src/server.ts --format esm --dts"
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createConnection,
|
|
3
|
+
TextDocuments,
|
|
4
|
+
ProposedFeatures,
|
|
5
|
+
TextDocumentSyncKind,
|
|
6
|
+
type InitializeParams,
|
|
7
|
+
type InitializeResult,
|
|
8
|
+
type Diagnostic as LspDiagnostic,
|
|
9
|
+
DiagnosticSeverity,
|
|
10
|
+
DiagnosticTag,
|
|
11
|
+
type CompletionItem as LspCompletionItem,
|
|
12
|
+
CompletionItemKind,
|
|
13
|
+
MarkupKind,
|
|
14
|
+
type CodeLens,
|
|
15
|
+
CodeLensRefreshRequest,
|
|
16
|
+
type InlayHint,
|
|
17
|
+
InlayHintKind,
|
|
18
|
+
} from "vscode-languageserver/node";
|
|
19
|
+
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
20
|
+
import { typeValueToString } from "@nudojs/core";
|
|
21
|
+
import {
|
|
22
|
+
analyzeFile,
|
|
23
|
+
getTypeAtPosition,
|
|
24
|
+
getCompletionsAtPosition,
|
|
25
|
+
getCasesForFile,
|
|
26
|
+
type DiagnosticSeverity as JsDiagSeverity,
|
|
27
|
+
} from "@nudojs/service";
|
|
28
|
+
|
|
29
|
+
const connection = createConnection(ProposedFeatures.all);
|
|
30
|
+
const documents = new TextDocuments(TextDocument);
|
|
31
|
+
|
|
32
|
+
const activeCases = new Map<string, Map<string, number>>();
|
|
33
|
+
|
|
34
|
+
function getActiveCasesForUri(uri: string): Map<string, number> {
|
|
35
|
+
const existing = activeCases.get(uri);
|
|
36
|
+
if (existing) return existing;
|
|
37
|
+
const map = new Map<string, number>();
|
|
38
|
+
activeCases.set(uri, map);
|
|
39
|
+
return map;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const severityMap: Record<JsDiagSeverity, DiagnosticSeverity> = {
|
|
43
|
+
error: DiagnosticSeverity.Error,
|
|
44
|
+
warning: DiagnosticSeverity.Warning,
|
|
45
|
+
info: DiagnosticSeverity.Information,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
connection.onInitialize((_params: InitializeParams): InitializeResult => ({
|
|
49
|
+
capabilities: {
|
|
50
|
+
textDocumentSync: TextDocumentSyncKind.Full,
|
|
51
|
+
hoverProvider: true,
|
|
52
|
+
completionProvider: {
|
|
53
|
+
triggerCharacters: ["."],
|
|
54
|
+
resolveProvider: false,
|
|
55
|
+
},
|
|
56
|
+
codeLensProvider: {
|
|
57
|
+
resolveProvider: false,
|
|
58
|
+
},
|
|
59
|
+
inlayHintProvider: true,
|
|
60
|
+
},
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
let debounceTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
64
|
+
|
|
65
|
+
documents.onDidChangeContent((change) => {
|
|
66
|
+
const uri = change.document.uri;
|
|
67
|
+
nudoFileCache.delete(uri);
|
|
68
|
+
const existing = debounceTimers.get(uri);
|
|
69
|
+
if (existing) clearTimeout(existing);
|
|
70
|
+
|
|
71
|
+
debounceTimers.set(
|
|
72
|
+
uri,
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
debounceTimers.delete(uri);
|
|
75
|
+
validateDocument(change.document);
|
|
76
|
+
}, 300),
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
documents.onDidClose((event) => {
|
|
81
|
+
const timer = debounceTimers.get(event.document.uri);
|
|
82
|
+
if (timer) clearTimeout(timer);
|
|
83
|
+
debounceTimers.delete(event.document.uri);
|
|
84
|
+
nudoFileCache.delete(event.document.uri);
|
|
85
|
+
connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
function validateDocument(document: TextDocument): void {
|
|
89
|
+
const uri = document.uri;
|
|
90
|
+
if (!isNudoFile(uri)) {
|
|
91
|
+
connection.sendDiagnostics({ uri, diagnostics: [] });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const filePath = uriToFilePath(uri);
|
|
96
|
+
const source = document.getText();
|
|
97
|
+
const cases = getActiveCasesForUri(uri);
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const result = analyzeFile(filePath, source, cases);
|
|
101
|
+
const diagnostics: LspDiagnostic[] = result.diagnostics.map((d) => {
|
|
102
|
+
const diag: LspDiagnostic = {
|
|
103
|
+
severity: severityMap[d.severity],
|
|
104
|
+
range: {
|
|
105
|
+
start: { line: d.range.start.line - 1, character: d.range.start.column },
|
|
106
|
+
end: { line: d.range.end.line - 1, character: d.range.end.column },
|
|
107
|
+
},
|
|
108
|
+
message: d.message,
|
|
109
|
+
source: "nudo",
|
|
110
|
+
};
|
|
111
|
+
if (d.tags?.includes("unnecessary")) {
|
|
112
|
+
diag.tags = [DiagnosticTag.Unnecessary];
|
|
113
|
+
}
|
|
114
|
+
return diag;
|
|
115
|
+
});
|
|
116
|
+
connection.sendDiagnostics({ uri, diagnostics });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
connection.sendDiagnostics({
|
|
119
|
+
uri,
|
|
120
|
+
diagnostics: [
|
|
121
|
+
{
|
|
122
|
+
severity: DiagnosticSeverity.Error,
|
|
123
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
|
|
124
|
+
message: `Analysis error: ${(err as Error).message}`,
|
|
125
|
+
source: "nudo",
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
connection.onHover((params) => {
|
|
133
|
+
const document = documents.get(params.textDocument.uri);
|
|
134
|
+
if (!document) return null;
|
|
135
|
+
if (!isNudoFile(params.textDocument.uri)) return null;
|
|
136
|
+
|
|
137
|
+
const filePath = uriToFilePath(params.textDocument.uri);
|
|
138
|
+
const source = document.getText();
|
|
139
|
+
const line = params.position.line + 1;
|
|
140
|
+
const column = params.position.character;
|
|
141
|
+
const cases = getActiveCasesForUri(params.textDocument.uri);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const tv = getTypeAtPosition(filePath, source, line, column, cases);
|
|
145
|
+
if (!tv) return null;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
contents: {
|
|
149
|
+
kind: MarkupKind.Markdown,
|
|
150
|
+
value: `\`\`\`nudo\n${typeValueToString(tv)}\n\`\`\``,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
connection.onCompletion((params) => {
|
|
159
|
+
const document = documents.get(params.textDocument.uri);
|
|
160
|
+
if (!document) return [];
|
|
161
|
+
if (!isNudoFile(params.textDocument.uri)) return [];
|
|
162
|
+
|
|
163
|
+
const filePath = uriToFilePath(params.textDocument.uri);
|
|
164
|
+
const source = document.getText();
|
|
165
|
+
const line = params.position.line + 1;
|
|
166
|
+
const column = params.position.character;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const items = getCompletionsAtPosition(filePath, source, line, column);
|
|
170
|
+
return items.map((item): LspCompletionItem => ({
|
|
171
|
+
label: item.label,
|
|
172
|
+
kind: item.kind === "method"
|
|
173
|
+
? CompletionItemKind.Method
|
|
174
|
+
: item.kind === "property"
|
|
175
|
+
? CompletionItemKind.Property
|
|
176
|
+
: CompletionItemKind.Variable,
|
|
177
|
+
detail: item.detail,
|
|
178
|
+
}));
|
|
179
|
+
} catch {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
connection.onCodeLens((params) => {
|
|
185
|
+
const document = documents.get(params.textDocument.uri);
|
|
186
|
+
if (!document) return [];
|
|
187
|
+
if (!isNudoFile(params.textDocument.uri)) return [];
|
|
188
|
+
|
|
189
|
+
const filePath = uriToFilePath(params.textDocument.uri);
|
|
190
|
+
const source = document.getText();
|
|
191
|
+
const cases = getActiveCasesForUri(params.textDocument.uri);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const fnCases = getCasesForFile(filePath, source);
|
|
195
|
+
const lenses: CodeLens[] = [];
|
|
196
|
+
|
|
197
|
+
for (const fn of fnCases) {
|
|
198
|
+
if (fn.cases.length === 0) continue;
|
|
199
|
+
const activeIdx = cases.get(fn.functionName) ?? 0;
|
|
200
|
+
|
|
201
|
+
for (const c of fn.cases) {
|
|
202
|
+
const isActive = c.index === activeIdx;
|
|
203
|
+
const title = isActive ? `● case "${c.name}"` : `○ case "${c.name}"`;
|
|
204
|
+
lenses.push({
|
|
205
|
+
range: {
|
|
206
|
+
start: { line: fn.loc.start.line - 1, character: 0 },
|
|
207
|
+
end: { line: fn.loc.start.line - 1, character: 0 },
|
|
208
|
+
},
|
|
209
|
+
command: {
|
|
210
|
+
title,
|
|
211
|
+
command: "nudo.selectCase",
|
|
212
|
+
arguments: [params.textDocument.uri, fn.functionName, c.index, c.name],
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return lenses;
|
|
219
|
+
} catch {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
connection.languages.inlayHint.on((params) => {
|
|
225
|
+
const document = documents.get(params.textDocument.uri);
|
|
226
|
+
if (!document) return [];
|
|
227
|
+
if (!isNudoFile(params.textDocument.uri)) return [];
|
|
228
|
+
|
|
229
|
+
const filePath = uriToFilePath(params.textDocument.uri);
|
|
230
|
+
const source = document.getText();
|
|
231
|
+
const cases = getActiveCasesForUri(params.textDocument.uri);
|
|
232
|
+
const lines = source.split("\n");
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const result = analyzeFile(filePath, source, cases);
|
|
236
|
+
const hints: InlayHint[] = [];
|
|
237
|
+
|
|
238
|
+
for (const hint of result.caseHints) {
|
|
239
|
+
const lineIdx = hint.line - 1;
|
|
240
|
+
if (lineIdx < 0 || lineIdx >= lines.length) continue;
|
|
241
|
+
const lineLen = lines[lineIdx].length;
|
|
242
|
+
|
|
243
|
+
hints.push({
|
|
244
|
+
position: { line: lineIdx, character: lineLen },
|
|
245
|
+
label: ` ${hint.label}`,
|
|
246
|
+
kind: InlayHintKind.Type,
|
|
247
|
+
paddingLeft: true,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return hints;
|
|
252
|
+
} catch {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
connection.onRequest("nudo/selectCase", (params: { uri: string; functionName: string; caseIndex: number }) => {
|
|
258
|
+
const cases = getActiveCasesForUri(params.uri);
|
|
259
|
+
cases.set(params.functionName, params.caseIndex);
|
|
260
|
+
|
|
261
|
+
const document = documents.get(params.uri);
|
|
262
|
+
if (document) {
|
|
263
|
+
validateDocument(document);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
connection.sendRequest(CodeLensRefreshRequest.type).catch(() => {});
|
|
267
|
+
|
|
268
|
+
return { success: true };
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
connection.onRequest("nudo/getActiveCases", (params: { uri: string }) => {
|
|
272
|
+
const cases = getActiveCasesForUri(params.uri);
|
|
273
|
+
const result: Record<string, number> = {};
|
|
274
|
+
for (const [fn, idx] of cases) {
|
|
275
|
+
result[fn] = idx;
|
|
276
|
+
}
|
|
277
|
+
return result;
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const nudoFileCache = new Map<string, boolean>();
|
|
281
|
+
|
|
282
|
+
function isNudoFile(uri: string): boolean {
|
|
283
|
+
if (!uri.endsWith(".js") && !uri.endsWith(".ts") && !uri.endsWith(".mjs")) return false;
|
|
284
|
+
const cached = nudoFileCache.get(uri);
|
|
285
|
+
if (cached !== undefined) return cached;
|
|
286
|
+
const doc = documents.get(uri);
|
|
287
|
+
if (!doc) return false;
|
|
288
|
+
const result = hasNudoDirectives(doc.getText());
|
|
289
|
+
nudoFileCache.set(uri, result);
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function hasNudoDirectives(source: string): boolean {
|
|
294
|
+
return /@nudo:(case|mock|pure|skip|sample|returns)\b/.test(source);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function uriToFilePath(uri: string): string {
|
|
298
|
+
return uri.startsWith("file://") ? decodeURIComponent(uri.slice(7)) : uri;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
documents.listen(connection);
|
|
302
|
+
connection.listen();
|