@x-oasis/map-diff-range 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,253 @@
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
9
+ background: #f5f5f5;
10
+ padding: 20px;
11
+ line-height: 1.6;
12
+ }
13
+
14
+ .container {
15
+ max-width: 1600px;
16
+ margin: 0 auto;
17
+ background: white;
18
+ border-radius: 8px;
19
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
20
+ padding: 30px;
21
+ }
22
+
23
+ h1 {
24
+ color: #333;
25
+ margin-bottom: 10px;
26
+ font-size: 28px;
27
+ }
28
+
29
+ .subtitle {
30
+ color: #666;
31
+ margin-bottom: 30px;
32
+ font-size: 14px;
33
+ }
34
+
35
+ .section {
36
+ margin-bottom: 30px;
37
+ }
38
+
39
+ .section-title {
40
+ font-size: 18px;
41
+ font-weight: 600;
42
+ color: #333;
43
+ margin-bottom: 15px;
44
+ padding-bottom: 10px;
45
+ border-bottom: 2px solid #e0e0e0;
46
+ }
47
+
48
+ .file-comparison {
49
+ display: grid;
50
+ grid-template-columns: 1fr 1fr 1fr;
51
+ gap: 20px;
52
+ margin-bottom: 20px;
53
+ }
54
+
55
+ .file-panel {
56
+ border: 1px solid #ddd;
57
+ border-radius: 4px;
58
+ overflow: hidden;
59
+ }
60
+
61
+ .file-header {
62
+ background: #f8f8f8;
63
+ padding: 10px 15px;
64
+ font-weight: 600;
65
+ color: #555;
66
+ border-bottom: 1px solid #ddd;
67
+ font-size: 13px;
68
+ }
69
+
70
+ .file-content {
71
+ padding: 15px;
72
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
73
+ font-size: 13px;
74
+ line-height: 1.8;
75
+ white-space: pre-wrap;
76
+ word-wrap: break-word;
77
+ max-height: 400px;
78
+ overflow-y: auto;
79
+ background: #fff;
80
+ resize: vertical;
81
+ min-height: 200px;
82
+ width: 100%;
83
+ border: none;
84
+ outline: none;
85
+ }
86
+
87
+ .file-content:focus {
88
+ outline: none;
89
+ }
90
+
91
+ .controls {
92
+ display: grid;
93
+ grid-template-columns: 1fr 1fr auto;
94
+ gap: 15px;
95
+ margin-bottom: 20px;
96
+ align-items: end;
97
+ }
98
+
99
+ .control-group {
100
+ display: flex;
101
+ flex-direction: column;
102
+ }
103
+
104
+ .control-group label {
105
+ font-size: 12px;
106
+ font-weight: 600;
107
+ color: #555;
108
+ margin-bottom: 5px;
109
+ }
110
+
111
+ .control-group input {
112
+ padding: 10px;
113
+ border: 1px solid #ddd;
114
+ border-radius: 4px;
115
+ font-size: 14px;
116
+ }
117
+
118
+ .control-group input:focus {
119
+ outline: none;
120
+ border-color: #4a90e2;
121
+ box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1);
122
+ }
123
+
124
+ button {
125
+ padding: 10px 20px;
126
+ background: #4a90e2;
127
+ color: white;
128
+ border: none;
129
+ border-radius: 4px;
130
+ font-size: 14px;
131
+ font-weight: 600;
132
+ cursor: pointer;
133
+ transition: background 0.2s;
134
+ }
135
+
136
+ button:hover {
137
+ background: #357abd;
138
+ }
139
+
140
+ button:active {
141
+ transform: translateY(1px);
142
+ }
143
+
144
+ .result-panel {
145
+ border: 2px solid #4a90e2;
146
+ border-radius: 4px;
147
+ padding: 15px;
148
+ background: #f0f7ff;
149
+ margin-top: 20px;
150
+ }
151
+
152
+ .result-title {
153
+ font-weight: 600;
154
+ color: #4a90e2;
155
+ margin-bottom: 10px;
156
+ }
157
+
158
+ .result-content {
159
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
160
+ font-size: 13px;
161
+ line-height: 1.8;
162
+ white-space: pre-wrap;
163
+ word-wrap: break-word;
164
+ background: white;
165
+ padding: 15px;
166
+ border-radius: 4px;
167
+ border: 1px solid #ddd;
168
+ max-height: 400px;
169
+ overflow-y: auto;
170
+ }
171
+
172
+ .info-box {
173
+ background: #e7f3ff;
174
+ border-left: 4px solid #4a90e2;
175
+ padding: 12px;
176
+ margin-bottom: 20px;
177
+ border-radius: 4px;
178
+ }
179
+
180
+ .info-box strong {
181
+ color: #4a90e2;
182
+ }
183
+
184
+ .analysis-panel {
185
+ margin-top: 20px;
186
+ padding: 15px;
187
+ background: #f9f9f9;
188
+ border-radius: 4px;
189
+ border: 1px solid #ddd;
190
+ }
191
+
192
+ .analysis-item {
193
+ margin-bottom: 15px;
194
+ padding: 10px;
195
+ background: white;
196
+ border-radius: 4px;
197
+ border: 1px solid #e0e0e0;
198
+ }
199
+
200
+ .analysis-item strong {
201
+ color: #4a90e2;
202
+ display: block;
203
+ margin-bottom: 5px;
204
+ }
205
+
206
+ .analysis-value {
207
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
208
+ font-size: 12px;
209
+ color: #333;
210
+ }
211
+
212
+ .diff-entry {
213
+ margin: 5px 0;
214
+ padding: 5px;
215
+ border-radius: 3px;
216
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
217
+ font-size: 12px;
218
+ }
219
+
220
+ .diff-entry.insert {
221
+ background: #d4edda;
222
+ color: #155724;
223
+ }
224
+
225
+ .diff-entry.delete {
226
+ background: #f8d7da;
227
+ color: #721c24;
228
+ }
229
+
230
+ .diff-entry.equal {
231
+ background: #f8f9fa;
232
+ color: #333;
233
+ }
234
+
235
+ .range-info {
236
+ display: inline-block;
237
+ margin: 0 5px;
238
+ padding: 2px 6px;
239
+ background: #e7f3ff;
240
+ border-radius: 3px;
241
+ font-family: monospace;
242
+ font-size: 12px;
243
+ color: #4a90e2;
244
+ }
245
+
246
+ .error-message {
247
+ padding: 10px;
248
+ background: #f8d7da;
249
+ color: #721c24;
250
+ border-radius: 4px;
251
+ margin-top: 10px;
252
+ border: 1px solid #f5c6cb;
253
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+ import './index.css';
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ );
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "baseUrl": ".",
19
+ "paths": {
20
+ "@x-oasis/map-diff-range": ["../src/index.ts"]
21
+ }
22
+ },
23
+ "include": ["src"],
24
+ "references": [{ "path": "./tsconfig.node.json" }]
25
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true
8
+ },
9
+ "include": ["vite.config.ts"]
10
+ }
@@ -0,0 +1,30 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import path from 'path';
4
+
5
+ // 如果设置了 GITHUB_PAGES 环境变量,使用仓库名和子路径作为 base 路径
6
+ // 格式: owner/repo-name,我们只需要 repo-name
7
+ // 子路径从 GITHUB_PAGES_PATH 环境变量获取,例如: map-diff-range
8
+ const getBasePath = () => {
9
+ if (process.env.GITHUB_PAGES === 'true') {
10
+ const repo = process.env.GITHUB_REPOSITORY || 'red-armor/x-oasis';
11
+ const repoName = repo.split('/')[1];
12
+ const subPath = process.env.GITHUB_PAGES_PATH || 'map-diff-range';
13
+ return `/${repoName}/${subPath}/`;
14
+ }
15
+ return '/';
16
+ };
17
+
18
+ export default defineConfig({
19
+ base: getBasePath(),
20
+ plugins: [react()],
21
+ resolve: {
22
+ alias: {
23
+ '@x-oasis/map-diff-range': path.resolve(__dirname, '../src/index.ts'),
24
+ },
25
+ },
26
+ server: {
27
+ port: 3000,
28
+ open: true,
29
+ },
30
+ });
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@x-oasis/map-diff-range",
3
+ "version": "0.1.0",
4
+ "description": "Range mapping and change analysis tool based on diff-match-patch",
5
+ "main": "dist/index.js",
6
+ "typings": "dist/index.d.ts",
7
+ "module": "dist/map-diff-range.esm.js",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "author": "",
12
+ "license": "ISC",
13
+ "devDependencies": {
14
+ "tsdx": "^0.14.1"
15
+ },
16
+ "dependencies": {
17
+ "diff-match-patch": "^1.0.5"
18
+ },
19
+ "scripts": {
20
+ "build": "tsdx build --tsconfig tsconfig.build.json",
21
+ "clean": "rimraf ./dist",
22
+ "test": "vitest",
23
+ "compile": "tsc -p tsconfig.build.json"
24
+ }
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,352 @@
1
+ /**
2
+ * 基于 diff-match-patch 的 range 映射与变更分析工具
3
+ * 用于在 originalContent / currentContent / finalContent 之间映射 range,并分析片段级变更
4
+ *
5
+ * 参考: https://github.com/red-armor/x-oasis/blob/main/packages/diff/diff-match-patch/src/index.ts#L123
6
+ */
7
+
8
+ import { diff_match_patch } from 'diff-match-patch';
9
+
10
+ /** diff 操作常量,与 diff-match-patch 一致 */
11
+ const DIFF_DELETE = -1;
12
+ const DIFF_INSERT = 1;
13
+ const DIFF_EQUAL = 0;
14
+
15
+ /** 单端 range,start 含头,end 含尾(substring 语义) */
16
+ export interface Range {
17
+ start: number;
18
+ end: number;
19
+ }
20
+
21
+ /** 片段级变更分析结果 */
22
+ export interface FragmentChangeAnalysis {
23
+ /** 原始片段内容 */
24
+ originalFragment: string;
25
+ /** 最终片段内容 */
26
+ finalFragment: string;
27
+ /** 是否完全相同 */
28
+ equal: boolean;
29
+ /** 仅删除(无新增) */
30
+ onlyDeletion: boolean;
31
+ /** 仅新增(无删除) */
32
+ onlyInsertion: boolean;
33
+ /** 既有删除又有新增(替换) */
34
+ replacement: boolean;
35
+ /** 语义化描述(简短) */
36
+ summary: string;
37
+ /** 详细 diff 条目,便于上层做展示或进一步处理 */
38
+ diffs: Array<[number, string]>;
39
+ }
40
+
41
+ /**
42
+ * 将「新内容」中的 offset range 映射到「旧内容」中的 offset range
43
+ * 等价于 x-oasis 的 mapCurrentRangeToOriginal:diffs = diff_main(older, newer),给定 newer 上的 [start,end],返回 older 上的 [start,end]
44
+ *
45
+ * @param olderContent 旧内容(diff 的 text1)
46
+ * @param newerContent 新内容(diff 的 text2)
47
+ * @param startOffset 新内容中的 range 起始 offset
48
+ * @param endOffset 新内容中的 range 结束 offset(不含尾,即 [startOffset, endOffset) 或含尾由调用方约定,此处按 substring 含头含尾)
49
+ * @returns 旧内容中对应的 range;若越界或无效则返回 { start: 0, end: 0 }
50
+ */
51
+ export function mapNewerRangeToOlder(
52
+ olderContent: string,
53
+ newerContent: string,
54
+ startOffset: number,
55
+ endOffset: number
56
+ ): Range {
57
+ const dmp = new diff_match_patch();
58
+ const diffs = dmp.diff_main(olderContent, newerContent);
59
+ dmp.diff_cleanupSemantic(diffs);
60
+
61
+ let newerOffset = 0;
62
+ let olderOffset = 0;
63
+ let olderStart: number | null = null;
64
+ let olderEnd: number | null = null;
65
+
66
+ for (let i = 0; i < diffs.length; i++) {
67
+ const [op, text] = diffs[i];
68
+ const len = text.length;
69
+ const nextDiff = i < diffs.length - 1 ? diffs[i + 1] : null;
70
+
71
+ if (op === DIFF_EQUAL) {
72
+ const rangeStart = newerOffset;
73
+ const rangeEnd = newerOffset + len;
74
+
75
+ if (
76
+ olderStart === null &&
77
+ startOffset >= rangeStart &&
78
+ startOffset < rangeEnd
79
+ ) {
80
+ olderStart = olderOffset + (startOffset - rangeStart);
81
+ }
82
+ if (
83
+ olderEnd === null &&
84
+ endOffset > rangeStart &&
85
+ endOffset <= rangeEnd
86
+ ) {
87
+ const offsetInRange = endOffset - rangeStart;
88
+ olderEnd = olderOffset + offsetInRange;
89
+ if (endOffset === rangeEnd && nextDiff && nextDiff[0] === DIFF_DELETE) {
90
+ olderEnd = olderOffset + len + nextDiff[1].length;
91
+ }
92
+ }
93
+
94
+ newerOffset += len;
95
+ olderOffset += len;
96
+ } else if (op === DIFF_INSERT) {
97
+ const rangeStart = newerOffset;
98
+ const rangeEnd = newerOffset + len;
99
+
100
+ if (
101
+ olderStart === null &&
102
+ startOffset >= rangeStart &&
103
+ startOffset < rangeEnd
104
+ ) {
105
+ olderStart = olderOffset;
106
+ }
107
+ if (
108
+ olderEnd === null &&
109
+ endOffset > rangeStart &&
110
+ endOffset <= rangeEnd
111
+ ) {
112
+ olderEnd = olderOffset;
113
+ }
114
+
115
+ newerOffset += len;
116
+ } else if (op === DIFF_DELETE) {
117
+ if (olderStart === null && startOffset <= newerOffset) {
118
+ olderStart = olderOffset;
119
+ }
120
+ if (olderEnd === null && endOffset <= newerOffset) {
121
+ olderEnd = endOffset === newerOffset ? olderOffset + len : olderOffset;
122
+ } else if (olderEnd === null && endOffset === newerOffset) {
123
+ olderEnd = olderOffset + len;
124
+ }
125
+
126
+ olderOffset += len;
127
+ }
128
+
129
+ if (olderStart !== null && olderEnd !== null) break;
130
+ }
131
+
132
+ if (olderStart === null) olderStart = olderOffset;
133
+ if (olderEnd === null) olderEnd = olderOffset;
134
+
135
+ return { start: olderStart, end: olderEnd };
136
+ }
137
+
138
+ /**
139
+ * 将「旧内容」中的 offset range 映射到「新内容」中的 offset range
140
+ * 与 mapNewerRangeToOlder 对称:diffs = diff_main(older, newer),给定 older 上的 [start,end],返回 newer 上的 [start,end]
141
+ *
142
+ * @param olderContent 旧内容(diff 的 text1)
143
+ * @param newerContent 新内容(diff 的 text2)
144
+ * @param startOffset 旧内容中的 range 起始 offset
145
+ * @param endOffset 旧内容中的 range 结束 offset
146
+ * @returns 新内容中对应的 range
147
+ */
148
+ export function mapOlderRangeToNewer(
149
+ olderContent: string,
150
+ newerContent: string,
151
+ startOffset: number,
152
+ endOffset: number
153
+ ): Range {
154
+ const dmp = new diff_match_patch();
155
+ const diffs = dmp.diff_main(olderContent, newerContent);
156
+ dmp.diff_cleanupSemantic(diffs);
157
+
158
+ let olderOffset = 0;
159
+ let newerOffset = 0;
160
+ let newerStart: number | null = null;
161
+ let newerEnd: number | null = null;
162
+
163
+ for (let i = 0; i < diffs.length; i++) {
164
+ const [op, text] = diffs[i];
165
+ const len = text.length;
166
+ const nextDiff = i < diffs.length - 1 ? diffs[i + 1] : null;
167
+
168
+ if (op === DIFF_EQUAL) {
169
+ const oldRangeStart = olderOffset;
170
+ const oldRangeEnd = olderOffset + len;
171
+
172
+ if (
173
+ newerStart === null &&
174
+ startOffset >= oldRangeStart &&
175
+ startOffset < oldRangeEnd
176
+ ) {
177
+ newerStart = newerOffset + (startOffset - oldRangeStart);
178
+ }
179
+ if (
180
+ newerEnd === null &&
181
+ endOffset > oldRangeStart &&
182
+ endOffset <= oldRangeEnd
183
+ ) {
184
+ const offsetInRange = endOffset - oldRangeStart;
185
+ newerEnd = newerOffset + offsetInRange;
186
+ if (
187
+ endOffset === oldRangeEnd &&
188
+ nextDiff &&
189
+ nextDiff[0] === DIFF_INSERT
190
+ ) {
191
+ newerEnd = newerOffset + len + nextDiff[1].length;
192
+ }
193
+ }
194
+
195
+ olderOffset += len;
196
+ newerOffset += len;
197
+ } else if (op === DIFF_INSERT) {
198
+ newerOffset += len;
199
+ } else if (op === DIFF_DELETE) {
200
+ const oldRangeStart = olderOffset;
201
+ const oldRangeEnd = olderOffset + len;
202
+
203
+ if (
204
+ newerStart === null &&
205
+ startOffset >= oldRangeStart &&
206
+ startOffset < oldRangeEnd
207
+ ) {
208
+ newerStart = newerOffset;
209
+ }
210
+ if (
211
+ newerEnd === null &&
212
+ endOffset > oldRangeStart &&
213
+ endOffset <= oldRangeEnd
214
+ ) {
215
+ newerEnd = newerOffset;
216
+ } else if (newerEnd === null && endOffset <= olderOffset) {
217
+ newerEnd = newerOffset;
218
+ }
219
+
220
+ olderOffset += len;
221
+ }
222
+
223
+ if (newerStart !== null && newerEnd !== null) break;
224
+ }
225
+
226
+ if (newerStart === null) newerStart = newerOffset;
227
+ if (newerEnd === null) newerEnd = newerOffset;
228
+
229
+ return { start: newerStart, end: newerEnd };
230
+ }
231
+
232
+ /**
233
+ * 对比两段片段内容,分析发生的变更类型并生成简短描述
234
+ *
235
+ * @param originalFragment 原始片段
236
+ * @param finalFragment 变更后片段
237
+ * @returns 变更分析结果
238
+ */
239
+ export function analyzeFragmentChange(
240
+ originalFragment: string,
241
+ finalFragment: string
242
+ ): FragmentChangeAnalysis {
243
+ const dmp = new diff_match_patch();
244
+ const diffs = dmp.diff_main(originalFragment, finalFragment);
245
+ dmp.diff_cleanupSemantic(diffs);
246
+
247
+ const hasDelete = diffs.some(([op]) => op === DIFF_DELETE);
248
+ const hasInsert = diffs.some(([op]) => op === DIFF_INSERT);
249
+ const equal = !hasDelete && !hasInsert;
250
+ const onlyDeletion = hasDelete && !hasInsert;
251
+ const onlyInsertion = !hasDelete && hasInsert;
252
+ const replacement = hasDelete && hasInsert;
253
+
254
+ let summary: string;
255
+ if (equal) {
256
+ summary = '无变更';
257
+ } else if (onlyDeletion) {
258
+ const deleted = diffs
259
+ .filter(([op]) => op === DIFF_DELETE)
260
+ .map(([, text]) => text)
261
+ .join('');
262
+ summary = `删除: ${formatSnippet(deleted)}`;
263
+ } else if (onlyInsertion) {
264
+ const inserted = diffs
265
+ .filter(([op]) => op === DIFF_INSERT)
266
+ .map(([, text]) => text)
267
+ .join('');
268
+ summary = `新增: ${formatSnippet(inserted)}`;
269
+ } else {
270
+ const deleted = diffs
271
+ .filter(([op]) => op === DIFF_DELETE)
272
+ .map(([, text]) => text)
273
+ .join('');
274
+ const inserted = diffs
275
+ .filter(([op]) => op === DIFF_INSERT)
276
+ .map(([, text]) => text)
277
+ .join('');
278
+ summary = `替换: ${formatSnippet(deleted)} → ${formatSnippet(inserted)}`;
279
+ }
280
+
281
+ return {
282
+ originalFragment,
283
+ finalFragment,
284
+ equal,
285
+ onlyDeletion,
286
+ onlyInsertion,
287
+ replacement,
288
+ summary,
289
+ diffs,
290
+ };
291
+ }
292
+
293
+ /** 片段截断展示,避免过长 */
294
+ function formatSnippet(s: string, maxLen = 40): string {
295
+ const t = s.replace(/\s+/g, ' ').trim();
296
+ if (t.length <= maxLen) return t;
297
+ return `${t.slice(0, maxLen)}…`;
298
+ }
299
+
300
+ /**
301
+ * 根据当前内容和 range,找到下一个内容对应的 range
302
+ * 用于将 currentContent 中的 range 映射到 nextContent 中对应的 range
303
+ *
304
+ * @param options.currentContent 当前文件内容
305
+ * @param options.nextContent 下一个文件内容
306
+ * @param options.currentRange 当前文件中需要映射的 range
307
+ * @returns 映射得到的 nextRange、对应片段、以及片段级变更分析;若无法解析则返回 undefined
308
+ */
309
+ export function resolveGroupChangeFragments(options: {
310
+ currentContent: string;
311
+ nextContent: string;
312
+ currentRange: Range;
313
+ }):
314
+ | {
315
+ nextRange: Range;
316
+ currentFragment: string;
317
+ nextFragment: string;
318
+ changeAnalysis: FragmentChangeAnalysis;
319
+ }
320
+ | undefined {
321
+ const { currentContent, nextContent, currentRange } = options;
322
+
323
+ const { start: startOffset, end: endOffset } = currentRange;
324
+
325
+ if (
326
+ startOffset < 0 ||
327
+ endOffset < 0 ||
328
+ startOffset > endOffset ||
329
+ endOffset > currentContent.length
330
+ ) {
331
+ return undefined;
332
+ }
333
+
334
+ const nextRange = mapOlderRangeToNewer(
335
+ currentContent,
336
+ nextContent,
337
+ startOffset,
338
+ endOffset
339
+ );
340
+
341
+ const currentFragment = currentContent.substring(startOffset, endOffset);
342
+ const nextFragment = nextContent.substring(nextRange.start, nextRange.end);
343
+
344
+ const changeAnalysis = analyzeFragmentChange(currentFragment, nextFragment);
345
+
346
+ return {
347
+ nextRange,
348
+ currentFragment,
349
+ nextFragment,
350
+ changeAnalysis,
351
+ };
352
+ }