pulse-rb 1.2.24

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.
Files changed (54) hide show
  1. package/README.md +225 -0
  2. package/adapters/linoria.lua +233 -0
  3. package/adapters/windui.llms.txt +366 -0
  4. package/adapters/windui.lua +505 -0
  5. package/bin/rb.js +2 -0
  6. package/dist/index.js +3285 -0
  7. package/package.json +59 -0
  8. package/pulse/dev/debuggui.lua +1206 -0
  9. package/pulse/dev/devconfig.lua +81 -0
  10. package/pulse/dev/ui/9_DevPanel.lua +384 -0
  11. package/pulse/helpers/aim.lua +193 -0
  12. package/pulse/helpers/cache.lua +68 -0
  13. package/pulse/helpers/cleaner.lua +110 -0
  14. package/pulse/helpers/conn.lua +33 -0
  15. package/pulse/helpers/cooldown.lua +47 -0
  16. package/pulse/helpers/draw.lua +122 -0
  17. package/pulse/helpers/hitbox.lua +63 -0
  18. package/pulse/helpers/input.lua +24 -0
  19. package/pulse/helpers/log.lua +228 -0
  20. package/pulse/helpers/loop.lua +58 -0
  21. package/pulse/helpers/memory.lua +48 -0
  22. package/pulse/helpers/narrate.lua +160 -0
  23. package/pulse/helpers/notify.lua +51 -0
  24. package/pulse/helpers/perf.lua +48 -0
  25. package/pulse/helpers/remote.lua +128 -0
  26. package/pulse/helpers/restore.lua +59 -0
  27. package/pulse/helpers/store.lua +39 -0
  28. package/pulse/helpers/team.lua +83 -0
  29. package/pulse/helpers/testmode.lua +80 -0
  30. package/pulse/helpers/trace.lua +111 -0
  31. package/pulse/helpers/track.lua +85 -0
  32. package/pulse/helpers/vec.lua +52 -0
  33. package/pulse/helpers/world.lua +51 -0
  34. package/pulse/runtime.lua +343 -0
  35. package/pulse/ui/linoria_settings.lua +55 -0
  36. package/pulse/ui/windui_settings.lua +87 -0
  37. package/templates/AGENTS.md +177 -0
  38. package/templates/CLAUDE.md +424 -0
  39. package/templates/deploy_config.example +17 -0
  40. package/templates/example_esp.rblua +69 -0
  41. package/templates/example_fov.rblua +20 -0
  42. package/templates/example_speed.rblua +25 -0
  43. package/templates/gitignore +4 -0
  44. package/templates/globals.lua +66 -0
  45. package/templates/layout.rblua +28 -0
  46. package/templates/module.lua +7 -0
  47. package/templates/module.rblua +32 -0
  48. package/templates/page_home.rblua +9 -0
  49. package/templates/remote.lua +6 -0
  50. package/templates/remotes.lua +14 -0
  51. package/vscode/language-configuration.json +35 -0
  52. package/vscode/package.json +35 -0
  53. package/vscode/src/extension.js +397 -0
  54. package/vscode/syntaxes/rblua.tmLanguage.json +126 -0
