Skip to content

Commit b866b29

Browse files
authored
1 parent e30b235 commit b866b29

File tree

11 files changed

+274
-52
lines changed

11 files changed

+274
-52
lines changed

src/vs/editor/common/diff/algorithms/diffAlgorithm.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ export class SequenceDiff {
4949
public join(other: SequenceDiff): SequenceDiff {
5050
return new SequenceDiff(this.seq1Range.join(other.seq1Range), this.seq2Range.join(other.seq2Range));
5151
}
52+
53+
public delta(offset: number): SequenceDiff {
54+
if (offset === 0) {
55+
return this;
56+
}
57+
return new SequenceDiff(this.seq1Range.delta(offset), this.seq2Range.delta(offset));
58+
}
5259
}
5360

5461
export interface ISequence {

src/vs/editor/common/diff/algorithms/joinSequenceDiffs.ts

Lines changed: 80 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -50,34 +50,78 @@ export function joinSequenceDiffs(sequence1: ISequence, sequence2: ISequence, se
5050
result.push(sequenceDiffs[0]);
5151
}
5252

53+
// First move them all to the left as much as possible and join them if possible
5354
for (let i = 1; i < sequenceDiffs.length; i++) {
54-
const lastResult = result[result.length - 1];
55-
const cur = sequenceDiffs[i];
56-
57-
if (cur.seq1Range.isEmpty) {
58-
let all = true;
59-
const length = cur.seq1Range.start - lastResult.seq1Range.endExclusive;
60-
for (let i = 1; i <= length; i++) {
61-
if (sequence2.getElement(cur.seq2Range.start - i) !== sequence2.getElement(cur.seq2Range.endExclusive - i)) {
62-
all = false;
55+
const prevResult = sequenceDiffs[i - 1];
56+
let cur = sequenceDiffs[i];
57+
58+
if (cur.seq1Range.isEmpty || cur.seq2Range.isEmpty) {
59+
const length = cur.seq1Range.start - prevResult.seq1Range.endExclusive;
60+
let d;
61+
for (d = 1; d <= length; d++) {
62+
if (
63+
sequence1.getElement(cur.seq1Range.start - d) !== sequence1.getElement(cur.seq1Range.endExclusive - d) ||
64+
sequence2.getElement(cur.seq2Range.start - d) !== sequence2.getElement(cur.seq2Range.endExclusive - d)) {
6365
break;
6466
}
6567
}
68+
d--;
6669

67-
if (all) {
70+
if (d === length) {
6871
// Merge previous and current diff
69-
result[result.length - 1] = new SequenceDiff(lastResult.seq1Range, new OffsetRange(
70-
lastResult.seq2Range.start,
71-
cur.seq2Range.endExclusive - length
72-
));
72+
result[result.length - 1] = new SequenceDiff(
73+
new OffsetRange(prevResult.seq1Range.start, cur.seq1Range.endExclusive - length),
74+
new OffsetRange(prevResult.seq2Range.start, cur.seq2Range.endExclusive - length),
75+
);
7376
continue;
7477
}
78+
79+
cur = cur.delta(-d);
7580
}
7681

7782
result.push(cur);
7883
}
7984

80-
return result;
85+
const result2: SequenceDiff[] = [];
86+
// Then move them all to the right and join them again if possible
87+
for (let i = 0; i < result.length - 1; i++) {
88+
const nextResult = result[i + 1];
89+
let cur = result[i];
90+
91+
if (cur.seq1Range.isEmpty || cur.seq2Range.isEmpty) {
92+
const length = nextResult.seq1Range.start - cur.seq1Range.endExclusive;
93+
let d;
94+
for (d = 0; d < length; d++) {
95+
if (
96+
sequence1.getElement(cur.seq1Range.start + d) !== sequence1.getElement(cur.seq1Range.endExclusive + d) ||
97+
sequence2.getElement(cur.seq2Range.start + d) !== sequence2.getElement(cur.seq2Range.endExclusive + d)
98+
) {
99+
break;
100+
}
101+
}
102+
103+
if (d === length) {
104+
// Merge previous and current diff, write to result!
105+
result[i + 1] = new SequenceDiff(
106+
new OffsetRange(cur.seq1Range.start + length, nextResult.seq1Range.endExclusive),
107+
new OffsetRange(cur.seq2Range.start + length, nextResult.seq2Range.endExclusive),
108+
);
109+
continue;
110+
}
111+
112+
if (d > 0) {
113+
cur = cur.delta(d);
114+
}
115+
}
116+
117+
result2.push(cur);
118+
}
119+
120+
if (result.length > 0) {
121+
result2.push(result[result.length - 1]);
122+
}
123+
124+
return result2;
81125
}
82126

83127
// align character level diffs at whitespace characters
@@ -102,37 +146,45 @@ export function shiftSequenceDiffs(sequence1: ISequence, sequence2: ISequence, s
102146
}
103147

104148
for (let i = 0; i < sequenceDiffs.length; i++) {
149+
const prevDiff = (i > 0 ? sequenceDiffs[i - 1] : undefined);
105150
const diff = sequenceDiffs[i];
151+
const nextDiff = (i + 1 < sequenceDiffs.length ? sequenceDiffs[i + 1] : undefined);
152+
153+
const seq1ValidRange = new OffsetRange(prevDiff ? prevDiff.seq1Range.start + 1 : 0, nextDiff ? nextDiff.seq1Range.endExclusive - 1 : sequence1.length);
154+
const seq2ValidRange = new OffsetRange(prevDiff ? prevDiff.seq2Range.start + 1 : 0, nextDiff ? nextDiff.seq2Range.endExclusive - 1 : sequence2.length);
155+
106156
if (diff.seq1Range.isEmpty) {
107-
const seq2PrevEndExclusive = (i > 0 ? sequenceDiffs[i - 1].seq2Range.endExclusive : -1);
108-
const seq2NextStart = (i + 1 < sequenceDiffs.length ? sequenceDiffs[i + 1].seq2Range.start : sequence2.length);
109-
sequenceDiffs[i] = shiftDiffToBetterPosition(diff, sequence1, sequence2, seq2NextStart, seq2PrevEndExclusive);
157+
sequenceDiffs[i] = shiftDiffToBetterPosition(diff, sequence1, sequence2, seq1ValidRange, seq2ValidRange);
110158
} else if (diff.seq2Range.isEmpty) {
111-
const seq1PrevEndExclusive = (i > 0 ? sequenceDiffs[i - 1].seq1Range.endExclusive : -1);
112-
const seq1NextStart = (i + 1 < sequenceDiffs.length ? sequenceDiffs[i + 1].seq1Range.start : sequence1.length);
113-
sequenceDiffs[i] = shiftDiffToBetterPosition(diff.reverse(), sequence2, sequence1, seq1NextStart, seq1PrevEndExclusive).reverse();
159+
sequenceDiffs[i] = shiftDiffToBetterPosition(diff.reverse(), sequence2, sequence1, seq2ValidRange, seq1ValidRange).reverse();
114160
}
115161
}
116162

117163
return sequenceDiffs;
118164
}
119165

120-
function shiftDiffToBetterPosition(diff: SequenceDiff, sequence1: ISequence, sequence2: ISequence, seq2NextStart: number, seq2PrevEndExclusive: number) {
121-
const maxShiftLimit = 20; // To prevent performance issues
166+
function shiftDiffToBetterPosition(diff: SequenceDiff, sequence1: ISequence, sequence2: ISequence, seq1ValidRange: OffsetRange, seq2ValidRange: OffsetRange,) {
167+
const maxShiftLimit = 100; // To prevent performance issues
122168

123169
// don't touch previous or next!
124170
let deltaBefore = 1;
125-
while (diff.seq2Range.start - deltaBefore > seq2PrevEndExclusive &&
171+
while (
172+
diff.seq1Range.start - deltaBefore >= seq1ValidRange.start &&
173+
diff.seq2Range.start - deltaBefore >= seq2ValidRange.start &&
126174
sequence2.getElement(diff.seq2Range.start - deltaBefore) ===
127-
sequence2.getElement(diff.seq2Range.endExclusive - deltaBefore) && deltaBefore < maxShiftLimit) {
175+
sequence2.getElement(diff.seq2Range.endExclusive - deltaBefore) && deltaBefore < maxShiftLimit
176+
) {
128177
deltaBefore++;
129178
}
130179
deltaBefore--;
131180

132181
let deltaAfter = 0;
133-
while (diff.seq2Range.start + deltaAfter < seq2NextStart &&
182+
while (
183+
diff.seq1Range.start + deltaAfter < seq1ValidRange.endExclusive &&
184+
diff.seq2Range.endExclusive + deltaAfter < seq2ValidRange.endExclusive &&
134185
sequence2.getElement(diff.seq2Range.start + deltaAfter) ===
135-
sequence2.getElement(diff.seq2Range.endExclusive + deltaAfter) && deltaAfter < maxShiftLimit) {
186+
sequence2.getElement(diff.seq2Range.endExclusive + deltaAfter) && deltaAfter < maxShiftLimit
187+
) {
136188
deltaAfter++;
137189
}
138190

@@ -158,8 +210,5 @@ function shiftDiffToBetterPosition(diff: SequenceDiff, sequence1: ISequence, seq
158210
}
159211
}
160212

161-
if (bestDelta !== 0) {
162-
return new SequenceDiff(diff.seq1Range.delta(bestDelta), diff.seq2Range.delta(bestDelta));
163-
}
164-
return diff;
213+
return diff.delta(bestDelta);
165214
}

src/vs/editor/common/diff/standardLinesDiffComputer.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -154,23 +154,23 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer {
154154
}
155155

156156
private refineDiff(originalLines: string[], modifiedLines: string[], diff: SequenceDiff, timeout: ITimeout, considerWhitespaceChanges: boolean): { mappings: RangeMapping[]; hitTimeout: boolean } {
157-
const sourceSlice = new Slice(originalLines, diff.seq1Range, considerWhitespaceChanges);
158-
const targetSlice = new Slice(modifiedLines, diff.seq2Range, considerWhitespaceChanges);
157+
const slice1 = new Slice(originalLines, diff.seq1Range, considerWhitespaceChanges);
158+
const slice2 = new Slice(modifiedLines, diff.seq2Range, considerWhitespaceChanges);
159159

160-
const diffResult = sourceSlice.length + targetSlice.length < 500
161-
? this.dynamicProgrammingDiffing.compute(sourceSlice, targetSlice, timeout)
162-
: this.myersDiffingAlgorithm.compute(sourceSlice, targetSlice, timeout);
160+
const diffResult = slice1.length + slice2.length < 500
161+
? this.dynamicProgrammingDiffing.compute(slice1, slice2, timeout)
162+
: this.myersDiffingAlgorithm.compute(slice1, slice2, timeout);
163163

164164
let diffs = diffResult.diffs;
165-
diffs = optimizeSequenceDiffs(sourceSlice, targetSlice, diffs);
166-
diffs = coverFullWords(sourceSlice, targetSlice, diffs);
167-
diffs = smoothenSequenceDiffs(sourceSlice, targetSlice, diffs);
165+
diffs = optimizeSequenceDiffs(slice1, slice2, diffs);
166+
diffs = coverFullWords(slice1, slice2, diffs);
167+
diffs = smoothenSequenceDiffs(slice1, slice2, diffs);
168168

169169
const result = diffs.map(
170170
(d) =>
171171
new RangeMapping(
172-
sourceSlice.translateRange(d.seq1Range),
173-
targetSlice.translateRange(d.seq2Range)
172+
slice1.translateRange(d.seq1Range),
173+
slice2.translateRange(d.seq2Range)
174174
)
175175
);
176176

@@ -297,6 +297,9 @@ export function lineRangeMappingFromRangeMappings(alignments: RangeMapping[], or
297297
}
298298

299299
assertFn(() => {
300+
if (changes.length > 0 && changes[0].originalRange.startLineNumber !== changes[0].modifiedRange.startLineNumber) {
301+
return false;
302+
}
300303
return checkAdjacentItems(changes,
301304
(m1, m2) => m2.originalRange.startLineNumber - m1.originalRange.endLineNumberExclusive === m2.modifiedRange.startLineNumber - m1.modifiedRange.endLineNumberExclusive &&
302305
// There has to be an unchanged line in between (otherwise both diffs should have been joined)

src/vs/editor/test/node/diffing/diffingFixture.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,22 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as assert from 'assert';
7-
import { readdirSync, readFileSync, existsSync, writeFileSync, rmSync } from 'fs';
7+
import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';
88
import { join, resolve } from 'path';
9+
import { setUnexpectedErrorHandler } from 'vs/base/common/errors';
910
import { FileAccess } from 'vs/base/common/network';
1011
import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
1112
import { SmartLinesDiffComputer } from 'vs/editor/common/diff/smartLinesDiffComputer';
1213
import { StandardLinesDiffComputer } from 'vs/editor/common/diff/standardLinesDiffComputer';
1314

1415
suite('diff fixtures', () => {
16+
setup(() => {
17+
setUnexpectedErrorHandler(e => {
18+
throw e;
19+
});
20+
});
21+
22+
1523
const fixturesOutDir = FileAccess.asFileUri('vs/editor/test/node/diffing/fixtures').fsPath;
1624
// We want the dir in src, so we can directly update the source files if they disagree and create invalid files to capture the previous state.
1725
// This makes it very easy to update the fixtures.
@@ -104,7 +112,7 @@ suite('diff fixtures', () => {
104112
}
105113

106114
test(`test`, () => {
107-
runTest('move-1', 'advanced');
115+
runTest('issue-185779', 'advanced');
108116
});
109117

110118
for (const folder of folders) {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
2+
private doAddView(view: IView<TLayoutContext>, size: number | Sizing, index = this.viewItems.length, skipLayout?: boolean): void {
3+
if (this.state !== State.Idle) {
4+
throw new Error('Cant modify splitview');
5+
}
6+
7+
this.state = State.Busy;
8+
9+
// Add view
10+
const container = $('.split-view-view');
11+
12+
if (index === this.viewItems.length) {
13+
this.viewContainer.appendChild(container);
14+
} else {
15+
this.viewContainer.insertBefore(container, this.viewContainer.children.item(index));
16+
}
17+
18+
const onChangeDisposable = view.onDidChange(size => this.onViewChange(item, size));
19+
const containerDisposable = toDisposable(() => this.viewContainer.removeChild(container));
20+
const disposable = combinedDisposable(onChangeDisposable, containerDisposable);
21+
22+
let viewSize: ViewItemSize;
23+
24+
if (typeof size === 'number') {
25+
viewSize = size;
26+
} else if (size.type === 'split') {
27+
viewSize = this.getViewSize(size.index) / 2;
28+
} else if (size.type === 'invisible') {
29+
viewSize = { cachedVisibleSize: size.cachedVisibleSize };
30+
} else {
31+
viewSize = view.minimumSize;
32+
}
33+
34+
const item = this.orientation === Orientation.VERTICAL
35+
? new VerticalViewItem(container, view, viewSize, disposable)
36+
: new HorizontalViewItem(container, view, viewSize, disposable);
37+
38+
this.viewItems.splice(index, 0, item);
39+
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
2+
private doAddView(view: IView<TLayoutContext>, size: number | Sizing, index = this.viewItems.length, skipLayout?: boolean): void {
3+
if (this.state !== State.Idle) {
4+
throw new Error('Cant modify splitview');
5+
}
6+
7+
this.state = State.Busy;
8+
9+
// Add view
10+
const container = $('.split-view-view');
11+
12+
if (index === this.viewItems.length) {
13+
this.viewContainer.appendChild(container);
14+
} else {
15+
this.viewContainer.insertBefore(container, this.viewContainer.children.item(index));
16+
}
17+
18+
const onChangeDisposable = view.onDidChange(size => this.onViewChange(item, size));
19+
const containerDisposable = toDisposable(() => this.viewContainer.removeChild(container));
20+
const disposable = combinedDisposable(onChangeDisposable, containerDisposable);
21+
22+
let viewSize: ViewItemSize;
23+
24+
if (typeof size === 'number') {
25+
viewSize = size;
26+
} else {
27+
if (size.type === 'auto') {
28+
if (this.areViewsDistributed()) {
29+
size = { type: 'distribute' };
30+
} else {
31+
size = { type: 'split', index: size.index };
32+
}
33+
}
34+
35+
if (size.type === 'split') {
36+
viewSize = this.getViewSize(size.index) / 2;
37+
} else if (size.type === 'invisible') {
38+
viewSize = { cachedVisibleSize: size.cachedVisibleSize };
39+
} else {
40+
viewSize = view.minimumSize;
41+
}
42+
}
43+
44+
const item = this.orientation === Orientation.VERTICAL
45+
? new VerticalViewItem(container, view, viewSize, disposable)
46+
: new HorizontalViewItem(container, view, viewSize, disposable);
47+
48+
this.viewItems.splice(index, 0, item);
49+
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"original": {
3+
"content": "\n\tprivate doAddView(view: IView<TLayoutContext>, size: number | Sizing, index = this.viewItems.length, skipLayout?: boolean): void {\n\t\tif (this.state !== State.Idle) {\n\t\t\tthrow new Error('Cant modify splitview');\n\t\t}\n\n\t\tthis.state = State.Busy;\n\n\t\t// Add view\n\t\tconst container = $('.split-view-view');\n\n\t\tif (index === this.viewItems.length) {\n\t\t\tthis.viewContainer.appendChild(container);\n\t\t} else {\n\t\t\tthis.viewContainer.insertBefore(container, this.viewContainer.children.item(index));\n\t\t}\n\n\t\tconst onChangeDisposable = view.onDidChange(size => this.onViewChange(item, size));\n\t\tconst containerDisposable = toDisposable(() => this.viewContainer.removeChild(container));\n\t\tconst disposable = combinedDisposable(onChangeDisposable, containerDisposable);\n\n\t\tlet viewSize: ViewItemSize;\n\n\t\tif (typeof size === 'number') {\n\t\t\tviewSize = size;\n\t\t} else if (size.type === 'split') {\n\t\t\tviewSize = this.getViewSize(size.index) / 2;\n\t\t} else if (size.type === 'invisible') {\n\t\t\tviewSize = { cachedVisibleSize: size.cachedVisibleSize };\n\t\t} else {\n\t\t\tviewSize = view.minimumSize;\n\t\t}\n\n\t\tconst item = this.orientation === Orientation.VERTICAL\n\t\t\t? new VerticalViewItem(container, view, viewSize, disposable)\n\t\t\t: new HorizontalViewItem(container, view, viewSize, disposable);\n\n\t\tthis.viewItems.splice(index, 0, item);\n\n",
4+
"fileName": "./1.txt"
5+
},
6+
"modified": {
7+
"content": "\n\tprivate doAddView(view: IView<TLayoutContext>, size: number | Sizing, index = this.viewItems.length, skipLayout?: boolean): void {\n\t\tif (this.state !== State.Idle) {\n\t\t\tthrow new Error('Cant modify splitview');\n\t\t}\n\n\t\tthis.state = State.Busy;\n\n\t\t// Add view\n\t\tconst container = $('.split-view-view');\n\n\t\tif (index === this.viewItems.length) {\n\t\t\tthis.viewContainer.appendChild(container);\n\t\t} else {\n\t\t\tthis.viewContainer.insertBefore(container, this.viewContainer.children.item(index));\n\t\t}\n\n\t\tconst onChangeDisposable = view.onDidChange(size => this.onViewChange(item, size));\n\t\tconst containerDisposable = toDisposable(() => this.viewContainer.removeChild(container));\n\t\tconst disposable = combinedDisposable(onChangeDisposable, containerDisposable);\n\n\t\tlet viewSize: ViewItemSize;\n\n\t\tif (typeof size === 'number') {\n\t\t\tviewSize = size;\n\t\t} else {\n\t\t\tif (size.type === 'auto') {\n\t\t\t\tif (this.areViewsDistributed()) {\n\t\t\t\t\tsize = { type: 'distribute' };\n\t\t\t\t} else {\n\t\t\t\t\tsize = { type: 'split', index: size.index };\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (size.type === 'split') {\n\t\t\t\tviewSize = this.getViewSize(size.index) / 2;\n\t\t\t} else if (size.type === 'invisible') {\n\t\t\t\tviewSize = { cachedVisibleSize: size.cachedVisibleSize };\n\t\t\t} else {\n\t\t\t\tviewSize = view.minimumSize;\n\t\t\t}\n\t\t}\n\n\t\tconst item = this.orientation === Orientation.VERTICAL\n\t\t\t? new VerticalViewItem(container, view, viewSize, disposable)\n\t\t\t: new HorizontalViewItem(container, view, viewSize, disposable);\n\n\t\tthis.viewItems.splice(index, 0, item);\n\n",
8+
"fileName": "./2.txt"
9+
},
10+
"diffs": [
11+
{
12+
"originalRange": "[26,33)",
13+
"modifiedRange": "[26,43)",
14+
"innerChanges": [
15+
{
16+
"originalRange": "[26,10 -> 26,10]",
17+
"modifiedRange": "[26,10 -> 35,4]"
18+
},
19+
{
20+
"originalRange": "[27,1 -> 27,1]",
21+
"modifiedRange": "[36,1 -> 36,2]"
22+
},
23+
{
24+
"originalRange": "[28,1 -> 28,1]",
25+
"modifiedRange": "[37,1 -> 37,2]"
26+
},
27+
{
28+
"originalRange": "[29,1 -> 29,1]",
29+
"modifiedRange": "[38,1 -> 38,2]"
30+
},
31+
{
32+
"originalRange": "[30,1 -> 30,1]",
33+
"modifiedRange": "[39,1 -> 39,2]"
34+
},
35+
{
36+
"originalRange": "[31,1 -> 31,1]",
37+
"modifiedRange": "[40,1 -> 40,2]"
38+
},
39+
{
40+
"originalRange": "[32,1 -> 32,1]",
41+
"modifiedRange": "[41,1 -> 41,2]"
42+
},
43+
{
44+
"originalRange": "[33,1 -> 33,1]",
45+
"modifiedRange": "[42,1 -> 43,1]"
46+
}
47+
]
48+
}
49+
]
50+
}

0 commit comments

Comments
 (0)