@reqlan/language 1.9.6 → 1.9.7

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 (42) hide show
  1. package/README.md +8 -0
  2. package/README.template.md +2 -0
  3. package/out/index.d.ts +2 -1
  4. package/out/index.js +2 -1
  5. package/out/index.js.map +1 -1
  6. package/out/reqlan-code-action-provider.d.ts +1 -0
  7. package/out/reqlan-code-action-provider.js +12 -21
  8. package/out/reqlan-code-action-provider.js.map +1 -1
  9. package/out/reqlan-comment-diagnostics.d.ts +11 -0
  10. package/out/reqlan-comment-diagnostics.js +81 -11
  11. package/out/reqlan-comment-diagnostics.js.map +1 -1
  12. package/out/reqlan-document-builder.d.ts +9 -0
  13. package/out/reqlan-document-builder.js +26 -0
  14. package/out/reqlan-document-builder.js.map +1 -0
  15. package/out/reqlan-document-link-provider.d.ts +3 -1
  16. package/out/reqlan-document-link-provider.js +6 -1
  17. package/out/reqlan-document-link-provider.js.map +1 -1
  18. package/out/reqlan-idea-move-imports.d.ts +24 -0
  19. package/out/reqlan-idea-move-imports.js +309 -0
  20. package/out/reqlan-idea-move-imports.js.map +1 -0
  21. package/out/reqlan-idea-refactor.d.ts +12 -4
  22. package/out/reqlan-idea-refactor.js +85 -13
  23. package/out/reqlan-idea-refactor.js.map +1 -1
  24. package/out/reqlan-import-edits.d.ts +1 -0
  25. package/out/reqlan-import-edits.js +8 -0
  26. package/out/reqlan-import-edits.js.map +1 -1
  27. package/out/reqlan-module.js +3 -1
  28. package/out/reqlan-module.js.map +1 -1
  29. package/out/reqlan-validator.d.ts +5 -0
  30. package/out/reqlan-validator.js +17 -0
  31. package/out/reqlan-validator.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/index.ts +4 -0
  34. package/src/reqlan-code-action-provider.ts +12 -24
  35. package/src/reqlan-comment-diagnostics.ts +108 -11
  36. package/src/reqlan-document-builder.ts +35 -0
  37. package/src/reqlan-document-link-provider.ts +18 -3
  38. package/src/reqlan-idea-move-imports.ts +398 -0
  39. package/src/reqlan-idea-refactor.ts +116 -14
  40. package/src/reqlan-import-edits.ts +9 -0
  41. package/src/reqlan-module.ts +3 -1
  42. package/src/reqlan-validator.ts +23 -0
