@reqlan/language 1.6.2 → 1.7.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/README.md +6 -0
- package/out/generated/ast.d.ts +3 -3
- package/out/generated/ast.js +1 -1
- package/out/generated/ast.js.map +1 -1
- package/out/generated/grammar.js +2 -3318
- package/out/generated/grammar.js.map +1 -1
- package/out/generated/module.d.ts +2 -2
- package/out/generated/module.js +2 -2
- package/out/generated/module.js.map +1 -1
- package/out/index.d.ts +3 -0
- package/out/index.js +3 -0
- package/out/index.js.map +1 -1
- package/out/reqlan-async-parser.d.ts +37 -0
- package/out/reqlan-async-parser.js +117 -0
- package/out/reqlan-async-parser.js.map +1 -0
- package/out/reqlan-completion-provider.js +1 -1
- package/out/reqlan-completion-provider.js.map +1 -1
- package/out/reqlan-document-factory.d.ts +16 -0
- package/out/reqlan-document-factory.js +50 -0
- package/out/reqlan-document-factory.js.map +1 -0
- package/out/reqlan-linker.js +2 -2
- package/out/reqlan-linker.js.map +1 -1
- package/out/reqlan-module.d.ts +2 -1
- package/out/reqlan-module.js +12 -2
- package/out/reqlan-module.js.map +1 -1
- package/out/reqlan-parse-budget.d.ts +50 -0
- package/out/reqlan-parse-budget.js +89 -0
- package/out/reqlan-parse-budget.js.map +1 -0
- package/out/reqlan-parse-worker.d.ts +1 -0
- package/out/reqlan-parse-worker.js +34 -0
- package/out/reqlan-parse-worker.js.map +1 -0
- package/out/reqlan-token-builder.js +102 -25
- package/out/reqlan-token-builder.js.map +1 -1
- package/out/reqlan-validator.d.ts +9 -1
- package/out/reqlan-validator.js +16 -2
- package/out/reqlan-validator.js.map +1 -1
- package/out/reqlan-workspace-manager.d.ts +11 -0
- package/out/reqlan-workspace-manager.js +24 -0
- package/out/reqlan-workspace-manager.js.map +1 -0
- package/package.json +1 -1
- package/src/generated/ast.ts +3 -3
- package/src/generated/grammar.ts +2 -3318
- package/src/generated/module.ts +2 -2
- package/src/index.ts +3 -0
- package/src/reqlan-async-parser.ts +178 -0
- package/src/reqlan-completion-provider.ts +1 -1
- package/src/reqlan-document-factory.ts +75 -0
- package/src/reqlan-linker.ts +4 -2
- package/src/reqlan-module.ts +15 -3
- package/src/reqlan-parse-budget.ts +155 -0
- package/src/reqlan-parse-worker.ts +35 -0
- package/src/reqlan-token-builder.ts +122 -25
- package/src/reqlan-validator.ts +22 -1
- package/src/reqlan-workspace-manager.ts +38 -0
- package/src/reqlan.langium +3 -1
|
@@ -108,7 +108,15 @@ const markdownLink = (text: string, offset: number): RegExpExecArray | null => {
|
|
|
108
108
|
return null;
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
-
const
|
|
111
|
+
const blockOpenerLine = /^[ \t]*(?:[A-Za-z_.][\w.-]*|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\s*$/;
|
|
112
|
+
|
|
113
|
+
function lineStartOffset(text: string, offset: number): number {
|
|
114
|
+
return text.lastIndexOf('\n', offset - 1) + 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function textBeforeOnLine(text: string, offset: number): string {
|
|
118
|
+
return text.slice(lineStartOffset(text, offset), offset).replace(/[ \t]+$/, '');
|
|
119
|
+
}
|
|
112
120
|
|
|
113
121
|
function isEscapedAt(text: string, offset: number): boolean {
|
|
114
122
|
let backslashes = 0;
|
|
@@ -125,9 +133,11 @@ function isStructuralOpenBraceAtDepth(text: string, offset: number, depth: numbe
|
|
|
125
133
|
if (isEscapedAt(text, offset)) {
|
|
126
134
|
return false;
|
|
127
135
|
}
|
|
128
|
-
const before = text
|
|
136
|
+
const before = textBeforeOnLine(text, offset);
|
|
129
137
|
|
|
130
|
-
|
|
138
|
+
// Top-level named blocks (`name {`) and nameless recoverable blocks (`{` alone).
|
|
139
|
+
// Leading indentation is allowed — top-level ideas are often indented in fixtures.
|
|
140
|
+
if (depth === 0 && (blockOpenerLine.test(before) || (isLineStartAt(text, offset) && before.trim().length === 0))) {
|
|
131
141
|
return true;
|
|
132
142
|
}
|
|
133
143
|
if (/@[A-Za-z_][\w-]*(?::)?\s*$/.test(before)) {
|
|
@@ -137,8 +147,7 @@ function isStructuralOpenBraceAtDepth(text: string, offset: number, depth: numbe
|
|
|
137
147
|
return true;
|
|
138
148
|
}
|
|
139
149
|
if (depth >= 1 && /[A-Za-z_][\w-]*\s*$/.test(before) && !isLineStartAt(text, offset)) {
|
|
140
|
-
|
|
141
|
-
return /^[ \t]*(?:\r?\n|$)/.test(after);
|
|
150
|
+
return restOfLineIsBlank(text, offset);
|
|
142
151
|
}
|
|
143
152
|
return false;
|
|
144
153
|
}
|
|
@@ -156,12 +165,10 @@ function isStructuralCloseBraceAt(text: string, offset: number): boolean {
|
|
|
156
165
|
if (isEscapedAt(text, offset)) {
|
|
157
166
|
return false;
|
|
158
167
|
}
|
|
159
|
-
|
|
160
|
-
const restOfLine = after.split(/\r?\n/, 1)[0] ?? '';
|
|
161
|
-
if (restOfLine.trim().length !== 0) {
|
|
168
|
+
if (!restOfLineIsBlank(text, offset)) {
|
|
162
169
|
return false;
|
|
163
170
|
}
|
|
164
|
-
const { structuralDepth, proseDepth } =
|
|
171
|
+
const { structuralDepth, proseDepth } = braceStateBefore(text, offset);
|
|
165
172
|
if (structuralDepth <= 0) {
|
|
166
173
|
return false;
|
|
167
174
|
}
|
|
@@ -176,16 +183,84 @@ interface BraceScanState {
|
|
|
176
183
|
proseDepth: number;
|
|
177
184
|
}
|
|
178
185
|
|
|
179
|
-
|
|
186
|
+
/**
|
|
187
|
+
* Sparse depth timeline: event `i` applies for offsets in
|
|
188
|
+
* [offsets[i], offsets[i + 1]). Avoids O(n) Int32Arrays on every re-lex.
|
|
189
|
+
*/
|
|
190
|
+
interface BraceScanCache {
|
|
191
|
+
text: string;
|
|
192
|
+
offsets: number[];
|
|
193
|
+
structuralDepth: number[];
|
|
194
|
+
proseDepth: number[];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let braceScanCache: BraceScanCache | undefined;
|
|
198
|
+
|
|
199
|
+
function fenceEndAfter(text: string, openOffset: number): number {
|
|
200
|
+
// Match CODE_FENCE: ```…``` including optional body; return index after closing fence.
|
|
201
|
+
if (
|
|
202
|
+
text.charCodeAt(openOffset) !== 96
|
|
203
|
+
|| text.charCodeAt(openOffset + 1) !== 96
|
|
204
|
+
|| text.charCodeAt(openOffset + 2) !== 96
|
|
205
|
+
) {
|
|
206
|
+
return openOffset;
|
|
207
|
+
}
|
|
208
|
+
const afterOpen = openOffset + 3;
|
|
209
|
+
const firstNewline = text.indexOf('\n', afterOpen);
|
|
210
|
+
if (firstNewline < 0) {
|
|
211
|
+
const sameLineClose = text.indexOf('```', afterOpen);
|
|
212
|
+
return sameLineClose < 0 ? text.length : sameLineClose + 3;
|
|
213
|
+
}
|
|
214
|
+
const close = text.indexOf('```', firstNewline + 1);
|
|
215
|
+
return close < 0 ? text.length : close + 3;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function restOfLineIsBlank(text: string, offset: number): boolean {
|
|
219
|
+
for (let index = offset + 1; index < text.length; index++) {
|
|
220
|
+
const char = text[index];
|
|
221
|
+
if (char === '\n' || char === '\r') {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
if (char !== ' ' && char !== '\t') {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function buildBraceScanCache(text: string): BraceScanCache {
|
|
180
232
|
let structuralDepth = 0;
|
|
181
233
|
const proseDepthByStructural: number[] = [0];
|
|
234
|
+
const offsets = [0];
|
|
235
|
+
const structuralDepthAt = [0];
|
|
236
|
+
const proseDepthAtOffsets = [0];
|
|
182
237
|
|
|
183
238
|
const proseDepthAt = (): number => proseDepthByStructural[structuralDepth] ?? 0;
|
|
184
239
|
const setProseDepth = (value: number): void => {
|
|
185
240
|
proseDepthByStructural[structuralDepth] = value;
|
|
186
241
|
};
|
|
242
|
+
const recordStateFrom = (offset: number): void => {
|
|
243
|
+
const last = offsets.length - 1;
|
|
244
|
+
if (offsets[last] === offset) {
|
|
245
|
+
structuralDepthAt[last] = structuralDepth;
|
|
246
|
+
proseDepthAtOffsets[last] = proseDepthAt();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
offsets.push(offset);
|
|
250
|
+
structuralDepthAt.push(structuralDepth);
|
|
251
|
+
proseDepthAtOffsets.push(proseDepthAt());
|
|
252
|
+
};
|
|
187
253
|
|
|
188
|
-
for (let index = 0; index <
|
|
254
|
+
for (let index = 0; index < text.length; index++) {
|
|
255
|
+
// Fenced snippets are opaque to the parser; braces inside must not change depth.
|
|
256
|
+
if (
|
|
257
|
+
text.charCodeAt(index) === 96
|
|
258
|
+
&& text.charCodeAt(index + 1) === 96
|
|
259
|
+
&& text.charCodeAt(index + 2) === 96
|
|
260
|
+
) {
|
|
261
|
+
index = fenceEndAfter(text, index) - 1;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
189
264
|
const char = text[index];
|
|
190
265
|
if (char === '{') {
|
|
191
266
|
if (isEscapedAt(text, index)) {
|
|
@@ -197,6 +272,7 @@ function scanBraceState(text: string, offset: number): BraceScanState {
|
|
|
197
272
|
} else {
|
|
198
273
|
setProseDepth(proseDepthAt() + 1);
|
|
199
274
|
}
|
|
275
|
+
recordStateFrom(index + 1);
|
|
200
276
|
continue;
|
|
201
277
|
}
|
|
202
278
|
if (char !== '}') {
|
|
@@ -205,23 +281,24 @@ function scanBraceState(text: string, offset: number): BraceScanState {
|
|
|
205
281
|
if (isEscapedAt(text, index)) {
|
|
206
282
|
continue;
|
|
207
283
|
}
|
|
208
|
-
const
|
|
209
|
-
const restOfLine = after.split(/\r?\n/, 1)[0] ?? '';
|
|
210
|
-
const atEndOfLine = restOfLine.trim().length === 0;
|
|
284
|
+
const atEndOfLine = restOfLineIsBlank(text, index);
|
|
211
285
|
if (!atEndOfLine) {
|
|
212
286
|
if (proseDepthAt() > 0) {
|
|
213
287
|
setProseDepth(proseDepthAt() - 1);
|
|
288
|
+
recordStateFrom(index + 1);
|
|
214
289
|
}
|
|
215
290
|
continue;
|
|
216
291
|
}
|
|
217
292
|
if (structuralDepth <= 0) {
|
|
218
293
|
if (proseDepthAt() > 0) {
|
|
219
294
|
setProseDepth(proseDepthAt() - 1);
|
|
295
|
+
recordStateFrom(index + 1);
|
|
220
296
|
}
|
|
221
297
|
continue;
|
|
222
298
|
}
|
|
223
299
|
if (isLineStartAt(text, index)) {
|
|
224
300
|
structuralDepth--;
|
|
301
|
+
recordStateFrom(index + 1);
|
|
225
302
|
continue;
|
|
226
303
|
}
|
|
227
304
|
if (proseDepthAt() > 0) {
|
|
@@ -229,20 +306,41 @@ function scanBraceState(text: string, offset: number): BraceScanState {
|
|
|
229
306
|
} else {
|
|
230
307
|
structuralDepth--;
|
|
231
308
|
}
|
|
309
|
+
recordStateFrom(index + 1);
|
|
232
310
|
}
|
|
233
311
|
|
|
312
|
+
return { text, offsets, structuralDepth: structuralDepthAt, proseDepth: proseDepthAtOffsets };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function braceStateAtOffset(cache: BraceScanCache, offset: number): BraceScanState {
|
|
316
|
+
const { offsets, structuralDepth, proseDepth } = cache;
|
|
317
|
+
let low = 0;
|
|
318
|
+
let high = offsets.length - 1;
|
|
319
|
+
while (low <= high) {
|
|
320
|
+
const mid = (low + high) >> 1;
|
|
321
|
+
if (offsets[mid]! <= offset) {
|
|
322
|
+
low = mid + 1;
|
|
323
|
+
} else {
|
|
324
|
+
high = mid - 1;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const index = Math.max(0, high);
|
|
234
328
|
return {
|
|
235
|
-
structuralDepth,
|
|
236
|
-
proseDepth:
|
|
329
|
+
structuralDepth: structuralDepth[index] ?? 0,
|
|
330
|
+
proseDepth: proseDepth[index] ?? 0
|
|
237
331
|
};
|
|
238
332
|
}
|
|
239
333
|
|
|
240
|
-
function
|
|
241
|
-
|
|
334
|
+
function braceStateBefore(text: string, offset: number): BraceScanState {
|
|
335
|
+
if (braceScanCache?.text !== text) {
|
|
336
|
+
braceScanCache = buildBraceScanCache(text);
|
|
337
|
+
}
|
|
338
|
+
const boundedOffset = Math.max(0, Math.min(offset, text.length));
|
|
339
|
+
return braceStateAtOffset(braceScanCache, boundedOffset);
|
|
242
340
|
}
|
|
243
341
|
|
|
244
342
|
function isStructuralOpenBraceAt(text: string, offset: number): boolean {
|
|
245
|
-
return isStructuralOpenBraceAtDepth(text, offset,
|
|
343
|
+
return isStructuralOpenBraceAtDepth(text, offset, braceStateBefore(text, offset).structuralDepth);
|
|
246
344
|
}
|
|
247
345
|
|
|
248
346
|
const structuralOpenBrace = (text: string, offset: number): RegExpExecArray | null => {
|
|
@@ -274,7 +372,7 @@ const proseCloseBrace = (text: string, offset: number): RegExpExecArray | null =
|
|
|
274
372
|
};
|
|
275
373
|
|
|
276
374
|
function braceDepthBefore(text: string, offset: number): number {
|
|
277
|
-
return
|
|
375
|
+
return braceStateBefore(text, offset).structuralDepth;
|
|
278
376
|
}
|
|
279
377
|
|
|
280
378
|
function isLineStartAt(text: string, offset: number): boolean {
|
|
@@ -287,15 +385,14 @@ function isLineStartAt(text: string, offset: number): boolean {
|
|
|
287
385
|
|
|
288
386
|
function isStringLiteralContext(text: string, offset: number): boolean {
|
|
289
387
|
const depth = braceDepthBefore(text, offset);
|
|
290
|
-
const trimmed = text
|
|
388
|
+
const trimmed = textBeforeOnLine(text, offset);
|
|
291
389
|
if (depth === 0 && /\b(?:from|import)\s*$/.test(trimmed)) {
|
|
292
390
|
return true;
|
|
293
391
|
}
|
|
294
392
|
if (/\[\s*$/.test(trimmed)) {
|
|
295
393
|
return true;
|
|
296
394
|
}
|
|
297
|
-
|
|
298
|
-
if (trimmed.slice(lineStart).length === 0 && depth === 0) {
|
|
395
|
+
if (trimmed.length === 0 && depth === 0) {
|
|
299
396
|
return true;
|
|
300
397
|
}
|
|
301
398
|
return false;
|
|
@@ -330,7 +427,7 @@ function topLevelImportKeyword(text: string, offset: number): RegExpExecArray |
|
|
|
330
427
|
if (isLineStartAt(text, offset)) {
|
|
331
428
|
return makeMatch(text, offset, 6);
|
|
332
429
|
}
|
|
333
|
-
const before = text
|
|
430
|
+
const before = textBeforeOnLine(text, offset);
|
|
334
431
|
if (/(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')$/.test(before)) {
|
|
335
432
|
return makeMatch(text, offset, 6);
|
|
336
433
|
}
|
|
@@ -348,7 +445,7 @@ function topLevelAsKeyword(text: string, offset: number): RegExpExecArray | null
|
|
|
348
445
|
if (braceDepthBefore(text, offset) !== 0) {
|
|
349
446
|
return null;
|
|
350
447
|
}
|
|
351
|
-
const before = text
|
|
448
|
+
const before = textBeforeOnLine(text, offset);
|
|
352
449
|
if (/(?:[_a-zA-Z][\w-]*|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')$/.test(before)) {
|
|
353
450
|
return makeMatch(text, offset, 2);
|
|
354
451
|
}
|
package/src/reqlan-validator.ts
CHANGED
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
isFromImport,
|
|
6
6
|
isIdea,
|
|
7
7
|
isInvalidFromImport,
|
|
8
|
+
isModel,
|
|
8
9
|
isOneLinerIdea,
|
|
10
|
+
type AnonymousBlock,
|
|
9
11
|
type Model
|
|
10
12
|
} from './generated/ast.js';
|
|
11
13
|
import {
|
|
@@ -27,12 +29,14 @@ import { unquoteReqlanString } from './reqlan-quoted-strings.js';
|
|
|
27
29
|
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_does_not_exist_error]
|
|
28
30
|
* rq:["../../../reqlan rq/language/imports.rq".import_error_recovery]
|
|
29
31
|
* rq:["../../../reqlan rq/language/imports.rq".import_tokenisation]
|
|
32
|
+
* rq:["../../../reqlan rq/language/syntax.rq".no_name_idea_safe_warning]
|
|
30
33
|
*/
|
|
31
34
|
export function registerValidationChecks(services: ReqlanServices) {
|
|
32
35
|
const registry = services.validation.ValidationRegistry;
|
|
33
36
|
const validator = services.validation.ReqlanValidator;
|
|
34
37
|
const checks: ValidationChecks<ReqlanAstType> = {
|
|
35
|
-
Model: validator.checkModelDuplicates
|
|
38
|
+
Model: validator.checkModelDuplicates,
|
|
39
|
+
AnonymousBlock: validator.checkNamelessIdeaBlock
|
|
36
40
|
};
|
|
37
41
|
registry.register(checks, validator);
|
|
38
42
|
}
|
|
@@ -42,6 +46,7 @@ export function registerValidationChecks(services: ReqlanServices) {
|
|
|
42
46
|
* rq:["../../../reqlan rq/extension/language-support/features-imports.rq".import_does_not_exist_error]
|
|
43
47
|
* rq:["../../../reqlan rq/language/imports.rq".import_error_recovery]
|
|
44
48
|
* rq:["../../../reqlan rq/language/imports.rq".import_tokenisation]
|
|
49
|
+
* rq:["../../../reqlan rq/language/syntax.rq".no_name_idea_safe_warning]
|
|
45
50
|
*/
|
|
46
51
|
export class ReqlanValidator {
|
|
47
52
|
|
|
@@ -55,6 +60,22 @@ export class ReqlanValidator {
|
|
|
55
60
|
this.checkFileReferenceTargets(model, accept);
|
|
56
61
|
}
|
|
57
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Top-level nameless `{ ... }` blocks parse so the rest of the file stays alive,
|
|
65
|
+
* but they need a name to be addressable ideas.
|
|
66
|
+
* List-item anonymous blocks remain valid (see lists).
|
|
67
|
+
*/
|
|
68
|
+
checkNamelessIdeaBlock(block: AnonymousBlock, accept: ValidationAcceptor): void {
|
|
69
|
+
if (!isModel(block.$container)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
accept(
|
|
73
|
+
'warning',
|
|
74
|
+
'Nameless idea block: add a name before `{` (for example `my_idea { ... }`). The block was kept so the rest of the file still parses.',
|
|
75
|
+
{ node: block }
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
58
79
|
/**
|
|
59
80
|
* Local diagnostics for recoverable but invalid import shapes.
|
|
60
81
|
* Keeps the rest of the file parseable — see import_error_recovery.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DefaultWorkspaceManager,
|
|
3
|
+
type LangiumDocument,
|
|
4
|
+
type LangiumSharedCoreServices,
|
|
5
|
+
type Stream,
|
|
6
|
+
type URI
|
|
7
|
+
} from 'langium';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Loads workspace documents independently so one catastrophic parse failure
|
|
11
|
+
* (for example Chevrotain recovery stack overflow) cannot abort LSP workspace init.
|
|
12
|
+
* rq:["../../../reqlan rq/language/syntax.rq".no_name_idea_safe_warning]
|
|
13
|
+
* rq:["../../../reqlan rq/language/parser_lexer.rq".parse_budget_timeout]
|
|
14
|
+
*/
|
|
15
|
+
export class ReqlanWorkspaceManager extends DefaultWorkspaceManager {
|
|
16
|
+
constructor(services: LangiumSharedCoreServices) {
|
|
17
|
+
super(services);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
protected override async loadWorkspaceDocuments(
|
|
21
|
+
uris: Stream<URI>,
|
|
22
|
+
collector: (document: LangiumDocument) => void
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
await Promise.all(
|
|
25
|
+
uris.map(async uri => {
|
|
26
|
+
try {
|
|
27
|
+
const document = await this.langiumDocuments.getOrCreateDocument(uri);
|
|
28
|
+
collector(document);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error(
|
|
31
|
+
`[reqlan] Skipping document that failed to load during workspace init: ${uri.toString()}`,
|
|
32
|
+
error
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/reqlan.langium
CHANGED
|
@@ -17,8 +17,10 @@ entry Model:
|
|
|
17
17
|
(NL* elements+=TopLevelElement)*
|
|
18
18
|
NL*;
|
|
19
19
|
|
|
20
|
+
// Nameless `{ ... }` at top level is recoverable (no_name_idea_safe_warning):
|
|
21
|
+
// parse it, warn, and keep later ideas/imports intact — do not abort the file.
|
|
20
22
|
TopLevelElement:
|
|
21
|
-
Idea | IdeaSet | OneLinerIdea;
|
|
23
|
+
Idea | IdeaSet | OneLinerIdea | AnonymousBlock;
|
|
22
24
|
|
|
23
25
|
// ---------------------------------------------------------------------------
|
|
24
26
|
// idea_name, one_liner_idea, block_idea, ideaset
|