@shapeshift-labs/frontier-lang-compiler 0.2.96 → 0.2.98
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/dist/declarations/semantic-edit-script.d.ts +2 -0
- package/dist/internal/index-impl/projectSemanticEditScriptToSource.js +44 -56
- package/dist/internal/index-impl/semanticEditOperationCoverage.js +141 -0
- package/dist/internal/index-impl/semanticEditScriptClassification.js +1 -0
- package/dist/internal/index-impl/semanticEditScripts.js +6 -2
- package/dist/internal/index-impl/semanticSourceEditDedupe.js +99 -0
- package/dist/native-region-scanner-js.js +6 -1
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ export type SemanticEditScriptOperationStatus =
|
|
|
10
10
|
| 'candidate'
|
|
11
11
|
| 'portable'
|
|
12
12
|
| 'already-applied'
|
|
13
|
+
| 'covered'
|
|
13
14
|
| 'needs-port'
|
|
14
15
|
| 'conflict'
|
|
15
16
|
| 'stale'
|
|
@@ -82,6 +83,7 @@ export interface SemanticEditScriptSummary {
|
|
|
82
83
|
readonly conflicts: number;
|
|
83
84
|
readonly stale: number;
|
|
84
85
|
readonly blocked: number;
|
|
86
|
+
readonly covered?: number;
|
|
85
87
|
readonly candidates: number;
|
|
86
88
|
readonly autoMergeCandidates: number;
|
|
87
89
|
readonly semanticKeys?: readonly string[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
2
|
import { idFragment, uniqueStrings } from '../../native-import-utils.js';
|
|
3
3
|
import { semanticEditIdentityFields } from './semanticEditIdentityRecords.js';
|
|
4
|
+
import { applySourceEdits, dedupeSourceEdits, validateSourceEdits } from './semanticSourceEditDedupe.js';
|
|
4
5
|
|
|
5
6
|
export function projectSemanticEditScriptToSource(input = {}) {
|
|
6
7
|
const script = input.script;
|
|
@@ -12,7 +13,12 @@ export function projectSemanticEditScriptToSource(input = {}) {
|
|
|
12
13
|
if (typeof workerSourceText !== 'string') reasonCodes.push('missing-worker-source-text');
|
|
13
14
|
if (typeof headSourceText !== 'string') reasonCodes.push('missing-head-source-text');
|
|
14
15
|
const edits = [];
|
|
16
|
+
const coveredOperationIds = [];
|
|
15
17
|
for (const [index, operation] of (script.operations ?? []).entries()) {
|
|
18
|
+
if (operation.status === 'covered') {
|
|
19
|
+
coveredOperationIds.push(operation.id);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
16
22
|
const edit = sourceEditForOperation(operation, workerSourceText, headSourceText, index);
|
|
17
23
|
if (edit.ok) edits.push(edit.value);
|
|
18
24
|
else reasonCodes.push(...edit.reasonCodes);
|
|
@@ -34,7 +40,7 @@ export function projectSemanticEditScriptToSource(input = {}) {
|
|
|
34
40
|
headHash: script.headHash,
|
|
35
41
|
projectedHash: sourceText === undefined ? undefined : hashSemanticValue(sourceText),
|
|
36
42
|
appliedOperations: blocked ? [] : deduped.edits.map((edit) => edit.operationId),
|
|
37
|
-
skippedOperations: blocked ? (script.operations ?? []).map((operation) => operation.id) : deduped.skippedOperationIds,
|
|
43
|
+
skippedOperations: blocked ? (script.operations ?? []).map((operation) => operation.id) : uniqueStrings([...coveredOperationIds, ...deduped.skippedOperationIds]),
|
|
38
44
|
edits: blocked ? [] : deduped.edits.map(projectionEditRecord),
|
|
39
45
|
sourceText,
|
|
40
46
|
admission: {
|
|
@@ -65,6 +71,9 @@ function sourceEditForOperation(operation, workerSourceText, headSourceText, ord
|
|
|
65
71
|
if (operation.changeKind === 'added' || String(operation.kind ?? '').startsWith('add')) {
|
|
66
72
|
return insertionEditForOperation(operation, identity, workerSourceText, headSourceText, order);
|
|
67
73
|
}
|
|
74
|
+
if (operation.changeKind === 'removed' || String(operation.kind ?? '').startsWith('remove')) {
|
|
75
|
+
return removalEditForOperation(operation, identity, headSourceText, order);
|
|
76
|
+
}
|
|
68
77
|
const workerOffsets = spanOffsets(workerSourceText, operation.spans?.worker);
|
|
69
78
|
const headOffsets = spanOffsets(headSourceText, operation.spans?.head ?? operation.spans?.base ?? operation.anchor?.sourceSpan);
|
|
70
79
|
const reasons = [];
|
|
@@ -98,6 +107,33 @@ function sourceEditForOperation(operation, workerSourceText, headSourceText, ord
|
|
|
98
107
|
};
|
|
99
108
|
}
|
|
100
109
|
|
|
110
|
+
function removalEditForOperation(operation, identity, headSourceText, order) {
|
|
111
|
+
const headOffsets = spanOffsets(headSourceText, operation.spans?.head ?? operation.spans?.base ?? operation.anchor?.sourceSpan);
|
|
112
|
+
const reasons = [];
|
|
113
|
+
if (!headOffsets) reasons.push(`head-span-not-resolvable:${operation.id}`);
|
|
114
|
+
if (reasons.length) return { ok: false, reasonCodes: reasons };
|
|
115
|
+
const rawCurrent = headSourceText.slice(headOffsets.start, headOffsets.end);
|
|
116
|
+
const expectedHeadHash = operation.hashes?.headTextHash ?? operation.hashes?.baseTextHash;
|
|
117
|
+
if (expectedHeadHash && hashSemanticValue(rawCurrent) !== expectedHeadHash) {
|
|
118
|
+
reasons.push(`head-span-hash-mismatch:${operation.id}`);
|
|
119
|
+
}
|
|
120
|
+
if (reasons.length) return { ok: false, reasonCodes: reasons };
|
|
121
|
+
const range = removalRange(headSourceText, headOffsets);
|
|
122
|
+
return {
|
|
123
|
+
ok: true,
|
|
124
|
+
value: {
|
|
125
|
+
operationId: operation.id,
|
|
126
|
+
order,
|
|
127
|
+
...identity,
|
|
128
|
+
editKind: 'delete',
|
|
129
|
+
start: range.start,
|
|
130
|
+
end: range.end,
|
|
131
|
+
current: headSourceText.slice(range.start, range.end),
|
|
132
|
+
replacement: ''
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
101
137
|
function insertionEditForOperation(operation, identity, workerSourceText, headSourceText, order) {
|
|
102
138
|
const workerOffsets = spanOffsets(workerSourceText, operation.spans?.worker);
|
|
103
139
|
const reasons = [];
|
|
@@ -207,44 +243,6 @@ function semanticEditIdentity(operation) {
|
|
|
207
243
|
});
|
|
208
244
|
}
|
|
209
245
|
|
|
210
|
-
function applySourceEdits(sourceText, edits) {
|
|
211
|
-
return edits.filter((edit) => !edit.alreadyApplied)
|
|
212
|
-
.sort(sourceEditSort)
|
|
213
|
-
.reduce((text, edit) => text.slice(0, edit.start) + edit.replacement + text.slice(edit.end), sourceText);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function dedupeSourceEdits(edits) {
|
|
217
|
-
const seen = new Map();
|
|
218
|
-
const result = [];
|
|
219
|
-
const skippedOperationIds = [];
|
|
220
|
-
for (const edit of edits) {
|
|
221
|
-
const key = duplicateEditKey(edit);
|
|
222
|
-
if (key && seen.has(key)) {
|
|
223
|
-
skippedOperationIds.push(edit.operationId);
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
if (key) seen.set(key, edit.operationId);
|
|
227
|
-
result.push(edit);
|
|
228
|
-
}
|
|
229
|
-
return { edits: result, skippedOperationIds };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function duplicateEditKey(edit) {
|
|
233
|
-
if (edit.editKind !== 'insert') return undefined;
|
|
234
|
-
return [
|
|
235
|
-
'insert',
|
|
236
|
-
edit.start,
|
|
237
|
-
edit.end,
|
|
238
|
-
edit.insertion?.mode,
|
|
239
|
-
edit.insertion?.anchorKey,
|
|
240
|
-
hashSemanticValue(edit.replacementSpanText ?? edit.replacement)
|
|
241
|
-
].join(':');
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function sourceEditSort(left, right) {
|
|
245
|
-
return right.start - left.start || right.end - left.end || (right.order ?? 0) - (left.order ?? 0);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
246
|
function projectedSourcePath(script, edits) {
|
|
249
247
|
return edits.map((edit) => edit.sourcePath).find(Boolean) ?? script.sourcePath;
|
|
250
248
|
}
|
|
@@ -278,6 +276,13 @@ function insertionOffset(sourceText, insertion) {
|
|
|
278
276
|
return { ok: false, reasonCodes: ['insertion-mode-unsupported'] };
|
|
279
277
|
}
|
|
280
278
|
|
|
279
|
+
function removalRange(sourceText, span) {
|
|
280
|
+
const range = { ...span };
|
|
281
|
+
if (range.end < sourceText.length && sourceText[range.end] === '\n') range.end += 1;
|
|
282
|
+
else if (range.start > 0 && sourceText[range.start - 1] === '\n') range.start -= 1;
|
|
283
|
+
return range;
|
|
284
|
+
}
|
|
285
|
+
|
|
281
286
|
function insertionReplacement(text, sourceText, offset) {
|
|
282
287
|
let replacement = String(text ?? '');
|
|
283
288
|
if (offset > 0 && sourceText[offset - 1] !== '\n') replacement = `\n${replacement}`;
|
|
@@ -291,23 +296,6 @@ function afterLineOffset(sourceText, offset) {
|
|
|
291
296
|
return sourceText[offset] === '\n' ? offset + 1 : offset;
|
|
292
297
|
}
|
|
293
298
|
|
|
294
|
-
function validateSourceEdits(edits) {
|
|
295
|
-
const reasons = [];
|
|
296
|
-
const ordered = edits.filter((edit) => !edit.alreadyApplied).sort((left, right) => left.start - right.start || left.end - right.end);
|
|
297
|
-
for (let index = 1; index < ordered.length; index += 1) {
|
|
298
|
-
const previous = ordered[index - 1];
|
|
299
|
-
const current = ordered[index];
|
|
300
|
-
if (editsOverlap(previous, current)) reasons.push(`source-edit-overlap:${previous.operationId}:${current.operationId}`);
|
|
301
|
-
}
|
|
302
|
-
return uniqueStrings(reasons);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function editsOverlap(left, right) {
|
|
306
|
-
if (left.start === left.end) return right.start < left.start && left.start < right.end;
|
|
307
|
-
if (right.start === right.end) return left.start < right.start && right.start < left.end;
|
|
308
|
-
return left.start < right.end && right.start < left.end;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
299
|
function compactRecord(value) {
|
|
312
300
|
return Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined && (!Array.isArray(entry) || entry.length > 0)));
|
|
313
301
|
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { uniqueStrings } from '../../native-import-utils.js';
|
|
2
|
+
import { nativeImportSourceText } from './nativeImportSourceText.js';
|
|
3
|
+
|
|
4
|
+
export function markCoveredSemanticEditOperations(operations, context) {
|
|
5
|
+
const sourceText = {
|
|
6
|
+
base: nativeImportSourceText(context.base),
|
|
7
|
+
worker: nativeImportSourceText(context.worker)
|
|
8
|
+
};
|
|
9
|
+
return (operations ?? []).map((operation) => {
|
|
10
|
+
const coveredBy = coveredByChildOperations(operation, operations, sourceText);
|
|
11
|
+
if (!coveredBy.length) return operation;
|
|
12
|
+
return {
|
|
13
|
+
...operation,
|
|
14
|
+
status: 'covered',
|
|
15
|
+
readiness: 'ready',
|
|
16
|
+
confidence: Math.max(operation.confidence ?? 0, 0.82),
|
|
17
|
+
reasonCodes: uniqueStrings([...(operation.reasonCodes ?? []), 'container-covered-by-child-edits']),
|
|
18
|
+
evidenceIds: uniqueStrings(operation.evidenceIds ?? []),
|
|
19
|
+
metadata: {
|
|
20
|
+
...(operation.metadata ?? {}),
|
|
21
|
+
coveredByOperationIds: coveredBy.map((child) => child.id)
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function coveredByChildOperations(container, operations, sourceText) {
|
|
28
|
+
if (!isCoverableContainer(container)) return [];
|
|
29
|
+
const containerBase = spanOffsets(sourceText.base, container.spans?.base);
|
|
30
|
+
const containerWorker = spanOffsets(sourceText.worker, container.spans?.worker);
|
|
31
|
+
if (!containerBase || !containerWorker) return [];
|
|
32
|
+
const childEdits = (operations ?? [])
|
|
33
|
+
.filter((operation) => operation.id !== container.id)
|
|
34
|
+
.map((operation) => childEdit(operation, sourceText, containerBase))
|
|
35
|
+
.filter(Boolean);
|
|
36
|
+
if (!childEdits.length) return [];
|
|
37
|
+
const baseText = sourceText.base.slice(containerBase.start, containerBase.end);
|
|
38
|
+
const workerText = sourceText.worker.slice(containerWorker.start, containerWorker.end);
|
|
39
|
+
return applyLocalEdits(baseText, childEdits) === workerText ? childEdits.map((edit) => edit.operation) : [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isCoverableContainer(operation) {
|
|
43
|
+
if (operation.changeKind !== 'modified') return false;
|
|
44
|
+
if (!operation.spans?.base || !operation.spans?.worker) return false;
|
|
45
|
+
const kind = String(operation.anchor?.regionKind ?? operation.regionKind ?? '');
|
|
46
|
+
return kind === 'type' || kind === 'config' || kind === 'content' || kind === 'route' || kind === 'property';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function childEdit(operation, sourceText, containerBase) {
|
|
50
|
+
if (!['portable', 'already-applied'].includes(operation.status)) return undefined;
|
|
51
|
+
if (operation.changeKind === 'modified') return replacementChildEdit(operation, sourceText, containerBase);
|
|
52
|
+
if (operation.changeKind === 'added') return insertionChildEdit(operation, sourceText, containerBase);
|
|
53
|
+
if (operation.changeKind === 'removed') return removalChildEdit(operation, sourceText, containerBase);
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function replacementChildEdit(operation, sourceText, containerBase) {
|
|
58
|
+
const base = spanOffsets(sourceText.base, operation.spans?.base);
|
|
59
|
+
const worker = spanOffsets(sourceText.worker, operation.spans?.worker);
|
|
60
|
+
if (!contained(base, containerBase) || !worker) return undefined;
|
|
61
|
+
return {
|
|
62
|
+
operation,
|
|
63
|
+
start: base.start - containerBase.start,
|
|
64
|
+
end: base.end - containerBase.start,
|
|
65
|
+
replacement: sourceText.worker.slice(worker.start, worker.end)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function insertionChildEdit(operation, sourceText, containerBase) {
|
|
70
|
+
const worker = spanOffsets(sourceText.worker, operation.spans?.worker);
|
|
71
|
+
const offset = insertionOffset(sourceText.base, operation.insertion, containerBase);
|
|
72
|
+
if (!worker || offset === undefined) return undefined;
|
|
73
|
+
const baseText = sourceText.base.slice(containerBase.start, containerBase.end);
|
|
74
|
+
const replacement = insertionReplacement(sourceText.worker.slice(worker.start, worker.end), baseText, offset);
|
|
75
|
+
return { operation, start: offset, end: offset, replacement };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function removalChildEdit(operation, sourceText, containerBase) {
|
|
79
|
+
const base = spanOffsets(sourceText.base, operation.spans?.base);
|
|
80
|
+
if (!contained(base, containerBase)) return undefined;
|
|
81
|
+
const range = removalRange(sourceText.base, base, containerBase);
|
|
82
|
+
return { operation, start: range.start - containerBase.start, end: range.end - containerBase.start, replacement: '' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function insertionOffset(sourceText, insertion, containerBase) {
|
|
86
|
+
const mode = insertion?.mode;
|
|
87
|
+
if (mode === 'file-start') return 0;
|
|
88
|
+
if (mode === 'file-end') return containerBase.end - containerBase.start;
|
|
89
|
+
const span = spanOffsets(sourceText, insertion?.baseSpan ?? insertion?.headSpan);
|
|
90
|
+
if (!contained(span, containerBase)) return undefined;
|
|
91
|
+
if (mode === 'before') return span.start - containerBase.start;
|
|
92
|
+
if (mode === 'after') return afterLineOffset(sourceText, span.end) - containerBase.start;
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function applyLocalEdits(sourceText, edits) {
|
|
97
|
+
return edits
|
|
98
|
+
.sort((left, right) => right.start - left.start || right.end - left.end)
|
|
99
|
+
.reduce((text, edit) => text.slice(0, edit.start) + edit.replacement + text.slice(edit.end), sourceText);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function spanOffsets(sourceText, span) {
|
|
103
|
+
if (typeof sourceText !== 'string' || !span) return undefined;
|
|
104
|
+
if (typeof span.start === 'number' && typeof span.end === 'number' && span.end >= span.start) return { start: span.start, end: span.end };
|
|
105
|
+
if (typeof span.startLine !== 'number') return undefined;
|
|
106
|
+
const lineStarts = [0];
|
|
107
|
+
for (let index = 0; index < sourceText.length; index += 1) if (sourceText[index] === '\n') lineStarts.push(index + 1);
|
|
108
|
+
const startLine = Math.max(1, span.startLine);
|
|
109
|
+
const endLine = Math.max(startLine, typeof span.endLine === 'number' ? span.endLine : startLine);
|
|
110
|
+
const start = lineStarts[startLine - 1];
|
|
111
|
+
const endLineStart = lineStarts[endLine - 1];
|
|
112
|
+
if (start === undefined || endLineStart === undefined) return undefined;
|
|
113
|
+
const startColumn = Math.max(1, span.startColumn ?? 1) - 1;
|
|
114
|
+
const lineEnd = lineStarts[endLine] === undefined ? sourceText.length : lineStarts[endLine] - 1;
|
|
115
|
+
const endColumn = span.endColumn === undefined ? lineEnd - endLineStart : Math.max(1, span.endColumn) - 1;
|
|
116
|
+
return { start: start + startColumn, end: endLineStart + endColumn };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function contained(inner, outer) {
|
|
120
|
+
return Boolean(inner && outer && outer.start <= inner.start && inner.end <= outer.end);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function insertionReplacement(text, sourceText, offset) {
|
|
124
|
+
let replacement = String(text ?? '');
|
|
125
|
+
if (offset > 0 && sourceText[offset - 1] !== '\n') replacement = `\n${replacement}`;
|
|
126
|
+
if (offset < sourceText.length && !replacement.endsWith('\n')) replacement += '\n';
|
|
127
|
+
if (offset === sourceText.length && sourceText && !sourceText.endsWith('\n')) replacement = `\n${replacement}`;
|
|
128
|
+
if (offset === sourceText.length && !replacement.endsWith('\n')) replacement += '\n';
|
|
129
|
+
return replacement;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function removalRange(sourceText, span, container) {
|
|
133
|
+
const range = { ...span };
|
|
134
|
+
if (range.end < container.end && sourceText[range.end] === '\n') range.end += 1;
|
|
135
|
+
else if (range.start > container.start && sourceText[range.start - 1] === '\n') range.start -= 1;
|
|
136
|
+
return range;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function afterLineOffset(sourceText, offset) {
|
|
140
|
+
return sourceText[offset] === '\n' ? offset + 1 : offset;
|
|
141
|
+
}
|
|
@@ -43,6 +43,7 @@ export function summarizeSemanticEditOperations(operations) {
|
|
|
43
43
|
conflicts: byStatus.conflict ?? 0,
|
|
44
44
|
stale: byStatus.stale ?? 0,
|
|
45
45
|
blocked: byStatus.blocked ?? 0,
|
|
46
|
+
covered: byStatus.covered ?? 0,
|
|
46
47
|
candidates: byStatus.candidate ?? 0,
|
|
47
48
|
autoMergeCandidates: (byStatus.portable ?? 0) + (byStatus['already-applied'] ?? 0),
|
|
48
49
|
semanticKeys: uniqueStrings(operations.map((operation) => operation.semanticKey).filter(Boolean)),
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
summarizeSemanticEditOperations
|
|
14
14
|
} from './semanticEditScriptClassification.js';
|
|
15
15
|
import { semanticEditInsertionAnchor } from './semanticEditInsertionAnchors.js';
|
|
16
|
+
import { markCoveredSemanticEditOperations } from './semanticEditOperationCoverage.js';
|
|
16
17
|
import { sourceTextForSpan } from './sourceTextForSpan.js';
|
|
17
18
|
import { semanticEditIdentityFields, semanticEditOperationContentHash } from './semanticEditIdentityRecords.js';
|
|
18
19
|
|
|
@@ -52,7 +53,10 @@ export function createSemanticEditScript(input = {}, options = {}) {
|
|
|
52
53
|
metadata: { source: 'createSemanticEditScript' }
|
|
53
54
|
}) : undefined;
|
|
54
55
|
const context = createEditContext({ base, worker, head, workerChangeSet, headChangeSet, headLineage });
|
|
55
|
-
const operations =
|
|
56
|
+
const operations = markCoveredSemanticEditOperations(
|
|
57
|
+
workerChangeSet.changedRegions.map((region, index) => semanticEditOperation(region, index, context, input)),
|
|
58
|
+
context
|
|
59
|
+
);
|
|
56
60
|
const summary = summarizeSemanticEditOperations(operations);
|
|
57
61
|
const admission = semanticEditAdmission({ operations, summary, head, workerChangeSet, headChangeSet, input });
|
|
58
62
|
const evidence = semanticEditEvidence({ input, language, sourcePath, workerChangeSet, headChangeSet, headLineage, summary, admission });
|
|
@@ -169,7 +173,7 @@ function semanticEditOperation(region, index, context, input) {
|
|
|
169
173
|
const identityRecord = semanticEditIdentityRecord({ kind, region, anchor });
|
|
170
174
|
const identity = semanticEditIdentityFields(identityRecord);
|
|
171
175
|
return compactRecord({
|
|
172
|
-
id: `semantic_edit_op_${idFragment([input.id ?? 'semantic_edit'
|
|
176
|
+
id: `semantic_edit_op_${idFragment([index, anchorKey, input.id ?? 'semantic_edit'].join(':'))}`,
|
|
173
177
|
kind,
|
|
174
178
|
changeKind: region.changeKind,
|
|
175
179
|
anchor,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
+
import { uniqueStrings } from '../../native-import-utils.js';
|
|
3
|
+
|
|
4
|
+
export function applySourceEdits(sourceText, edits) {
|
|
5
|
+
return edits.filter((edit) => !edit.alreadyApplied)
|
|
6
|
+
.sort(sourceEditSort)
|
|
7
|
+
.reduce((text, edit) => text.slice(0, edit.start) + edit.replacement + text.slice(edit.end), sourceText);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function dedupeSourceEdits(edits) {
|
|
11
|
+
const exact = dedupeExactInsertions(edits);
|
|
12
|
+
const covered = removeCoveredContainerReplacements(exact.edits);
|
|
13
|
+
return {
|
|
14
|
+
edits: covered.edits,
|
|
15
|
+
skippedOperationIds: [...exact.skippedOperationIds, ...covered.skippedOperationIds]
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function validateSourceEdits(edits) {
|
|
20
|
+
const reasons = [];
|
|
21
|
+
const ordered = edits.filter((edit) => !edit.alreadyApplied).sort((left, right) => left.start - right.start || left.end - right.end);
|
|
22
|
+
for (let index = 1; index < ordered.length; index += 1) {
|
|
23
|
+
const previous = ordered[index - 1];
|
|
24
|
+
const current = ordered[index];
|
|
25
|
+
if (editsOverlap(previous, current)) reasons.push(`source-edit-overlap:${previous.operationId}:${current.operationId}`);
|
|
26
|
+
}
|
|
27
|
+
return uniqueStrings(reasons);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function dedupeExactInsertions(edits) {
|
|
31
|
+
const seen = new Map();
|
|
32
|
+
const result = [];
|
|
33
|
+
const skippedOperationIds = [];
|
|
34
|
+
for (const edit of edits) {
|
|
35
|
+
const key = duplicateEditKey(edit);
|
|
36
|
+
if (key && seen.has(key)) {
|
|
37
|
+
skippedOperationIds.push(edit.operationId);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (key) seen.set(key, edit.operationId);
|
|
41
|
+
result.push(edit);
|
|
42
|
+
}
|
|
43
|
+
return { edits: result, skippedOperationIds };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function removeCoveredContainerReplacements(edits) {
|
|
47
|
+
const skippedOperationIds = [];
|
|
48
|
+
const result = [];
|
|
49
|
+
for (const edit of edits) {
|
|
50
|
+
if (containerReplacementCoveredByInsertions(edit, edits)) {
|
|
51
|
+
skippedOperationIds.push(edit.operationId);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
result.push(edit);
|
|
55
|
+
}
|
|
56
|
+
return { edits: result, skippedOperationIds };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function containerReplacementCoveredByInsertions(edit, edits) {
|
|
60
|
+
if (edit.editKind !== 'replace' || edit.alreadyApplied) return false;
|
|
61
|
+
const insertions = containedInsertions(edit, edits);
|
|
62
|
+
if (!insertions.length) return false;
|
|
63
|
+
const localEdits = insertions.map((insertion) => ({
|
|
64
|
+
...insertion,
|
|
65
|
+
start: insertion.start - edit.start,
|
|
66
|
+
end: insertion.end - edit.start
|
|
67
|
+
}));
|
|
68
|
+
return applySourceEdits(edit.current, localEdits) === edit.replacement;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function containedInsertions(container, edits) {
|
|
72
|
+
return edits
|
|
73
|
+
.filter((edit) => edit.editKind === 'insert' && !edit.alreadyApplied)
|
|
74
|
+
.filter((edit) => edit.operationId !== container.operationId)
|
|
75
|
+
.filter((edit) => container.start <= edit.start && edit.end <= container.end)
|
|
76
|
+
.sort((left, right) => left.start - right.start || (left.order ?? 0) - (right.order ?? 0));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function duplicateEditKey(edit) {
|
|
80
|
+
if (edit.editKind !== 'insert') return undefined;
|
|
81
|
+
return [
|
|
82
|
+
'insert',
|
|
83
|
+
edit.start,
|
|
84
|
+
edit.end,
|
|
85
|
+
edit.insertion?.mode,
|
|
86
|
+
edit.insertion?.anchorKey,
|
|
87
|
+
hashSemanticValue(edit.replacementSpanText ?? edit.replacement)
|
|
88
|
+
].join(':');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function sourceEditSort(left, right) {
|
|
92
|
+
return right.start - left.start || right.end - left.end || (right.order ?? 0) - (left.order ?? 0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function editsOverlap(left, right) {
|
|
96
|
+
if (left.start === left.end) return right.start < left.start && left.start < right.end;
|
|
97
|
+
if (right.start === right.end) return left.start < right.start && right.start < left.end;
|
|
98
|
+
return left.start < right.end && right.start < left.end;
|
|
99
|
+
}
|
|
@@ -160,11 +160,16 @@ function jsDeclarationWithSourceSpan(input, declaration, lines) {
|
|
|
160
160
|
startLine,
|
|
161
161
|
endLine,
|
|
162
162
|
startColumn: declaration.span?.startColumn ?? 1,
|
|
163
|
-
endColumn: declaration
|
|
163
|
+
endColumn: declarationEndColumn(declaration, startLine, endLine, endLineText)
|
|
164
164
|
}
|
|
165
165
|
};
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
function declarationEndColumn(declaration, startLine, endLine, endLineText) {
|
|
169
|
+
if (endLine !== startLine) return endLineText.length + 1;
|
|
170
|
+
return declaration.span?.endColumn ?? endLineText.length + 1;
|
|
171
|
+
}
|
|
172
|
+
|
|
168
173
|
function jsBalancedDeclarationEndLine(input, lines, startLine) {
|
|
169
174
|
const state = { inBlockComment: false, inTemplateString: false };
|
|
170
175
|
let depth = 0;
|
package/package.json
CHANGED