@reqlan/language 1.9.5 → 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 (52) hide show
  1. package/README.md +14 -0
  2. package/README.template.md +2 -0
  3. package/out/file-path-rewrite.d.ts +3 -0
  4. package/out/file-path-rewrite.js +1 -0
  5. package/out/file-path-rewrite.js.map +1 -1
  6. package/out/index.d.ts +2 -1
  7. package/out/index.js +2 -1
  8. package/out/index.js.map +1 -1
  9. package/out/reqlan-code-action-provider.d.ts +1 -0
  10. package/out/reqlan-code-action-provider.js +12 -21
  11. package/out/reqlan-code-action-provider.js.map +1 -1
  12. package/out/reqlan-comment-diagnostics.d.ts +11 -0
  13. package/out/reqlan-comment-diagnostics.js +81 -11
  14. package/out/reqlan-comment-diagnostics.js.map +1 -1
  15. package/out/reqlan-comment-rename.d.ts +1 -0
  16. package/out/reqlan-comment-rename.js.map +1 -1
  17. package/out/reqlan-document-builder.d.ts +9 -0
  18. package/out/reqlan-document-builder.js +26 -0
  19. package/out/reqlan-document-builder.js.map +1 -0
  20. package/out/reqlan-document-link-provider.d.ts +3 -1
  21. package/out/reqlan-document-link-provider.js +6 -1
  22. package/out/reqlan-document-link-provider.js.map +1 -1
  23. package/out/reqlan-idea-move-imports.d.ts +24 -0
  24. package/out/reqlan-idea-move-imports.js +309 -0
  25. package/out/reqlan-idea-move-imports.js.map +1 -0
  26. package/out/reqlan-idea-refactor.d.ts +15 -4
  27. package/out/reqlan-idea-refactor.js +106 -14
  28. package/out/reqlan-idea-refactor.js.map +1 -1
  29. package/out/reqlan-import-edits.d.ts +1 -0
  30. package/out/reqlan-import-edits.js +8 -0
  31. package/out/reqlan-import-edits.js.map +1 -1
  32. package/out/reqlan-module.js +3 -1
  33. package/out/reqlan-module.js.map +1 -1
  34. package/out/reqlan-path-references.js +8 -2
  35. package/out/reqlan-path-references.js.map +1 -1
  36. package/out/reqlan-validator.d.ts +5 -0
  37. package/out/reqlan-validator.js +17 -0
  38. package/out/reqlan-validator.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/file-path-rewrite.ts +3 -0
  41. package/src/index.ts +4 -0
  42. package/src/reqlan-code-action-provider.ts +12 -24
  43. package/src/reqlan-comment-diagnostics.ts +108 -11
  44. package/src/reqlan-comment-rename.ts +1 -0
  45. package/src/reqlan-document-builder.ts +35 -0
  46. package/src/reqlan-document-link-provider.ts +18 -3
  47. package/src/reqlan-idea-move-imports.ts +398 -0
  48. package/src/reqlan-idea-refactor.ts +152 -15
  49. package/src/reqlan-import-edits.ts +9 -0
  50. package/src/reqlan-module.ts +3 -1
  51. package/src/reqlan-path-references.ts +9 -2
  52. 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
+ }
@@ -3,9 +3,9 @@
3
3
  * rq:["../../../reqlan rq/extension/refactor_support.rq".refactor_symbol_move]
4
4
  * rq:["../../../reqlan rq/extension/refactor_support.rq".refactor_symbol_delete]
5
5
  * rq:["../../../reqlan rq/extension/refactor_support.rq".refactor_changes]
6
+ * rq:["../../../reqlan rq/extension/refactor_support.rq".comment_reference_refactor_support]
6
7
  */
7
- import type { AstNode, LangiumDocument, ReferenceDescription } from 'langium';
8
- import { AstUtils } from 'langium';
8
+ import { AstUtils, URI, type AstNode, type LangiumDocument, type ReferenceDescription } from 'langium';
9
9
  import type { Range, TextEdit } from 'vscode-languageserver';
