@markdy/language-server 0.7.15
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/LICENSE +1 -0
- package/README.md +15 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +367 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @markdy/language-server
|
|
2
|
+
|
|
3
|
+
Language Server Protocol (LSP) server for MarkdyScript.
|
|
4
|
+
|
|
5
|
+
Current capabilities:
|
|
6
|
+
- parse diagnostics from `@markdy/core`
|
|
7
|
+
- actor-aware action completion
|
|
8
|
+
- hover docs for common actions
|
|
9
|
+
- document symbols for scenes, actors, defs, and seqs
|
|
10
|
+
|
|
11
|
+
Run on stdio:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npx @markdy/language-server
|
|
15
|
+
```
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { ParseError, parse } from "@markdy/core";
|
|
5
|
+
import {
|
|
6
|
+
CompletionItemKind,
|
|
7
|
+
createConnection,
|
|
8
|
+
DiagnosticSeverity,
|
|
9
|
+
ProposedFeatures,
|
|
10
|
+
SymbolKind,
|
|
11
|
+
TextDocumentSyncKind,
|
|
12
|
+
TextDocuments
|
|
13
|
+
} from "vscode-languageserver/node.js";
|
|
14
|
+
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
15
|
+
var connection = createConnection(ProposedFeatures.all);
|
|
16
|
+
var documents = new TextDocuments(TextDocument);
|
|
17
|
+
var UNIVERSAL_ACTIONS = [
|
|
18
|
+
"enter",
|
|
19
|
+
"exit",
|
|
20
|
+
"move",
|
|
21
|
+
"fade_in",
|
|
22
|
+
"fade_out",
|
|
23
|
+
"scale",
|
|
24
|
+
"rotate",
|
|
25
|
+
"shake",
|
|
26
|
+
"say",
|
|
27
|
+
"throw",
|
|
28
|
+
"play"
|
|
29
|
+
];
|
|
30
|
+
var FIGURE_ACTIONS = [
|
|
31
|
+
"punch",
|
|
32
|
+
"kick",
|
|
33
|
+
"wave",
|
|
34
|
+
"nod",
|
|
35
|
+
"jump",
|
|
36
|
+
"bounce",
|
|
37
|
+
"face",
|
|
38
|
+
"rotate_part",
|
|
39
|
+
"pose"
|
|
40
|
+
];
|
|
41
|
+
var SYSTEM_ACTIONS = ["request", "response", "emit"];
|
|
42
|
+
var KEYWORDS = [
|
|
43
|
+
"scene",
|
|
44
|
+
"actor",
|
|
45
|
+
"asset",
|
|
46
|
+
"var",
|
|
47
|
+
"def",
|
|
48
|
+
"seq",
|
|
49
|
+
"preset",
|
|
50
|
+
"import",
|
|
51
|
+
"camera"
|
|
52
|
+
];
|
|
53
|
+
var SEMANTIC_TOKEN_TYPES = ["keyword", "variable", "method", "class"];
|
|
54
|
+
var SEMANTIC_TOKEN_TYPE_INDEX = new Map(
|
|
55
|
+
SEMANTIC_TOKEN_TYPES.map((type, i) => [type, i])
|
|
56
|
+
);
|
|
57
|
+
var ACTION_DOCS = {
|
|
58
|
+
enter: "Slide actor in from edge. Params: `from`, `dur`, `ease`.",
|
|
59
|
+
exit: "Slide actor out to edge. Params: `to`, `dur`, `ease`.",
|
|
60
|
+
move: "Move actor to coordinate. Params: `to=(x,y)`, `dur`, `ease`.",
|
|
61
|
+
fade_in: "Fade actor opacity to 1. Params: `dur`, `ease`.",
|
|
62
|
+
fade_out: "Fade actor opacity to 0. Params: `dur`, `ease`.",
|
|
63
|
+
say: "Show a speech bubble. Params: `text`, `dur`.",
|
|
64
|
+
play: "Expand sequence on actor. Params: `seqName, ...args`.",
|
|
65
|
+
request: "Draw an outbound flow edge. Params: `to`, `label`, `dur`, `style`.",
|
|
66
|
+
response: "Draw a return flow edge. Params: `to`, `label`, `dur`, `style`.",
|
|
67
|
+
emit: "Draw async fire-and-forget flow edge. Params: `to`, `label`, `dur`, `style=fire_and_forget`.",
|
|
68
|
+
pan: "Move camera center. Params: `to=(x,y)`, `dur`, `ease`.",
|
|
69
|
+
zoom: "Change camera zoom. Params: `to`, `dur`, `ease`.",
|
|
70
|
+
shake: "Shake actor/camera. Params: `intensity`, `dur`."
|
|
71
|
+
};
|
|
72
|
+
function extractActors(text) {
|
|
73
|
+
const actors = [];
|
|
74
|
+
const lines = text.split(/\r?\n/);
|
|
75
|
+
for (let i = 0; i < lines.length; i++) {
|
|
76
|
+
const m = /^actor\s+(\w+)\s*=\s*([\w.]+)\(/.exec(lines[i].trim());
|
|
77
|
+
if (!m) continue;
|
|
78
|
+
actors.push({ name: m[1], type: m[2], line: i });
|
|
79
|
+
}
|
|
80
|
+
return actors;
|
|
81
|
+
}
|
|
82
|
+
function getActionsForActorType(actorType) {
|
|
83
|
+
const out = [...UNIVERSAL_ACTIONS];
|
|
84
|
+
if (actorType === "figure") out.push(...FIGURE_ACTIONS);
|
|
85
|
+
if (actorType === "service" || actorType === "db" || actorType === "queue" || actorType === "client") {
|
|
86
|
+
out.push(...SYSTEM_ACTIONS);
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
function lineRange(doc, lineNumber) {
|
|
91
|
+
const line = Math.max(0, Math.min(lineNumber, doc.lineCount - 1));
|
|
92
|
+
const text = doc.getText({
|
|
93
|
+
start: { line, character: 0 },
|
|
94
|
+
end: { line, character: 1e4 }
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
start: { line, character: 0 },
|
|
98
|
+
end: { line, character: Math.max(1, text.length) }
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function parseWarningsToDiagnostics(warnings, doc) {
|
|
102
|
+
return warnings.map((w) => ({
|
|
103
|
+
severity: DiagnosticSeverity.Warning,
|
|
104
|
+
range: lineRange(doc, w.line - 1),
|
|
105
|
+
message: w.message,
|
|
106
|
+
source: "markdy"
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
function validateTextDocument(document) {
|
|
110
|
+
const text = document.getText();
|
|
111
|
+
const diagnostics = [];
|
|
112
|
+
try {
|
|
113
|
+
const ast = parse(text);
|
|
114
|
+
diagnostics.push(...parseWarningsToDiagnostics(ast.warnings, document));
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (err instanceof ParseError) {
|
|
117
|
+
diagnostics.push({
|
|
118
|
+
severity: DiagnosticSeverity.Error,
|
|
119
|
+
range: lineRange(document, err.line - 1),
|
|
120
|
+
message: err.message.replace(/^Line \d+:\s*/, ""),
|
|
121
|
+
source: "markdy"
|
|
122
|
+
});
|
|
123
|
+
} else if (err instanceof Error) {
|
|
124
|
+
diagnostics.push({
|
|
125
|
+
severity: DiagnosticSeverity.Error,
|
|
126
|
+
range: lineRange(document, 0),
|
|
127
|
+
message: err.message,
|
|
128
|
+
source: "markdy"
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
connection.sendDiagnostics({ uri: document.uri, diagnostics });
|
|
133
|
+
}
|
|
134
|
+
function getLinePrefix(doc, position) {
|
|
135
|
+
const lineStart = doc.offsetAt({ line: position.line, character: 0 });
|
|
136
|
+
const at = doc.offsetAt(position);
|
|
137
|
+
return doc.getText().slice(lineStart, at);
|
|
138
|
+
}
|
|
139
|
+
function toMethodCompletion(action) {
|
|
140
|
+
return {
|
|
141
|
+
label: action,
|
|
142
|
+
kind: CompletionItemKind.Method,
|
|
143
|
+
detail: "Markdy action",
|
|
144
|
+
documentation: ACTION_DOCS[action] ?? void 0,
|
|
145
|
+
insertText: `${action}()`
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function getTokenAtPosition(doc, position) {
|
|
149
|
+
const lineText = doc.getText({
|
|
150
|
+
start: { line: position.line, character: 0 },
|
|
151
|
+
end: { line: position.line, character: 1e4 }
|
|
152
|
+
});
|
|
153
|
+
const idx = Math.min(position.character, lineText.length);
|
|
154
|
+
const left = lineText.slice(0, idx).match(/[A-Za-z_][\w.]*$/)?.[0] ?? "";
|
|
155
|
+
const rightMatch = lineText.slice(idx).match(/^[\w.]*/);
|
|
156
|
+
const right = rightMatch ? rightMatch[0] : "";
|
|
157
|
+
const token = `${left}${right}`;
|
|
158
|
+
return token || null;
|
|
159
|
+
}
|
|
160
|
+
function pushSemanticToken(data, state, line, char, length, type) {
|
|
161
|
+
const tokenType = SEMANTIC_TOKEN_TYPE_INDEX.get(type);
|
|
162
|
+
if (tokenType === void 0) return;
|
|
163
|
+
const deltaLine = line - state.line;
|
|
164
|
+
const deltaStart = deltaLine === 0 ? char - state.char : char;
|
|
165
|
+
data.push(deltaLine, deltaStart, length, tokenType, 0);
|
|
166
|
+
state.line = line;
|
|
167
|
+
state.char = char;
|
|
168
|
+
}
|
|
169
|
+
function buildSemanticTokenData(document) {
|
|
170
|
+
const data = [];
|
|
171
|
+
const cursor = { line: 0, char: 0 };
|
|
172
|
+
const text = document.getText();
|
|
173
|
+
const actors = new Set(extractActors(text).map((a) => a.name));
|
|
174
|
+
const lines = text.split(/\r?\n/);
|
|
175
|
+
for (let line = 0; line < lines.length; line++) {
|
|
176
|
+
const content = lines[line];
|
|
177
|
+
const head = content.trim();
|
|
178
|
+
for (const keyword of KEYWORDS) {
|
|
179
|
+
const idx = head.indexOf(keyword);
|
|
180
|
+
if (idx === 0) {
|
|
181
|
+
const start = content.indexOf(keyword);
|
|
182
|
+
if (start >= 0) pushSemanticToken(data, cursor, line, start, keyword.length, "keyword");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const name of actors) {
|
|
186
|
+
const match = new RegExp(`\\b${name}\\b`).exec(content);
|
|
187
|
+
if (match?.index !== void 0) {
|
|
188
|
+
pushSemanticToken(data, cursor, line, match.index, name.length, "variable");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const actionMatch = /\.\s*([A-Za-z_]\w*)\s*\(/.exec(content);
|
|
192
|
+
if (actionMatch?.index !== void 0) {
|
|
193
|
+
const actionName = actionMatch[1];
|
|
194
|
+
const actionStart = actionMatch.index + actionMatch[0].indexOf(actionName);
|
|
195
|
+
pushSemanticToken(data, cursor, line, actionStart, actionName.length, "method");
|
|
196
|
+
}
|
|
197
|
+
const defMatch = /^def\s+([A-Za-z_]\w*)/.exec(head);
|
|
198
|
+
if (defMatch) {
|
|
199
|
+
const start = content.indexOf(defMatch[1]);
|
|
200
|
+
if (start >= 0) pushSemanticToken(data, cursor, line, start, defMatch[1].length, "class");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return data;
|
|
204
|
+
}
|
|
205
|
+
function buildDocumentSymbols(document) {
|
|
206
|
+
const symbols = [];
|
|
207
|
+
const lines = document.getText().split(/\r?\n/);
|
|
208
|
+
for (let i = 0; i < lines.length; i++) {
|
|
209
|
+
const raw = lines[i].trim();
|
|
210
|
+
const scene = /^scene\s+"([^"]+)"/.exec(raw);
|
|
211
|
+
if (scene) {
|
|
212
|
+
symbols.push({
|
|
213
|
+
name: scene[1],
|
|
214
|
+
detail: "scene",
|
|
215
|
+
kind: SymbolKind.Namespace,
|
|
216
|
+
range: lineRange(document, i),
|
|
217
|
+
selectionRange: lineRange(document, i),
|
|
218
|
+
children: []
|
|
219
|
+
});
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const actor = /^actor\s+(\w+)\s*=/.exec(raw);
|
|
223
|
+
if (actor) {
|
|
224
|
+
symbols.push({
|
|
225
|
+
name: actor[1],
|
|
226
|
+
detail: "actor",
|
|
227
|
+
kind: SymbolKind.Variable,
|
|
228
|
+
range: lineRange(document, i),
|
|
229
|
+
selectionRange: lineRange(document, i),
|
|
230
|
+
children: []
|
|
231
|
+
});
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const seq = /^seq\s+(\w+)/.exec(raw);
|
|
235
|
+
if (seq) {
|
|
236
|
+
symbols.push({
|
|
237
|
+
name: seq[1],
|
|
238
|
+
detail: "sequence",
|
|
239
|
+
kind: SymbolKind.Function,
|
|
240
|
+
range: lineRange(document, i),
|
|
241
|
+
selectionRange: lineRange(document, i),
|
|
242
|
+
children: []
|
|
243
|
+
});
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const def = /^def\s+(\w+)/.exec(raw);
|
|
247
|
+
if (def) {
|
|
248
|
+
symbols.push({
|
|
249
|
+
name: def[1],
|
|
250
|
+
detail: "template",
|
|
251
|
+
kind: SymbolKind.Class,
|
|
252
|
+
range: lineRange(document, i),
|
|
253
|
+
selectionRange: lineRange(document, i),
|
|
254
|
+
children: []
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return symbols;
|
|
259
|
+
}
|
|
260
|
+
connection.onInitialize((_params) => ({
|
|
261
|
+
capabilities: {
|
|
262
|
+
textDocumentSync: TextDocumentSyncKind.Incremental,
|
|
263
|
+
completionProvider: {
|
|
264
|
+
resolveProvider: false,
|
|
265
|
+
triggerCharacters: [".", "@"]
|
|
266
|
+
},
|
|
267
|
+
hoverProvider: true,
|
|
268
|
+
documentSymbolProvider: true,
|
|
269
|
+
semanticTokensProvider: {
|
|
270
|
+
full: true,
|
|
271
|
+
legend: {
|
|
272
|
+
tokenTypes: [...SEMANTIC_TOKEN_TYPES],
|
|
273
|
+
tokenModifiers: []
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}));
|
|
278
|
+
documents.onDidOpen((e) => validateTextDocument(e.document));
|
|
279
|
+
documents.onDidChangeContent((e) => validateTextDocument(e.document));
|
|
280
|
+
connection.onCompletion((params) => {
|
|
281
|
+
const doc = documents.get(params.textDocument.uri);
|
|
282
|
+
if (!doc) return [];
|
|
283
|
+
const actors = extractActors(doc.getText());
|
|
284
|
+
const actorByName = new Map(actors.map((a) => [a.name, a.type]));
|
|
285
|
+
const prefix = getLinePrefix(doc, params.position);
|
|
286
|
+
const actorAction = /(\w+)\.([A-Za-z_]*)$/.exec(prefix);
|
|
287
|
+
if (actorAction) {
|
|
288
|
+
const actorName = actorAction[1];
|
|
289
|
+
const partial = actorAction[2] ?? "";
|
|
290
|
+
const actorType = actorByName.get(actorName);
|
|
291
|
+
if (!actorType) return [];
|
|
292
|
+
return getActionsForActorType(actorType).filter((a) => a.startsWith(partial)).map(toMethodCompletion);
|
|
293
|
+
}
|
|
294
|
+
const lineEventPrefix = /@[\d.+]*:\s*$/.test(prefix);
|
|
295
|
+
if (lineEventPrefix) {
|
|
296
|
+
return actors.map((a) => ({
|
|
297
|
+
label: a.name,
|
|
298
|
+
kind: CompletionItemKind.Variable,
|
|
299
|
+
detail: `${a.type} actor`,
|
|
300
|
+
insertText: `${a.name}.`
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
const items = [];
|
|
304
|
+
for (const keyword of KEYWORDS) {
|
|
305
|
+
items.push({
|
|
306
|
+
label: keyword,
|
|
307
|
+
kind: CompletionItemKind.Keyword,
|
|
308
|
+
detail: "Markdy keyword"
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
for (const actor of actors) {
|
|
312
|
+
items.push({
|
|
313
|
+
label: actor.name,
|
|
314
|
+
kind: CompletionItemKind.Variable,
|
|
315
|
+
detail: `${actor.type} actor`
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return items;
|
|
319
|
+
});
|
|
320
|
+
connection.onHover((params) => {
|
|
321
|
+
const doc = documents.get(params.textDocument.uri);
|
|
322
|
+
if (!doc) return null;
|
|
323
|
+
const token = getTokenAtPosition(doc, params.position);
|
|
324
|
+
if (!token) return null;
|
|
325
|
+
const actor = extractActors(doc.getText()).find((a) => a.name === token);
|
|
326
|
+
if (actor) {
|
|
327
|
+
return {
|
|
328
|
+
contents: {
|
|
329
|
+
kind: "markdown",
|
|
330
|
+
value: `**${actor.name}**
|
|
331
|
+
|
|
332
|
+
Actor type: \`${actor.type}\``
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
if (ACTION_DOCS[token]) {
|
|
337
|
+
return {
|
|
338
|
+
contents: {
|
|
339
|
+
kind: "markdown",
|
|
340
|
+
value: `**${token}**
|
|
341
|
+
|
|
342
|
+
${ACTION_DOCS[token]}`
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (KEYWORDS.includes(token)) {
|
|
347
|
+
return {
|
|
348
|
+
contents: {
|
|
349
|
+
kind: "markdown",
|
|
350
|
+
value: `Markdy keyword: \`${token}\``
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
});
|
|
356
|
+
connection.onDocumentSymbol((params) => {
|
|
357
|
+
const doc = documents.get(params.textDocument.uri);
|
|
358
|
+
if (!doc) return [];
|
|
359
|
+
return buildDocumentSymbols(doc);
|
|
360
|
+
});
|
|
361
|
+
connection.languages.semanticTokens.on((params) => {
|
|
362
|
+
const doc = documents.get(params.textDocument.uri);
|
|
363
|
+
if (!doc) return { data: [] };
|
|
364
|
+
return { data: buildSemanticTokenData(doc) };
|
|
365
|
+
});
|
|
366
|
+
documents.listen(connection);
|
|
367
|
+
connection.listen();
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@markdy/language-server",
|
|
3
|
+
"version": "0.7.15",
|
|
4
|
+
"description": "Language Server Protocol implementation for MarkdyScript.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"bin": {
|
|
16
|
+
"markdy-language-server": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"markdy",
|
|
26
|
+
"lsp",
|
|
27
|
+
"language-server",
|
|
28
|
+
"markdyscript",
|
|
29
|
+
"editor"
|
|
30
|
+
],
|
|
31
|
+
"author": "Hoang Yell <hoangyell@gmail.com> (https://hoangyell.com)",
|
|
32
|
+
"homepage": "https://markdy.com",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/HoangYell/markdy-com.git",
|
|
36
|
+
"directory": "packages/markdy-language-server"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/HoangYell/markdy-com/issues"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"vscode-languageserver": "^9.0.1",
|
|
46
|
+
"vscode-languageserver-textdocument": "^1.0.12",
|
|
47
|
+
"@markdy/core": "0.7.15"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"tsup": "^8.5.1",
|
|
51
|
+
"typescript": "^5.9.3"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsup",
|
|
55
|
+
"test": "echo \"No tests yet\"",
|
|
56
|
+
"typecheck": "tsc --noEmit",
|
|
57
|
+
"lint": "tsc --noEmit"
|
|
58
|
+
}
|
|
59
|
+
}
|