@@ -0,0 +1,397 @@
1
+ 'use strict';
2
+
3
+ const vscode = require('vscode');
4
+
5
+ const VALID_SIDES = new Set(['left', 'right']);
6
+ const KNOWN_EVENTS = new Set([
7
+ 'Heartbeat', 'RenderStepped', 'Stepped',
8
+ 'InputBegan', 'InputEnded', 'InputChanged',
9
+ 'Respawn', 'Touched', 'TouchEnded',
10
+ ]);
11
+
12
+ let _diag;
13
+
14
+ // ─── Activation / Deactivation ───────────────────────────────────────────────
15
+
16
+ function activate(context) {
17
+ _diag = vscode.languages.createDiagnosticCollection('rblua');
18
+ context.subscriptions.push(_diag);
19
+
20
+ context.subscriptions.push(
21
+ vscode.languages.registerDocumentFormattingEditProvider('rblua', {
22
+ provideDocumentFormattingEdits: doc => format(doc),
23
+ })
24
+ );
25
+
26
+ context.subscriptions.push(
27
+ vscode.languages.registerCompletionItemProvider(
28
+ 'rblua',
29
+ { provideCompletionItems: (doc, pos) => completions(doc, pos) }
30
+ )
31
+ );
32
+
33
+ const lint = doc => { if (doc.languageId === 'rblua') lintDoc(doc); };
34
+ context.subscriptions.push(
35
+ vscode.workspace.onDidOpenTextDocument(lint),
36
+ vscode.workspace.onDidChangeTextDocument(e => lint(e.document)),
37
+ vscode.workspace.onDidCloseTextDocument(doc => _diag.delete(doc.uri)),
38
+ );
39
+ vscode.workspace.textDocuments.forEach(lint);
40
+ }
41
+
42
+ function deactivate() { if (_diag) _diag.dispose(); }
43
+
44
+ module.exports = { activate, deactivate };
45
+
46
+ // ─── Linter ──────────────────────────────────────────────────────────────────
47
+
48
+ function lintDoc(doc) {
49
+ const text = doc.getText();
50
+ const lines = text.split('\n');
51
+ const diags = [];
52
+
53
+ const stripped = blankStringsAndComments(text);
54
+ let depth = 0;
55
+ for (let i = 0; i < stripped.length; i++) {
56
+ const ch = stripped[i];
57
+ if (ch === '{') { depth++; continue; }
58
+ if (ch !== '}') continue;
59
+ depth--;
60
+ if (depth < 0) {
61
+ const pos = doc.positionAt(i);
62
+ diags.push(mkDiag(
63
+ new vscode.Range(pos, pos.translate(0, 1)),
64
+ 'Unexpected closing brace', vscode.DiagnosticSeverity.Error));
65
+ depth = 0;
66
+ }
67
+ }
68
+ if (depth > 0) {
69
+ const last = new vscode.Position(Math.max(0, doc.lineCount - 1), 0);
70
+ diags.push(mkDiag(new vscode.Range(last, last),
71
+ `${depth} unclosed brace${depth > 1 ? 's' : ''}`, vscode.DiagnosticSeverity.Error));
72
+ }
73
+
74
+ let signals = new Set(), inComponent = false;
75
+ for (let ln = 0; ln < lines.length; ln++) {
76
+ const raw = lines[ln], line = raw.trim();
77
+ const rng = new vscode.Range(ln, 0, ln, raw.length);
78
+
79
+ if (/^component\s*\{/.test(line)) { inComponent = true; signals = new Set(); }
80
+
81
+ const sigM = line.match(/^signal\s+(\w+)\s*=\s*(.+)$/);
82
+ if (sigM) {
83
+ signals.add(sigM[1]);
84
+ const val = sigM[2].trim().replace(/\s*--.*$/, '');
85
+ if (!isLiteral(val))
86
+ diags.push(mkDiag(rng,
87
+ `Signal default must be a string, number, or boolean — got: ${val}`,
88
+ vscode.DiagnosticSeverity.Warning));
89
+ continue;
90
+ }
91
+
92
+ const onM = line.match(/^on\s+(\w+)/);
93
+ if (onM && inComponent && signals.size > 0) {
94
+ const name = onM[1];
95
+ if (/^[a-z]/.test(name) && !signals.has(name) && !KNOWN_EVENTS.has(name))
96
+ diags.push(mkDiag(rng,
97
+ `'${name}' is not a declared signal in this component`,
98
+ vscode.DiagnosticSeverity.Warning));
99
+ }
100
+
101
+ const gbM = line.match(/^groupbox\s+(\w+)\s+"/);
102
+ if (gbM && !VALID_SIDES.has(gbM[1]))
103
+ diags.push(mkDiag(rng,
104
+ `Groupbox side must be 'left' or 'right', got '${gbM[1]}'`,
105
+ vscode.DiagnosticSeverity.Error));
106
+ }
107
+
108
+ _diag.set(doc.uri, diags);
109
+ }
110
+
111
+ function isLiteral(v) {
112
+ return /^"[^"]*"$/.test(v) || /^'[^']*'$/.test(v)
113
+ || /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(v)
114
+ || v === 'true' || v === 'false' || v === 'nil'
115
+ || /^\{[^}]*\}$/.test(v);
116
+ }
117
+ function mkDiag(range, msg, severity) {
118
+ const d = new vscode.Diagnostic(range, msg, severity);
119
+ d.source = 'rblua'; return d;
120
+ }
121
+
122
+ // ─── Formatter ───────────────────────────────────────────────────────────────
123
+
124
+ function format(doc) {
125
+ const text = doc.getText(), formatted = formatText(text);
126
+ if (formatted === text) return [];
127
+ return [vscode.TextEdit.replace(
128
+ new vscode.Range(doc.positionAt(0), doc.positionAt(text.length)), formatted)];
129
+ }
130
+
131
+ // init / on / func blocks contain Lua code; ui / page / groupbox blocks contain DSL.
132
+ function isLuaBlockOpener(clean) {
133
+ return /\binit\s*\{/.test(clean)
134
+ || /\bfunc\s+\w/.test(clean)
135
+ || /\bon\s+\w/.test(clean);
136
+ }
137
+
138
+ // ── Lua keyword indentation helpers ──────────────────────────────────────────
139
+
140
+ // How much to de-indent BEFORE emitting this Lua line.
141
+ function luaPreDecrement(s, leadClose) {
142
+ if (/^end\b/.test(s)) return 1;
143
+ if (/^else\b/.test(s)) return 1;
144
+ if (/^elseif\b/.test(s)) return 1;
145
+ if (/^until\b/.test(s)) return 1;
146
+ if (leadClose) return 1; // leading }
147
+ return 0;
148
+ }
149
+
150
+ // Net depth change AFTER emitting this Lua line.
151
+ // preDecrement tells us which closing keywords were already handled.
152
+ function luaPostNet(clean, opens, closes, pre) {
153
+ const s = clean.trimStart();
154
+
155
+ let openCount = (clean.match(/\bthen\b/g) || []).length
156
+ + (clean.match(/\bdo\b/g) || []).length
157
+ + (clean.match(/\brepeat\b/g) || []).length
158
+ + (clean.match(/\bfunction\b/g) || []).length
159
+ + opens;
160
+
161
+ // 'else' implicitly opens the next block (no explicit keyword like 'then')
162
+ if (/^else\b/.test(s) && !/^elseif\b/.test(s)) openCount += 1;
163
+
164
+ let closeCount = (clean.match(/\bend\b/g) || []).length
165
+ + (clean.match(/\buntil\b/g) || []).length
166
+ + closes;
167
+
168
+ // Subtract already-handled pre-decrements so we don't double-count
169
+ if (/^end\b/.test(s)) closeCount -= 1;
170
+ if (/^until\b/.test(s)) closeCount -= 1;
171
+ if (clean.trimStart().startsWith('}')) closeCount -= 1;
172
+
173
+ return openCount - closeCount;
174
+ }
175
+
176
+ // ── Main formatter ────────────────────────────────────────────────────────────
177
+
178
+ function formatText(text) {
179
+ const TAB = ' ';
180
+ const lines = text.split('\n');
181
+ const out = [];
182
+ let depth = 0, lastBlank = false;
183
+
184
+ // luaDepth > 0: we're inside a Lua code block.
185
+ // luaFloor: minimum depth allowed inside this Lua block (= content base depth).
186
+ let luaDepth = 0, luaFloor = 0;
187
+
188
+ for (const raw of lines) {
189
+ const line = raw.trim();
190
+ if (!line) {
191
+ if (!lastBlank && out.length > 0) out.push('');
192
+ lastBlank = true; continue;
193
+ }
194
+ lastBlank = false;
195
+
196
+ const clean = stripLineForCount(line);
197
+ const opens = (clean.match(/\{/g) || []).length;
198
+ const closes = (clean.match(/\}/g) || []).length;
199
+ const leadClose = clean.trimStart().startsWith('}');
200
+
201
+ if (luaDepth > 0) {
202
+ const net = opens - closes;
203
+
204
+ if (leadClose && luaDepth + net <= 0) {
205
+ // This } is the Lua block's closing brace — back to DSL mode
206
+ luaDepth = 0;
207
+ depth = Math.max(0, depth - 1);
208
+ out.push(TAB.repeat(depth) + line);
209
+ const afterNet = opens - (closes - 1);
210
+ if (afterNet > 0) depth += afterNet;
211
+ } else {
212
+ // Lua code — apply Lua-aware keyword indentation
213
+ const pre = luaPreDecrement(clean.trimStart(), leadClose);
214
+ depth = Math.max(luaFloor, depth - pre);
215
+ out.push(TAB.repeat(depth) + line);
216
+ depth = Math.max(luaFloor, depth + luaPostNet(clean, opens, closes, pre));
217
+ luaDepth += net;
218
+ }
219
+ continue;
220
+ }
221
+
222
+ // ── DSL mode ─────────────────────────────────────────────────────────
223
+ if (leadClose) depth = Math.max(0, depth - 1);
224
+ out.push(TAB.repeat(depth) + line);
225
+
226
+ if (opens > 0) {
227
+ const netAfter = leadClose ? opens - (closes - 1) : opens - closes;
228
+ if (isLuaBlockOpener(clean)) {
229
+ luaFloor = depth + netAfter;
230
+ luaDepth = netAfter;
231
+ }
232
+ depth = Math.max(0, depth + netAfter);
233
+ } else if (!leadClose && closes > 0) {
234
+ depth = Math.max(0, depth - closes);
235
+ }
236
+ }
237
+
238
+ while (out.length && !out[out.length - 1]) out.pop();
239
+ return out.join('\n') + '\n';
240
+ }
241
+
242
+ function stripLineForCount(line) {
243
+ let r = '', i = 0;
244
+ while (i < line.length) {
245
+ if (line[i] === '-' && line[i + 1] === '-') break;
246
+ if (line[i] === '"' || line[i] === "'") {
247
+ const q = line[i++];
248
+ while (i < line.length && line[i] !== q) { if (line[i] === '\\') i++; i++; }
249
+ i++; continue;
250
+ }
251
+ r += line[i++];
252
+ }
253
+ return r;
254
+ }
255
+
256
+ function blankStringsAndComments(text) {
257
+ let r = '', i = 0;
258
+ while (i < text.length) {
259
+ if (text[i] === '-' && text[i + 1] === '-') {
260
+ if (text[i + 2] === '[' && text[i + 3] === '[') {
261
+ const end = text.indexOf(']]', i + 4), len = end < 0 ? text.length - i : end + 2 - i;
262
+ r += ' '.repeat(len); i += len;
263
+ } else {
264
+ const nl = text.indexOf('\n', i), len = nl < 0 ? text.length - i : nl - i;
265
+ r += ' '.repeat(len); i += len;
266
+ }
267
+ continue;
268
+ }
269
+ if (text[i] === '[' && text[i + 1] === '[') {
270
+ const end = text.indexOf(']]', i + 2), len = end < 0 ? text.length - i : end + 2 - i;
271
+ r += ' '.repeat(len); i += len; continue;
272
+ }
273
+ if (text[i] === '"' || text[i] === "'") {
274
+ const q = text[i++]; r += ' ';
275
+ while (i < text.length && text[i] !== q && text[i] !== '\n') {
276
+ if (text[i] === '\\') { i++; r += ' '; } r += ' '; i++;
277
+ }
278
+ r += ' '; i++; continue;
279
+ }
280
+ r += text[i++];
281
+ }
282
+ return r;
283
+ }
284
+
285
+ // ─── Autocomplete ─────────────────────────────────────────────────────────────
286
+
287
+ const LUA_KEYWORDS = [
288
+ 'local', 'if', 'then', 'else', 'elseif', 'end', 'for', 'while', 'do',
289
+ 'repeat', 'until', 'return', 'break', 'in', 'function', 'not', 'and', 'or',
290
+ 'true', 'false', 'nil',
291
+ ];
292
+
293
+ const DSL_SNIPPETS = [
294
+ { label: 'component', detail: 'Pulse component skeleton', kind: vscode.CompletionItemKind.Snippet,
295
+ insert: new vscode.SnippetString(
296
+ `component {
297
+ signal \${1:name} = \${2:false}
298
+
299
+ init {
300
+ \$0
301
+ }
302
+
303
+ on \${1:name} {
304
+ func.\${3:Handler}(v)
305
+ }
306
+
307
+ ui {
308
+ toggle "\${4:Label}" -> \${1:name}
309
+ }
310
+ }`) },
311
+ { label: 'page', detail: 'Page DSL skeleton', kind: vscode.CompletionItemKind.Snippet,
312
+ insert: new vscode.SnippetString(
313
+ `page "\${1:Name}" {
314
+ groupbox \${2|left,right|} "\${3:Title}" {
315
+ \$0
316
+ }
317
+ }`) },
318
+ { label: 'signal', detail: 'signal name = default', kind: vscode.CompletionItemKind.Keyword,
319
+ insert: new vscode.SnippetString('signal \${1:name} = \${2:false}') },
320
+ { label: 'init', detail: 'Initialisation block', kind: vscode.CompletionItemKind.Snippet,
321
+ insert: new vscode.SnippetString('init {\n $0\n}') },
322
+ { label: 'on', detail: 'Signal or event handler', kind: vscode.CompletionItemKind.Snippet,
323
+ insert: new vscode.SnippetString('on \${1:signal} {\n $0\n}') },
324
+ { label: 'func', detail: 'Component method', kind: vscode.CompletionItemKind.Snippet,
325
+ insert: new vscode.SnippetString('func \${1:Name}() {\n $0\n}') },
326
+ { label: 'ui', detail: 'UI widget block', kind: vscode.CompletionItemKind.Snippet,
327
+ insert: new vscode.SnippetString('ui {\n $0\n}') },
328
+ { label: 'groupbox', detail: 'groupbox left|right "Title" { }', kind: vscode.CompletionItemKind.Snippet,
329
+ insert: new vscode.SnippetString('groupbox \${1|left,right|} "\${2:Title}" {\n $0\n}') },
330
+ { label: 'toggle', detail: 'toggle "Label" -> signal', kind: vscode.CompletionItemKind.Snippet,
331
+ insert: new vscode.SnippetString('toggle "\${1:Label}" -> \${2:Component}.\${3:signal}') },
332
+ { label: 'slider', detail: 'slider "Label" -> signal [min, max]', kind: vscode.CompletionItemKind.Snippet,
333
+ insert: new vscode.SnippetString('slider "\${1:Label}" -> \${2:Component}.\${3:signal} [\${4:0}, \${5:100}]') },
334
+ { label: 'dropdown', detail: 'dropdown "Label" -> signal ["A","B"]', kind: vscode.CompletionItemKind.Snippet,
335
+ insert: new vscode.SnippetString('dropdown "\${1:Label}" -> \${2:Component}.\${3:signal} ["\${4:A}", "\${5:B}"]') },
336
+ { label: 'button', detail: 'button "Label" -> Component:Method()', kind: vscode.CompletionItemKind.Snippet,
337
+ insert: new vscode.SnippetString('button "\${1:Label}" -> \${2:Component}:\${3:Method}()') },
338
+ { label: 'keybind', detail: 'keybind "Label" key="K" -> Component:Method()', kind: vscode.CompletionItemKind.Snippet,
339
+ insert: new vscode.SnippetString('keybind "\${1:Label}" key="\${2:K}" -> \${3:Component}:\${4:Method}()') },
340
+ { label: 'mount', detail: 'mount ComponentName', kind: vscode.CompletionItemKind.Keyword,
341
+ insert: new vscode.SnippetString('mount \${1:Component}') },
342
+ { label: 'separator', detail: 'Visual separator', kind: vscode.CompletionItemKind.Keyword,
343
+ insert: 'separator' },
344
+ { label: 'label', detail: 'label "text"', kind: vscode.CompletionItemKind.Snippet,
345
+ insert: new vscode.SnippetString('label "\${1:text}"') },
346
+ { label: 'when', detail: 'Conditional modifier (on Event when signal)', kind: vscode.CompletionItemKind.Keyword,
347
+ insert: new vscode.SnippetString('when \${1:signal}') },
348
+ ];
349
+
350
+ function completions(doc, pos) {
351
+ const prefix = doc.lineAt(pos.line).text.slice(0, pos.character).trimStart();
352
+ const items = [];
353
+
354
+ if (/^on\s+\w*$/.test(prefix)) {
355
+ for (const sig of parseSignals(doc.getText())) {
356
+ const item = new vscode.CompletionItem(sig, vscode.CompletionItemKind.Variable);
357
+ item.detail = 'signal'; item.sortText = '0' + sig; items.push(item);
358
+ }
359
+ for (const ev of KNOWN_EVENTS) {
360
+ const item = new vscode.CompletionItem(ev, vscode.CompletionItemKind.Event);
361
+ item.detail = 'event'; item.sortText = '1' + ev; items.push(item);
362
+ }
363
+ }
364
+
365
+ if (/^mount\s+\w*$/.test(prefix)) {
366
+ for (const comp of parseComponents(doc.getText())) {
367
+ const item = new vscode.CompletionItem(comp, vscode.CompletionItemKind.Class);
368
+ item.detail = 'component'; item.sortText = '0' + comp; items.push(item);
369
+ }
370
+ }
371
+
372
+ for (const s of DSL_SNIPPETS) {
373
+ const item = new vscode.CompletionItem(s.label, s.kind);
374
+ item.detail = s.detail; item.insertText = s.insert; item.sortText = '5' + s.label;
375
+ items.push(item);
376
+ }
377
+ for (const kw of LUA_KEYWORDS) {
378
+ const item = new vscode.CompletionItem(kw, vscode.CompletionItemKind.Keyword);
379
+ item.sortText = '9' + kw; items.push(item);
380
+ }
381
+ return items;
382
+ }
383
+
384
+ function parseSignals(text) {
385
+ const s = new Set();
386
+ for (const line of text.split('\n')) {
387
+ const m = line.trim().match(/^signal\s+(\w+)\s*=/);
388
+ if (m) s.add(m[1]);
389
+ }
390
+ return [...s];
391
+ }
392
+
393
+ function parseComponents(text) {
394
+ const s = new Set();
395
+ for (const m of text.matchAll(/\b([A-Z][a-zA-Z0-9_]*)\b/g)) s.add(m[1]);
396
+ return [...s];
397
+ }
@@ -0,0 +1,126 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
3
+ "name": "rblua",
4
+ "scopeName": "source.rblua",
5
+ "patterns": [
6
+ { "include": "#block-comment" },
7
+ { "include": "#line-comment" },
8
+ { "include": "#long-string" },
9
+ { "include": "#double-string" },
10
+ { "include": "#single-string" },
11
+ { "include": "#number" },
12
+ { "include": "#signal-decl" },
13
+ { "include": "#on-handler" },
14
+ { "include": "#func-decl" },
15
+ { "include": "#pulse-keywords" },
16
+ { "include": "#page-dsl" },
17
+ { "include": "#widget-types" },
18
+ { "include": "#lua-control" },
19
+ { "include": "#lua-storage" },
20
+ { "include": "#lua-constants" },
21
+ { "include": "#lua-operators" },
22
+ { "include": "#arrow" },
23
+ { "include": "#attribute-name" },
24
+ { "include": "#type-ref" }
25
+ ],
26
+ "repository": {
27
+ "block-comment": {
28
+ "name": "comment.block.rblua",
29
+ "begin": "--\\[\\[",
30
+ "end": "\\]\\]",
31
+ "beginCaptures": { "0": { "name": "punctuation.definition.comment.rblua" } },
32
+ "endCaptures": { "0": { "name": "punctuation.definition.comment.rblua" } }
33
+ },
34
+ "line-comment": {
35
+ "name": "comment.line.double-dash.rblua",
36
+ "match": "--.*$"
37
+ },
38
+ "long-string": {
39
+ "name": "string.quoted.other.rblua",
40
+ "begin": "\\[\\[",
41
+ "end": "\\]\\]"
42
+ },
43
+ "double-string": {
44
+ "name": "string.quoted.double.rblua",
45
+ "begin": "\"",
46
+ "end": "\"",
47
+ "patterns": [{ "name": "constant.character.escape.rblua", "match": "\\\\." }]
48
+ },
49
+ "single-string": {
50
+ "name": "string.quoted.single.rblua",
51
+ "begin": "'",
52
+ "end": "'",
53
+ "patterns": [{ "name": "constant.character.escape.rblua", "match": "\\\\." }]
54
+ },
55
+ "number": {
56
+ "name": "constant.numeric.rblua",
57
+ "match": "\\b(0[xX][0-9a-fA-F]+|\\d+\\.\\d+(?:[eE][+-]?\\d+)?|\\d+(?:[eE][+-]?\\d+)?)\\b"
58
+ },
59
+ "signal-decl": {
60
+ "comment": "signal name = value → 'signal' keyword + signal name",
61
+ "match": "\\b(signal)\\s+([a-z_][a-zA-Z0-9_]*)\\b",
62
+ "captures": {
63
+ "1": { "name": "storage.type.component.rblua" },
64
+ "2": { "name": "variable.other.readwrite.rblua" }
65
+ }
66
+ },
67
+ "on-handler": {
68
+ "comment": "on name [(params)] [when x] { → 'on' keyword + handler/signal name",
69
+ "match": "\\b(on)\\s+([A-Za-z_][a-zA-Z0-9_]*)\\b",
70
+ "captures": {
71
+ "1": { "name": "storage.type.component.rblua" },
72
+ "2": { "name": "entity.name.tag.rblua" }
73
+ }
74
+ },
75
+ "func-decl": {
76
+ "comment": "func MethodName([params]) { → 'func' keyword + method name",
77
+ "match": "\\b(func)\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*\\([^)]*\\)",
78
+ "captures": {
79
+ "1": { "name": "storage.type.component.rblua" },
80
+ "2": { "name": "entity.name.function.rblua" }
81
+ }
82
+ },
83
+ "pulse-keywords": {
84
+ "name": "storage.type.component.rblua",
85
+ "match": "\\b(component|signal|init|on|func|ui)\\b"
86
+ },
87
+ "page-dsl": {
88
+ "name": "keyword.control.page.rblua",
89
+ "match": "\\b(page|groupbox|mount|separator|left|right|when|every|debounce)\\b"
90
+ },
91
+ "widget-types": {
92
+ "name": "support.type.widget.rblua",
93
+ "match": "\\b(toggle|slider|dropdown|button|keybind|label)\\b"
94
+ },
95
+ "lua-control": {
96
+ "name": "keyword.control.lua",
97
+ "match": "\\b(if|then|else|elseif|end|for|while|do|repeat|until|return|break|in)\\b"
98
+ },
99
+ "lua-storage": {
100
+ "name": "storage.type.lua",
101
+ "match": "\\b(local|function)\\b"
102
+ },
103
+ "lua-constants": {
104
+ "name": "constant.language.lua",
105
+ "match": "\\b(true|false|nil)\\b"
106
+ },
107
+ "lua-operators": {
108
+ "name": "keyword.operator.lua",
109
+ "match": "\\b(not|and|or)\\b"
110
+ },
111
+ "arrow": {
112
+ "name": "keyword.operator.arrow.rblua",
113
+ "match": "->"
114
+ },
115
+ "attribute-name": {
116
+ "comment": "key= tip= min= max= in widget lines",
117
+ "name": "entity.other.attribute-name.rblua",
118
+ "match": "\\b(key|tip|min|max)(?=\\s*=)"
119
+ },
120
+ "type-ref": {
121
+ "comment": "PascalCase identifiers → component references",
122
+ "name": "entity.name.type.rblua",
123
+ "match": "\\b[A-Z][a-zA-Z0-9_]*\\b"
124
+ }
125
+ }
126
+ }