10
10
  import {
11
11
  isIdea,
@@ -15,11 +15,24 @@ import {
15
15
  type Model,
16
16
  type OneLinerIdea
17
17
  } from './generated/ast.js';
18
+ import { buildInboundPathRewriteEdits } from './file-path-rewrite.js';
18
19
  import {
19
20
  buildFromImportEdit,
21
+ buildNamespaceImportEdit,
22
+ fileBasenameAlias,
20
23
  findImportInsertPosition,
24
+ namespaceAliasForPath,
21
25
  relativeRqImportPath
22
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';
35
+ import { findCommentPathReferencesInText } from './reqlan-path-references.js';
23
36
 
24
37
  export type RefactorIdeaDeclaration = Idea | OneLinerIdea;
25
38
 
@@ -76,12 +89,21 @@ export function planIdeaDeleteEdits(
76
89
  return toDocumentEdits(byUri);
77
90
  }
78
91
 
79
- export function planIdeaMoveEdits(input: {
92
+ export interface PlanIdeaMoveInput {
80
93
  idea: RefactorIdeaDeclaration;
81
94
  sourceDocument: LangiumDocument;
82
95
  destinationDocument: LangiumDocument;
83
96
  references: readonly ReferenceDescription[];
84
- }): DocumentTextEdits[] {
97
+ /** Extra file texts (code or `.rq`) that may hold qualified comment idea links. */
98
+ documentsText?: Map<string, string>;
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[] {
85
107
  const ideaText = ideaDeclarationText(input.sourceDocument, input.idea);
86
108
  if (!ideaText) {
87
109
  return [];
@@ -91,30 +113,145 @@ export function planIdeaMoveEdits(input: {
91
113
  const sourceUri = input.sourceDocument.uri.toString();
92
114
  const destUri = input.destinationDocument.uri.toString();
93
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
+
94
129
  if (declarationRange) {
95
- 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 });
96
134
  }
97
135
 
98
- const destModel = input.destinationDocument.parseResult.value;
99
- if (isModel(destModel)) {
100
- const insert = findIdeaInsertPosition(destModel);
101
- pushEdit(byUri, destUri, {
102
- range: { start: insert, end: insert },
103
- newText: `${ideaText}\n`
104
- });
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);
105
173
  }
106
174
 
107
- if (sourceKeepsReferences(input.references, sourceUri, declarationRange)) {
108
- const importPath = relativeRqImportPath(input.sourceDocument.uri, input.destinationDocument.uri);
109
- 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);
110
184
  if (importEdit) {
111
185
  pushEdit(byUri, sourceUri, importEdit);
112
186
  }
113
187
  }
114
188
 
189
+ if (input.documentsText) {
190
+ for (const [uri, text] of input.documentsText) {
191
+ for (const edit of planCommentPathRewritesForMovedIdea(
192
+ text,
193
+ uri,
194
+ input.sourceDocument.uri,
195
+ input.destinationDocument.uri,
196
+ input.idea.name
197
+ )) {
198
+ pushEdit(byUri, uri, edit);
199
+ }
200
+ }
201
+ }
202
+
115
203
  return toDocumentEdits(byUri);
116
204
  }
117
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
+
238
+ function planCommentPathRewritesForMovedIdea(
239
+ text: string,
240
+ referencingUri: string,
241
+ oldTargetUri: URI,
242
+ newTargetUri: URI,
243
+ ideaName: string
244
+ ): TextEdit[] {
245
+ const refs = findCommentPathReferencesInText(text).filter(reference => reference.idea === ideaName);
246
+ return buildInboundPathRewriteEdits(
247
+ refs,
248
+ URI.parse(referencingUri),
249
+ oldTargetUri,
250
+ newTargetUri,
251
+ (_path, newPath) => JSON.stringify(newPath)
252
+ );
253
+ }
254
+
118
255
  function sourceKeepsReferences(
119
256
  references: readonly ReferenceDescription[],
120
257
  sourceUri: 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
 
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Collects relative file path strings in .rq imports and embedded references, and rq: paths in comments.
3
3
  * rq:["../../../reqlan rq/extension/refactor_support.rq".refactor_file_moves]
4
+ * rq:["../../../reqlan rq/extension/refactor_support.rq".comment_reference_refactor_support]
4
5
  * rq:["../../../reqlan rq/extension/features-mutation-hooks.rq".move_file]
5
6
  */
6
7
  import type { Range } from 'vscode-languageserver';
@@ -84,6 +85,7 @@ export function findCommentPathReferencesInText(text: string, lineOffset = 0): P
84
85
  const startPos = offsetToPosition(text, absoluteStart);
85
86
  references.push({
86
87
  path: commentRef.path,
88
+ idea: commentRef.idea,
87
89
  range: {
88
90
  start: startPos,
89
91
  end: { line: startPos.line, character: startPos.character + quoted.length }
@@ -94,9 +96,14 @@ export function findCommentPathReferencesInText(text: string, lineOffset = 0): P
94
96
  }
95
97
 
96
98
  export function findPathReferencesInMovedFile(text: string, isRqFile: boolean): PathReference[] {
99
+ const commentPaths = findCommentPathReferencesInText(text);
97
100
  const references = isRqFile
98
- ? [...findImportPathReferencesInText(text), ...findEmbeddedPathReferencesInText(text)]
99
- : findCommentPathReferencesInText(text);
101
+ ? [
102
+ ...findImportPathReferencesInText(text),
103
+ ...findEmbeddedPathReferencesInText(text),
104
+ ...commentPaths
105
+ ]
106
+ : commentPaths;
100
107
  return dedupePathReferences(references);
101
108
  }
102
109