@@ -0,0 +1,398 @@
1
+ /**
2
+ * Import rewrites for moving an idea between `.rq` files: drop unused source
3
+ * imports and copy required imports onto the destination.
4
+ * rq:["../../../reqlan rq/extension/refactor_support.rq".refactor_symbol_move]
5
+ */
6
+ import { AstUtils, type AstNode, type LangiumDocument } from 'langium';
7
+ import type { Position, Range, TextEdit } from 'vscode-languageserver';
8
+ import {
9
+ isFromImport,
10
+ isIdeaSet,
11
+ isLocalReference,
12
+ isModel,
13
+ isNamespaceImport,
14
+ isQualifiedImport,
15
+ isQualifiedReference,
16
+ type FromImport,
17
+ type FromImportSpecifier,
18
+ type Idea,
19
+ type Import,
20
+ type Model,
21
+ type OneLinerIdea
22
+ } from './generated/ast.js';
23
+ import {
24
+ findExistingFromImport,
25
+ findImportInsertPosition,
26
+ hasNamespaceImport,
27
+ relativeRqImportPath
28
+ } from './reqlan-import-edits.js';
29
+ import {
30
+ importBindings,
31
+ specifierBindingName
32
+ } from './reqlan-import-bindings.js';
33
+ import { resolveDocumentPathUri } from './reqlan-path-resolve.js';
34
+ import { unquoteReqlanString } from './reqlan-quoted-strings.js';
35
+
36
+ export interface DestFromSpecifierNeed {
37
+ ideaName: string;
38
+ alias?: string;
39
+ }
40
+
41
+ export function collectUsedBindingNames(root: AstNode, skip?: AstNode): Set<string> {
42
+ const names = new Set<string>();
43
+ for (const node of AstUtils.streamAst(root)) {
44
+ if (skip && isAstInside(node, skip)) {
45
+ continue;
46
+ }
47
+ if (isLocalReference(node)) {
48
+ const name = node.idea.$refText;
49
+ if (name) {
50
+ names.add(name);
51
+ }
52
+ }
53
+ if (isQualifiedReference(node)) {
54
+ const qualifier = node.qualifier?.$refText;
55
+ if (qualifier) {
56
+ names.add(qualifier);
57
+ }
58
+ }
59
+ if (isIdeaSet(node)) {
60
+ for (const member of node.members) {
61
+ if (member.$refText) {
62
+ names.add(member.$refText);
63
+ }
64
+ }
65
+ }
66
+ }
67
+ return names;
68
+ }
69
+
70
+ export function topLevelDeclaredNames(model: Model): Set<string> {
71
+ const names = new Set<string>();
72
+ for (const element of model.elements) {
73
+ if ('name' in element && typeof element.name === 'string' && element.name.length > 0) {
74
+ names.add(element.name);
75
+ }
76
+ }
77
+ return names;
78
+ }
79
+
80
+ export function sourceBoundNames(model: Model): Set<string> {
81
+ const names = topLevelDeclaredNames(model);
82
+ for (const importDecl of model.imports) {
83
+ for (const binding of importBindings(importDecl)) {
84
+ names.add(binding.name);
85
+ }
86
+ }
87
+ return names;
88
+ }
89
+
90
+ export function planUnusedImportEdits(
91
+ document: LangiumDocument,
92
+ usedNames: Set<string>
93
+ ): TextEdit[] {
94
+ const model = document.parseResult.value;
95
+ if (!isModel(model)) {
96
+ return [];
97
+ }
98
+ const edits: TextEdit[] = [];
99
+ for (const importDecl of model.imports) {
100
+ edits.push(...planUnusedEditsForImport(document, importDecl, usedNames));
101
+ }
102
+ return edits;
103
+ }
104
+
105
+ export function planDestRequiredImportEdits(input: {
106
+ idea: Idea | OneLinerIdea;
107
+ sourceDocument: LangiumDocument;
108
+ destinationDocument: LangiumDocument;
109
+ }): TextEdit[] {
110
+ const sourceModel = input.sourceDocument.parseResult.value;
111
+ const destModel = input.destinationDocument.parseResult.value;
112
+ if (!isModel(sourceModel) || !isModel(destModel)) {
113
+ return [];
114
+ }
115
+
116
+ const usedInIdea = collectUsedBindingNames(input.idea);
117
+ usedInIdea.delete(input.idea.name);
118
+ const destLocals = topLevelDeclaredNames(destModel);
119
+ const sourceSiblings = topLevelDeclaredNames(sourceModel);
120
+ sourceSiblings.delete(input.idea.name);
121
+
122
+ const fromNeeds = new Map<string, DestFromSpecifierNeed[]>();
123
+ const nsNeeds = new Map<string, string>();
124
+
125
+ const pushFrom = (path: string, specifier: DestFromSpecifierNeed) => {
126
+ const list = fromNeeds.get(path) ?? [];
127
+ if (list.some(entry => entry.ideaName === specifier.ideaName && entry.alias === specifier.alias)) {
128
+ fromNeeds.set(path, list);
129
+ return;
130
+ }
131
+ list.push(specifier);
132
+ fromNeeds.set(path, list);
133
+ };
134
+
135
+ for (const name of usedInIdea) {
136
+ if (destLocals.has(name)) {
137
+ continue;
138
+ }
139
+ if (sourceSiblings.has(name)) {
140
+ const path = relativeRqImportPath(input.destinationDocument.uri, input.sourceDocument.uri);
141
+ pushFrom(path, { ideaName: name });
142
+ continue;
143
+ }
144
+ const binding = findImportBinding(sourceModel, name);
145
+ if (!binding) {
146
+ continue;
147
+ }
148
+ const rewritten = rewriteImportPathForDocument(
149
+ binding.path,
150
+ input.sourceDocument,
151
+ input.destinationDocument
152
+ );
153
+ if (!rewritten) {
154
+ continue;
155
+ }
156
+ if (binding.kind === 'namespace') {
157
+ nsNeeds.set(rewritten, binding.alias ?? name);
158
+ continue;
159
+ }
160
+ pushFrom(rewritten, {
161
+ ideaName: binding.ideaName ?? name,
162
+ alias: binding.alias
163
+ });
164
+ }
165
+
166
+ return buildDestImportEdits(input.destinationDocument, destModel, fromNeeds, nsNeeds);
167
+ }
168
+
169
+ export function uniqueImportAlias(base: string, taken: ReadonlySet<string>): string {
170
+ const cleaned = base.replace(/[^A-Za-z0-9_]+/g, '_').replace(/^(\d)/, '_$1') || 'imported';
171
+ if (!taken.has(cleaned)) {
172
+ return cleaned;
173
+ }
174
+ let suffix = 2;
175
+ while (taken.has(`${cleaned}_${suffix}`)) {
176
+ suffix += 1;
177
+ }
178
+ return `${cleaned}_${suffix}`;
179
+ }
180
+
181
+ export function rewriteImportPathForDocument(
182
+ quotedOrRawPath: string,
183
+ fromDocument: LangiumDocument,
184
+ toDocument: LangiumDocument
185
+ ): string | undefined {
186
+ const path = unquoteReqlanString(quotedOrRawPath);
187
+ if (path.startsWith('@/')) {
188
+ return path;
189
+ }
190
+ const target = resolveDocumentPathUri(path, fromDocument, { config: null });
191
+ if (target.toString() === toDocument.uri.toString()) {
192
+ return undefined;
193
+ }
194
+ return relativeRqImportPath(toDocument.uri, target);
195
+ }
196
+
197
+ export function positionsEqual(left: Position, right: Position): boolean {
198
+ return left.line === right.line && left.character === right.character;
199
+ }
200
+
201
+ function planUnusedEditsForImport(
202
+ document: LangiumDocument,
203
+ importDecl: Import,
204
+ usedNames: Set<string>
205
+ ): TextEdit[] {
206
+ if (isFromImport(importDecl)) {
207
+ const unused = importDecl.specifiers.filter(specifier => {
208
+ const binding = specifierBindingName(specifier);
209
+ return !binding || !usedNames.has(binding);
210
+ });
211
+ if (unused.length === 0) {
212
+ return [];
213
+ }
214
+ if (unused.length === importDecl.specifiers.length) {
215
+ const range = expandLineRange(importDecl.$cstNode?.range);
216
+ return range ? [{ range, newText: '' }] : [];
217
+ }
218
+ return unused.flatMap(specifier => {
219
+ const edit = planSpecifierRemoval(document, specifier);
220
+ return edit ? [edit] : [];
221
+ });
222
+ }
223
+ const bindings = importBindings(importDecl);
224
+ const stillUsed = bindings.some(binding => usedNames.has(binding.name));
225
+ if (stillUsed) {
226
+ return [];
227
+ }
228
+ if (isNamespaceImport(importDecl) || isQualifiedImport(importDecl)) {
229
+ const range = expandLineRange(importDecl.$cstNode?.range);
230
+ return range ? [{ range, newText: '' }] : [];
231
+ }
232
+ return [];
233
+ }
234
+
235
+ function planSpecifierRemoval(
236
+ document: LangiumDocument,
237
+ specifier: FromImportSpecifier
238
+ ): TextEdit | undefined {
239
+ const node = specifier.$cstNode;
240
+ if (!node) {
241
+ return undefined;
242
+ }
243
+ const text = document.textDocument.getText();
244
+ const start = document.textDocument.offsetAt(node.range.start);
245
+ const end = document.textDocument.offsetAt(node.range.end);
246
+ let rangeStart = start;
247
+ let rangeEnd = end;
248
+ const commaAfter = text.slice(end).match(/^,\s*/);
249
+ if (commaAfter) {
250
+ rangeEnd = end + commaAfter[0].length;
251
+ } else {
252
+ const commaBefore = text.slice(0, start).match(/,\s*$/);
253
+ if (commaBefore) {
254
+ rangeStart = start - commaBefore[0].length;
255
+ }
256
+ }
257
+ return {
258
+ range: {
259
+ start: document.textDocument.positionAt(rangeStart),
260
+ end: document.textDocument.positionAt(rangeEnd)
261
+ },
262
+ newText: ''
263
+ };
264
+ }
265
+
266
+ function buildDestImportEdits(
267
+ destDocument: LangiumDocument,
268
+ destModel: Model,
269
+ fromNeeds: Map<string, DestFromSpecifierNeed[]>,
270
+ nsNeeds: Map<string, string>
271
+ ): TextEdit[] {
272
+ const edits: TextEdit[] = [];
273
+ const newLines: string[] = [];
274
+
275
+ for (const [path, alias] of nsNeeds) {
276
+ if (hasNamespaceImport(destModel, path)) {
277
+ continue;
278
+ }
279
+ newLines.push(`import "${path}" as ${alias}`);
280
+ }
281
+
282
+ for (const [path, specifiers] of fromNeeds) {
283
+ const existing = findExistingFromImport(destModel, path);
284
+ const toAdd = specifiers.filter(specifier => !fromImportHasSpecifier(existing, specifier));
285
+ if (toAdd.length === 0) {
286
+ continue;
287
+ }
288
+ if (existing && existing.specifiers.length > 0) {
289
+ const last = existing.specifiers[existing.specifiers.length - 1];
290
+ const insertAt = last?.$cstNode?.range.end;
291
+ if (insertAt) {
292
+ edits.push({
293
+ range: { start: insertAt, end: insertAt },
294
+ newText: `, ${toAdd.map(formatSpecifier).join(', ')}`
295
+ });
296
+ continue;
297
+ }
298
+ }
299
+ newLines.push(`from "${path}" import ${toAdd.map(formatSpecifier).join(', ')}`);
300
+ }
301
+
302
+ if (newLines.length > 0) {
303
+ const insert = findImportInsertPosition(destModel);
304
+ const block = `${newLines.join('\n')}\n`;
305
+ edits.push({
306
+ range: { start: insert.position, end: insert.position },
307
+ newText: insert.trailingNewline ? `${block}\n` : block
308
+ });
309
+ }
310
+ return edits;
311
+ }
312
+
313
+ function fromImportHasSpecifier(
314
+ existing: FromImport | undefined,
315
+ specifier: DestFromSpecifierNeed
316
+ ): boolean {
317
+ if (!existing) {
318
+ return false;
319
+ }
320
+ return existing.specifiers.some(entry => {
321
+ const binding = specifierBindingName(entry);
322
+ if (specifier.alias) {
323
+ return entry.alias === specifier.alias;
324
+ }
325
+ return binding === specifier.ideaName || entry.idea.$refText === specifier.ideaName;
326
+ });
327
+ }
328
+
329
+ function formatSpecifier(specifier: DestFromSpecifierNeed): string {
330
+ return specifier.alias
331
+ ? `${specifier.ideaName} as ${specifier.alias}`
332
+ : specifier.ideaName;
333
+ }
334
+
335
+ interface ImportBindingMatch {
336
+ kind: 'from' | 'namespace' | 'qualified';
337
+ path: string;
338
+ ideaName?: string;
339
+ alias?: string;
340
+ }
341
+
342
+ function findImportBinding(model: Model, name: string): ImportBindingMatch | undefined {
343
+ for (const importDecl of model.imports) {
344
+ if (isFromImport(importDecl)) {
345
+ const specifier = importDecl.specifiers.find(
346
+ entry => specifierBindingName(entry) === name
347
+ );
348
+ if (specifier) {
349
+ return {
350
+ kind: 'from',
351
+ path: importDecl.path,
352
+ ideaName: specifier.idea.$refText || name,
353
+ alias: specifier.alias
354
+ };
355
+ }
356
+ }
357
+ if (isNamespaceImport(importDecl) && importDecl.alias === name) {
358
+ return {
359
+ kind: 'namespace',
360
+ path: importDecl.path,
361
+ alias: importDecl.alias
362
+ };
363
+ }
364
+ if (isQualifiedImport(importDecl)) {
365
+ const binding = importDecl.alias ?? importDecl.idea.$refText;
366
+ if (binding === name) {
367
+ return {
368
+ kind: 'qualified',
369
+ path: importDecl.path,
370
+ ideaName: importDecl.idea.$refText || name,
371
+ alias: importDecl.alias
372
+ };
373
+ }
374
+ }
375
+ }
376
+ return undefined;
377
+ }
378
+
379
+ function expandLineRange(range: Range | undefined): Range | undefined {
380
+ if (!range) {
381
+ return undefined;
382
+ }
383
+ return {
384
+ start: { line: range.start.line, character: 0 },
385
+ end: { line: range.end.line + 1, character: 0 }
386
+ };
387
+ }
388
+
389
+ function isAstInside(node: AstNode, root: AstNode): boolean {
390
+ let current: AstNode | undefined = node;
391
+ while (current) {
392
+ if (current === root) {
393
+ return true;
394
+ }
395
+ current = current.$container;
396
+ }
397
+ return false;
398
+ }
@@ -18,9 +18,20 @@ import {
18
18
  import { buildInboundPathRewriteEdits } from './file-path-rewrite.js';
19
19
  import {
20
20
  buildFromImportEdit,
21
+ buildNamespaceImportEdit,
22
+ fileBasenameAlias,
21
23
  findImportInsertPosition,
24
+ namespaceAliasForPath,
22
25
  relativeRqImportPath
23
26
  } from './reqlan-import-edits.js';
27
+ import {
28
+ collectUsedBindingNames,
29
+ planDestRequiredImportEdits,
30
+ planUnusedImportEdits,
31
+ positionsEqual,
32
+ sourceBoundNames,
33
+ uniqueImportAlias
34
+ } from './reqlan-idea-move-imports.js';
24
35
  import { findCommentPathReferencesInText } from './reqlan-path-references.js';
25
36
 
26
37
  export type RefactorIdeaDeclaration = Idea | OneLinerIdea;
@@ -78,14 +89,21 @@ export function planIdeaDeleteEdits(
78
89
  return toDocumentEdits(byUri);
79
90
  }
80
91
 
81
- export function planIdeaMoveEdits(input: {
92
+ export interface PlanIdeaMoveInput {
82
93
  idea: RefactorIdeaDeclaration;
83
94
  sourceDocument: LangiumDocument;
84
95
  destinationDocument: LangiumDocument;
85
96
  references: readonly ReferenceDescription[];
86
- /** Extra file texts (code or `.rq`) that may hold `rq:["path".idea]` links. */
97
+ /** Extra file texts (code or `.rq`) that may hold qualified comment idea links. */
87
98
  documentsText?: Map<string, string>;
88
- }): DocumentTextEdits[] {
99
+ /**
100
+ * Leave a one-liner stub in the source that refers to the moved idea, and always
101
+ * import the destination file. Used by "Move idea content".
102
+ */
103
+ leaveSourceStub?: boolean;
104
+ }
105
+
106
+ export function planIdeaMoveEdits(input: PlanIdeaMoveInput): DocumentTextEdits[] {
89
107
  const ideaText = ideaDeclarationText(input.sourceDocument, input.idea);
90
108
  if (!ideaText) {
91
109
  return [];
@@ -95,22 +113,74 @@ export function planIdeaMoveEdits(input: {
95
113
  const sourceUri = input.sourceDocument.uri.toString();
96
114
  const destUri = input.destinationDocument.uri.toString();
97
115
  const declarationRange = expandDeclarationRange(input.idea);
116
+ const sourceModel = input.sourceDocument.parseResult.value;
117
+ const destModel = input.destinationDocument.parseResult.value;
118
+ const destImportPath = relativeRqImportPath(input.sourceDocument.uri, input.destinationDocument.uri);
119
+ const existingDestAlias = isModel(sourceModel)
120
+ ? namespaceAliasForPath(sourceModel, destImportPath)
121
+ : undefined;
122
+ const stubAlias = input.leaveSourceStub && isModel(sourceModel)
123
+ ? (existingDestAlias ?? uniqueImportAlias(
124
+ fileBasenameAlias(input.destinationDocument.uri),
125
+ sourceBoundNames(sourceModel)
126
+ ))
127
+ : undefined;
128
+
98
129
  if (declarationRange) {
99
- pushEdit(byUri, sourceUri, { range: declarationRange, newText: '' });
130
+ const stubText = stubAlias
131
+ ? `${input.idea.name} [${stubAlias}.${input.idea.name}]\n`
132
+ : '';
133
+ pushEdit(byUri, sourceUri, { range: declarationRange, newText: stubText });
100
134
  }
101
135
 
102
- const destModel = input.destinationDocument.parseResult.value;
103
- if (isModel(destModel)) {
104
- const insert = findIdeaInsertPosition(destModel);
105
- pushEdit(byUri, destUri, {
106
- range: { start: insert, end: insert },
107
- newText: `${ideaText}\n`
108
- });
136
+ if (isModel(sourceModel)) {
137
+ const usedAfterMove = collectUsedBindingNames(sourceModel, input.idea);
138
+ if (stubAlias) {
139
+ usedAfterMove.add(stubAlias);
140
+ }
141
+ for (const edit of planUnusedImportEdits(input.sourceDocument, usedAfterMove)) {
142
+ pushEdit(byUri, sourceUri, edit);
143
+ }
144
+ }
145
+
146
+ const destImportEdits = planDestRequiredImportEdits({
147
+ idea: input.idea,
148
+ sourceDocument: input.sourceDocument,
149
+ destinationDocument: input.destinationDocument
150
+ });
151
+ const ideaInsert = isModel(destModel) ? findIdeaInsertPosition(destModel) : undefined;
152
+ let insertedIdea = false;
153
+ if (ideaInsert) {
154
+ for (const edit of destImportEdits) {
155
+ if (
156
+ positionsEqual(edit.range.start, ideaInsert)
157
+ && positionsEqual(edit.range.end, ideaInsert)
158
+ ) {
159
+ edit.newText = `${edit.newText}${ideaText}\n`;
160
+ insertedIdea = true;
161
+ break;
162
+ }
163
+ }
164
+ if (!insertedIdea) {
165
+ pushEdit(byUri, destUri, {
166
+ range: { start: ideaInsert, end: ideaInsert },
167
+ newText: `${ideaText}\n`
168
+ });
169
+ }
170
+ }
171
+ for (const edit of destImportEdits) {
172
+ pushEdit(byUri, destUri, edit);
109
173
  }
110
174
 
111
- if (sourceKeepsReferences(input.references, sourceUri, declarationRange)) {
112
- const importPath = relativeRqImportPath(input.sourceDocument.uri, input.destinationDocument.uri);
113
- const importEdit = buildFromImportEdit(input.sourceDocument, importPath, input.idea.name);
175
+ if (stubAlias) {
176
+ if (!existingDestAlias) {
177
+ const nsEdit = buildNamespaceImportEdit(input.sourceDocument, destImportPath, stubAlias);
178
+ if (nsEdit) {
179
+ pushEdit(byUri, sourceUri, nsEdit);
180
+ }
181
+ }
182
+ } else if (sourceKeepsReferences(input.references, sourceUri, declarationRange)) {
183
+ const importEdit = buildFromImportEdit(input.sourceDocument, destImportPath, input.idea.name);
114
184
  if (importEdit) {
115
185
  pushEdit(byUri, sourceUri, importEdit);
116
186
  }
@@ -133,6 +203,38 @@ export function planIdeaMoveEdits(input: {
133
203
  return toDocumentEdits(byUri);
134
204
  }
135
205
 
206
+ export function findIdeaDeclarationAtRange(
207
+ document: LangiumDocument,
208
+ range: Range
209
+ ): RefactorIdeaDeclaration | undefined {
210
+ const offset = document.textDocument.offsetAt(range.start);
211
+ for (const node of AstUtils.streamAst(document.parseResult.value)) {
212
+ if (!isRefactorIdeaDeclaration(node) || !node.$cstNode) {
213
+ continue;
214
+ }
215
+ const start = document.textDocument.offsetAt(node.$cstNode.range.start);
216
+ const end = document.textDocument.offsetAt(node.$cstNode.range.end);
217
+ if (offset >= start && offset <= end) {
218
+ return node;
219
+ }
220
+ if (
221
+ range.start.line === node.$cstNode.range.start.line
222
+ && range.start.character <= (node.name?.length ?? 0) + 1
223
+ ) {
224
+ return node;
225
+ }
226
+ }
227
+ return undefined;
228
+ }
229
+
230
+ export function listRefactorIdeaDeclarations(document: LangiumDocument): RefactorIdeaDeclaration[] {
231
+ const model = document.parseResult.value;
232
+ if (!isModel(model)) {
233
+ return [];
234
+ }
235
+ return model.elements.filter(isRefactorIdeaDeclaration);
236
+ }
237
+
136
238
  function planCommentPathRewritesForMovedIdea(
137
239
  text: string,
138
240
  referencingUri: string,
@@ -61,6 +61,15 @@ export function hasNamespaceImport(model: Model, importPath: string): boolean {
61
61
  );
62
62
  }
63
63
 
64
+ export function namespaceAliasForPath(model: Model, importPath: string): string | undefined {
65
+ for (const decl of model.imports) {
66
+ if (isNamespaceImport(decl) && unquoteReqlanString(decl.path) === importPath && decl.alias) {
67
+ return decl.alias;
68
+ }
69
+ }
70
+ return undefined;
71
+ }
72
+
64
73
  export function buildFromImportEdit(
65
74
  document: LangiumDocument,
66
75
  importPath: string,
@@ -21,6 +21,7 @@ import { ReqlanValidator, registerValidationChecks } from './reqlan-validator.js
21
21
  import { ReqlanWorkspaceManager } from './reqlan-workspace-manager.js';
22
22
  import { ReqlanAsyncParser } from './reqlan-async-parser.js';
23
23
  import { ReqlanLangiumDocumentFactory } from './reqlan-document-factory.js';
24
+ import { ReqlanDocumentBuilder } from './reqlan-document-builder.js';
24
25
 
25
26
  /**
26
27
  * Declaration of custom services - add your own service classes here.
@@ -45,7 +46,8 @@ export type ReqlanServices = LangiumServices & ReqlanAddedServices
45
46
  export const ReqlanSharedModule: Module<LangiumSharedServices, PartialLangiumSharedCoreServices> = {
46
47
  workspace: {
47
48
  WorkspaceManager: services => new ReqlanWorkspaceManager(services),
48
- LangiumDocumentFactory: services => new ReqlanLangiumDocumentFactory(services)
49
+ LangiumDocumentFactory: services => new ReqlanLangiumDocumentFactory(services),
50
+ DocumentBuilder: services => new ReqlanDocumentBuilder(services)
49
51
  }
50
52
  };
51
53
 
@@ -11,6 +11,7 @@ import {
11
11
  type AnonymousBlock,
12
12
  type Model
13
13
  } from './generated/ast.js';
14
+ import { collectCommentReferenceIssues } from './reqlan-comment-diagnostics.js';
14
15
  import {
15
16
  collectFileLinks,
16
17
  fileLinkTargetIssueMessage
@@ -36,6 +37,8 @@ import {
36
37
  * rq:["../../../reqlan rq/language/imports.rq".import_error_recovery]
37
38
  * rq:["../../../reqlan rq/language/imports.rq".import_tokenisation]
38
39
  * rq:["../../../reqlan rq/language/syntax.rq".no_name_idea_safe_warning]
40
+ * rq:["../../../reqlan rq/language/syntax.rq".comment_reference_resolution_error]
41
+ * rq:["../../../reqlan rq/extension/features-non-rq-code-comment/functional-code-comment-references.rq".comment_reference_resolution_error_state]
39
42
  */
40
43
  export function registerValidationChecks(services: ReqlanServices) {
41
44
  const registry = services.validation.ValidationRegistry;
@@ -53,6 +56,8 @@ export function registerValidationChecks(services: ReqlanServices) {
53
56
  * rq:["../../../reqlan rq/language/imports.rq".import_error_recovery]
54
57
  * rq:["../../../reqlan rq/language/imports.rq".import_tokenisation]
55
58
  * rq:["../../../reqlan rq/language/syntax.rq".no_name_idea_safe_warning]
59
+ * rq:["../../../reqlan rq/language/syntax.rq".comment_reference_resolution_error]
60
+ * rq:["../../../reqlan rq/extension/features-non-rq-code-comment/functional-code-comment-references.rq".comment_reference_resolution_error_state]
56
61
  */
57
62
  export class ReqlanValidator {
58
63
 
@@ -64,6 +69,7 @@ export class ReqlanValidator {
64
69
  this.checkImportSyntax(model, accept);
65
70
  this.checkImportTargets(model, accept);
66
71
  this.checkFileReferenceTargets(model, accept);
72
+ this.checkCommentReferences(model, accept);
67
73
  this.checkWildcardReferences(model, accept);
68
74
  }
69
75
 
@@ -212,6 +218,23 @@ export class ReqlanValidator {
212
218
  }
213
219
  }
214
220
 
221
+ checkCommentReferences(model: Model, accept: ValidationAcceptor): void {
222
+ const document = AstUtils.getDocument(model);
223
+ const { shared } = this.services;
224
+ for (const issue of collectCommentReferenceIssues(
225
+ document,
226
+ shared.workspace.LangiumDocuments,
227
+ shared.workspace.FileSystemProvider,
228
+ pathResolveContextFromServices(this.services)
229
+ )) {
230
+ accept('error', issue.message, {
231
+ node: model,
232
+ range: issue.range,
233
+ code: issue.code
234
+ });
235
+ }
236
+ }
237
+
215
238
  checkWildcardReferences(model: Model, accept: ValidationAcceptor): void {
216
239
  const { shared } = this.services;
217
240
  const context = pathResolveContextFromServices(this.services);