@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.
- package/README.md +14 -0
- package/README.template.md +2 -0
- package/out/file-path-rewrite.d.ts +3 -0
- package/out/file-path-rewrite.js +1 -0
- package/out/file-path-rewrite.js.map +1 -1
- package/out/index.d.ts +2 -1
- package/out/index.js +2 -1
- package/out/index.js.map +1 -1
- package/out/reqlan-code-action-provider.d.ts +1 -0
- package/out/reqlan-code-action-provider.js +12 -21
- package/out/reqlan-code-action-provider.js.map +1 -1
- package/out/reqlan-comment-diagnostics.d.ts +11 -0
- package/out/reqlan-comment-diagnostics.js +81 -11
- package/out/reqlan-comment-diagnostics.js.map +1 -1
- package/out/reqlan-comment-rename.d.ts +1 -0
- package/out/reqlan-comment-rename.js.map +1 -1
- package/out/reqlan-document-builder.d.ts +9 -0
- package/out/reqlan-document-builder.js +26 -0
- package/out/reqlan-document-builder.js.map +1 -0
- package/out/reqlan-document-link-provider.d.ts +3 -1
- package/out/reqlan-document-link-provider.js +6 -1
- package/out/reqlan-document-link-provider.js.map +1 -1
- package/out/reqlan-idea-move-imports.d.ts +24 -0
- package/out/reqlan-idea-move-imports.js +309 -0
- package/out/reqlan-idea-move-imports.js.map +1 -0
- package/out/reqlan-idea-refactor.d.ts +15 -4
- package/out/reqlan-idea-refactor.js +106 -14
- package/out/reqlan-idea-refactor.js.map +1 -1
- package/out/reqlan-import-edits.d.ts +1 -0
- package/out/reqlan-import-edits.js +8 -0
- package/out/reqlan-import-edits.js.map +1 -1
- package/out/reqlan-module.js +3 -1
- package/out/reqlan-module.js.map +1 -1
- package/out/reqlan-path-references.js +8 -2
- package/out/reqlan-path-references.js.map +1 -1
- package/out/reqlan-validator.d.ts +5 -0
- package/out/reqlan-validator.js +17 -0
- package/out/reqlan-validator.js.map +1 -1
- package/package.json +1 -1
- package/src/file-path-rewrite.ts +3 -0
- package/src/index.ts +4 -0
- package/src/reqlan-code-action-provider.ts +12 -24
- package/src/reqlan-comment-diagnostics.ts +108 -11
- package/src/reqlan-comment-rename.ts +1 -0
- package/src/reqlan-document-builder.ts +35 -0
- package/src/reqlan-document-link-provider.ts +18 -3
- package/src/reqlan-idea-move-imports.ts +398 -0
- package/src/reqlan-idea-refactor.ts +152 -15
- package/src/reqlan-import-edits.ts +9 -0
- package/src/reqlan-module.ts +3 -1
- package/src/reqlan-path-references.ts +9 -2
- 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
|
|
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
|
|
92
|
+
export interface PlanIdeaMoveInput {
|
|
80
93
|
idea: RefactorIdeaDeclaration;
|
|
81
94
|
sourceDocument: LangiumDocument;
|
|
82
95
|
destinationDocument: LangiumDocument;
|
|
83
96
|
references: readonly ReferenceDescription[];
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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 (
|
|
108
|
-
|
|
109
|
-
|
|
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,
|
package/src/reqlan-module.ts
CHANGED
|
@@ -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
|
-
? [
|
|
99
|
-
|
|
101
|
+
? [
|
|
102
|
+
...findImportPathReferencesInText(text),
|
|
103
|
+
...findEmbeddedPathReferencesInText(text),
|
|
104
|
+
...commentPaths
|
|
105
|
+
]
|
|
106
|
+
: commentPaths;
|
|
100
107
|
return dedupePathReferences(references);
|
|
101
108
|
}
|
|
102
109
|
|