Skip to content

Commit bf787b0

Browse files
authored
Merge pull request microsoft#152254 from microsoft/3wm
2 parents 6f7c824 + 37e70da commit bf787b0

File tree

9 files changed

+631
-323
lines changed

9 files changed

+631
-323
lines changed

src/vs/base/common/arrays.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,18 @@ export function compareBy<TItem, TCompareBy>(selector: (item: TItem) => TCompare
677677
return (a, b) => comparator(selector(a), selector(b));
678678
}
679679

680+
export function tieBreakComparators<TItem>(...comparators: Comparator<TItem>[]): Comparator<TItem> {
681+
return (item1, item2) => {
682+
for (const comparator of comparators) {
683+
const result = comparator(item1, item2);
684+
if (!CompareResult.isNeitherLessOrGreaterThan(result)) {
685+
return result;
686+
}
687+
}
688+
return CompareResult.neitherLessOrGreaterThan;
689+
};
690+
}
691+
680692
/**
681693
* The natural order on numbers.
682694
*/

src/vs/workbench/contrib/mergeEditor/browser/colors.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,34 @@ export const diffInput1 = registerColor(
6262
)
6363
);
6464

65+
export const diff = registerColor(
66+
'mergeEditor.diff',
67+
{
68+
dark: '#d3d3d321',
69+
light: '#d3d3d321',
70+
hcDark: '#d3d3d321',
71+
hcLight: '#d3d3d321',
72+
},
73+
localize(
74+
'mergeEditor.diff',
75+
'The foreground color for changes in the result.'
76+
)
77+
);
78+
79+
export const diffWord = registerColor(
80+
'mergeEditor.diff.word',
81+
{
82+
dark: '#e571db21',
83+
light: '#e571db21',
84+
hcDark: '#e571db21',
85+
hcLight: '#e571db21',
86+
},
87+
localize(
88+
'mergeEditor.diff.word',
89+
'The foreground color for word changes in the result.'
90+
)
91+
);
92+
6593
export const diffInput2 = registerColor(
6694
'mergeEditor.diff.input2',
6795
{
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { Range } from 'vs/editor/common/core/range';
7+
import { ICharChange, IDiffComputationResult, ILineChange } from 'vs/editor/common/diff/diffComputer';
8+
import { ITextModel } from 'vs/editor/common/model';
9+
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
10+
import { LineRange, LineRangeMapping, RangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model';
11+
12+
export interface IDiffComputer {
13+
computeDiff(textModel1: ITextModel, textModel2: ITextModel): Promise<IDiffComputerResult>;
14+
}
15+
16+
export interface IDiffComputerResult {
17+
diffs: LineRangeMapping[] | null;
18+
}
19+
20+
export class EditorWorkerServiceDiffComputer implements IDiffComputer {
21+
constructor(@IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService) { }
22+
23+
async computeDiff(textModel1: ITextModel, textModel2: ITextModel): Promise<IDiffComputerResult> {
24+
const diffs = await this.editorWorkerService.computeDiff(textModel1.uri, textModel2.uri, false, 1000);
25+
if (!diffs || diffs.quitEarly) {
26+
return { diffs: null };
27+
}
28+
return { diffs: EditorWorkerServiceDiffComputer.fromDiffComputationResult(diffs, textModel1, textModel2) };
29+
}
30+
31+
public static fromDiffComputationResult(result: IDiffComputationResult, textModel1: ITextModel, textModel2: ITextModel): LineRangeMapping[] {
32+
return result.changes.map((c) => fromLineChange(c, textModel1, textModel2));
33+
}
34+
}
35+
36+
function fromLineChange(lineChange: ILineChange, originalTextModel: ITextModel, modifiedTextModel: ITextModel): LineRangeMapping {
37+
let originalRange: LineRange;
38+
if (lineChange.originalEndLineNumber === 0) {
39+
// Insertion
40+
originalRange = new LineRange(lineChange.originalStartLineNumber + 1, 0);
41+
} else {
42+
originalRange = new LineRange(lineChange.originalStartLineNumber, lineChange.originalEndLineNumber - lineChange.originalStartLineNumber + 1);
43+
}
44+
45+
let modifiedRange: LineRange;
46+
if (lineChange.modifiedEndLineNumber === 0) {
47+
// Deletion
48+
modifiedRange = new LineRange(lineChange.modifiedStartLineNumber + 1, 0);
49+
} else {
50+
modifiedRange = new LineRange(lineChange.modifiedStartLineNumber, lineChange.modifiedEndLineNumber - lineChange.modifiedStartLineNumber + 1);
51+
}
52+
53+
let innerDiffs = lineChange.charChanges?.map(c => rangeMappingFromCharChange(c));
54+
if (!innerDiffs) {
55+
innerDiffs = [rangeMappingFromLineRanges(originalRange, modifiedRange)];
56+
}
57+
58+
return new LineRangeMapping(
59+
originalTextModel,
60+
originalRange,
61+
modifiedTextModel,
62+
modifiedRange,
63+
innerDiffs
64+
);
65+
}
66+
67+
function rangeMappingFromLineRanges(originalRange: LineRange, modifiedRange: LineRange): RangeMapping {
68+
return new RangeMapping(
69+
new Range(
70+
originalRange.startLineNumber,
71+
1,
72+
originalRange.endLineNumberExclusive,
73+
1,
74+
),
75+
new Range(
76+
modifiedRange.startLineNumber,
77+
1,
78+
modifiedRange.endLineNumberExclusive,
79+
1,
80+
)
81+
);
82+
}
83+
84+
function rangeMappingFromCharChange(charChange: ICharChange): RangeMapping {
85+
return new RangeMapping(
86+
new Range(charChange.originalStartLineNumber, charChange.originalStartColumn, charChange.originalEndLineNumber, charChange.originalEndColumn),
87+
new Range(charChange.modifiedStartLineNumber, charChange.modifiedStartColumn, charChange.modifiedEndLineNumber, charChange.modifiedEndColumn)
88+
);
89+
}

src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@
5454
background-color: var(--vscode-mergeEditor-modifiedBaseRange-combination);
5555
}
5656

57+
.merge-editor-modified-base-range {
58+
background-color: var(--vscode-mergeEditor-diff);
59+
}
60+
5761
.gutter-item {
5862
position: absolute;
5963
}

src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts

Lines changed: 77 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel';
99
import { Orientation, Sizing } from 'vs/base/browser/ui/splitview/splitview';
1010
import { Toggle } from 'vs/base/browser/ui/toggle/toggle';
1111
import { IAction } from 'vs/base/common/actions';
12-
import { CompareResult, findLast } from 'vs/base/common/arrays';
12+
import { CompareResult } from 'vs/base/common/arrays';
1313
import { CancellationToken } from 'vs/base/common/cancellation';
1414
import { Codicon } from 'vs/base/common/codicons';
1515
import { Color } from 'vs/base/common/color';
@@ -47,7 +47,7 @@ import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions
4747
import { autorun, autorunWithStore, derivedObservable, IObservable, ITransaction, keepAlive, ObservableValue, transaction } from 'vs/workbench/contrib/audioCues/browser/observable';
4848
import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput';
4949
import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorModel';
50-
import { LineRange, ModifiedBaseRange } from 'vs/workbench/contrib/mergeEditor/browser/model';
50+
import { DocumentMapping, LineRange, SimpleLineRangeMapping, ToggleState } from 'vs/workbench/contrib/mergeEditor/browser/model';
5151
import { applyObservableDecorations, join, n, ReentrancyBarrier, setStyle } from 'vs/workbench/contrib/mergeEditor/browser/utils';
5252
import { settingsSashBorder } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry';
5353
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
@@ -109,17 +109,39 @@ export class MergeEditor extends AbstractTextEditor<any> {
109109
return undefined;
110110
}
111111
const resultDiffs = model.resultDiffs.read(reader);
112-
const modifiedBaseRanges = ModifiedBaseRange.fromDiffs(model.base, model.input1, model.input1LinesDiffs.read(reader), model.result, resultDiffs);
113-
return modifiedBaseRanges;
112+
const modifiedBaseRanges = DocumentMapping.fromDiffs(model.input1LinesDiffs.read(reader), resultDiffs, model.input1.getLineCount());
113+
114+
return new DocumentMapping(
115+
modifiedBaseRanges.lineRangeMappings.map((m) =>
116+
m.inputRange.isEmpty || m.outputRange.isEmpty
117+
? new SimpleLineRangeMapping(
118+
m.inputRange.deltaStart(-1),
119+
m.outputRange.deltaStart(-1)
120+
)
121+
: m
122+
),
123+
modifiedBaseRanges.inputLineCount
124+
);
114125
});
115126
const input2ResultMapping = derivedObservable('input2ResultMapping', reader => {
116127
const model = this.input2View.model.read(reader);
117128
if (!model) {
118129
return undefined;
119130
}
120131
const resultDiffs = model.resultDiffs.read(reader);
121-
const modifiedBaseRanges = ModifiedBaseRange.fromDiffs(model.base, model.input2, model.input2LinesDiffs.read(reader), model.result, resultDiffs);
122-
return modifiedBaseRanges;
132+
const modifiedBaseRanges = DocumentMapping.fromDiffs(model.input2LinesDiffs.read(reader), resultDiffs, model.input2.getLineCount());
133+
134+
return new DocumentMapping(
135+
modifiedBaseRanges.lineRangeMappings.map((m) =>
136+
m.inputRange.isEmpty || m.outputRange.isEmpty
137+
? new SimpleLineRangeMapping(
138+
m.inputRange.deltaStart(-1),
139+
m.outputRange.deltaStart(-1)
140+
)
141+
: m
142+
),
143+
modifiedBaseRanges.inputLineCount
144+
);
123145
});
124146

125147
this._register(keepAlive(input1ResultMapping));
@@ -390,7 +412,7 @@ export class MergeEditor extends AbstractTextEditor<any> {
390412
}
391413
}
392414

393-
function synchronizeScrolling(scrollingEditor: CodeEditorWidget, targetEditor: CodeEditorWidget, mapping: ModifiedBaseRange[] | undefined, sourceNumber: 1 | 2) {
415+
function synchronizeScrolling(scrollingEditor: CodeEditorWidget, targetEditor: CodeEditorWidget, mapping: DocumentMapping | undefined, sourceNumber: 1 | 2) {
394416
if (!mapping) {
395417
return;
396418
}
@@ -401,25 +423,27 @@ function synchronizeScrolling(scrollingEditor: CodeEditorWidget, targetEditor: C
401423
}
402424
const topLineNumber = visibleRanges[0].startLineNumber - 1;
403425

404-
const firstBefore = findLast(mapping, r => r.getInputRange(sourceNumber).startLineNumber <= topLineNumber);
405426
let sourceRange: LineRange;
406427
let targetRange: LineRange;
407428

408-
const targetNumber = sourceNumber === 1 ? 2 : 1;
409-
410-
const firstBeforeSourceRange = firstBefore?.getInputRange(sourceNumber);
411-
const firstBeforeTargetRange = firstBefore?.getInputRange(targetNumber);
412-
413-
if (firstBeforeSourceRange && firstBeforeSourceRange.contains(topLineNumber)) {
414-
sourceRange = firstBeforeSourceRange;
415-
targetRange = firstBeforeTargetRange!;
416-
} else if (firstBeforeSourceRange && firstBeforeSourceRange.isEmpty && firstBeforeSourceRange.startLineNumber === topLineNumber) {
417-
sourceRange = firstBeforeSourceRange.deltaEnd(1);
418-
targetRange = firstBeforeTargetRange!.deltaEnd(1);
429+
if (sourceNumber === 1) {
430+
const number = mapping.getOutputLine(topLineNumber);
431+
if (typeof number === 'number') {
432+
sourceRange = new LineRange(topLineNumber, 1);
433+
targetRange = new LineRange(number, 1);
434+
} else {
435+
sourceRange = number.inputRange;
436+
targetRange = number.outputRange;
437+
}
419438
} else {
420-
const delta = firstBeforeSourceRange ? firstBeforeTargetRange!.endLineNumberExclusive - firstBeforeSourceRange.endLineNumberExclusive : 0;
421-
sourceRange = new LineRange(topLineNumber, 1);
422-
targetRange = new LineRange(topLineNumber + delta, 1);
439+
const number = mapping.getInputLine(topLineNumber);
440+
if (typeof number === 'number') {
441+
sourceRange = new LineRange(topLineNumber, 1);
442+
targetRange = new LineRange(number, 1);
443+
} else {
444+
sourceRange = number.outputRange;
445+
targetRange = number.inputRange;
446+
}
423447
}
424448

425449
// sourceRange contains topLineNumber!
@@ -532,6 +556,21 @@ class InputCodeEditorView extends CodeEditorView {
532556
description: 'Base Range Projection'
533557
}
534558
});
559+
560+
const inputDiffs = m.getInputDiffs(this.inputNumber);
561+
for (const diff of inputDiffs) {
562+
if (diff.innerRangeMappings) {
563+
for (const d of diff.innerRangeMappings) {
564+
result.push({
565+
range: d.outputRange,
566+
options: {
567+
className: `merge-editor-diff-input${this.inputNumber}`,
568+
description: 'Base Range Projection'
569+
}
570+
});
571+
}
572+
}
573+
}
535574
}
536575
}
537576
return result;
@@ -585,7 +624,7 @@ class InputCodeEditorView extends CodeEditorView {
585624

586625
interface ModifiedBaseRangeGutterItemInfo extends IGutterItemInfo {
587626
enabled: IObservable<boolean>;
588-
toggleState: IObservable<boolean | undefined>;
627+
toggleState: IObservable<ToggleState>;
589628
setState(value: boolean, tx: ITransaction): void;
590629
}
591630

@@ -606,14 +645,14 @@ class MergeConflictGutterItemView extends Disposable implements IGutterItemView<
606645
autorun((reader) => {
607646
const item = this.item.read(reader)!;
608647
const value = item.toggleState.read(reader);
609-
checkBox.setIcon(
610-
value === true
611-
? Codicon.check
612-
: value === false
613-
? undefined
614-
: Codicon.circleFilled
615-
);
616-
checkBox.checked = value === true;
648+
const iconMap: Record<ToggleState, { icon: Codicon | undefined; checked: boolean }> = {
649+
[ToggleState.unset]: { icon: undefined, checked: false },
650+
[ToggleState.conflicting]: { icon: Codicon.circleFilled, checked: false },
651+
[ToggleState.first]: { icon: Codicon.check, checked: true },
652+
[ToggleState.second]: { icon: Codicon.checkAll, checked: true },
653+
};
654+
checkBox.setIcon(iconMap[value].icon);
655+
checkBox.checked = iconMap[value].checked;
617656

618657
if (!item.enabled.read(reader)) {
619658
checkBox.disable();
@@ -658,17 +697,17 @@ class ResultCodeEditorView extends CodeEditorView {
658697
model.modifiedBaseRanges.read(reader),
659698
model.resultDiffs.read(reader),
660699
(baseRange, diff) =>
661-
baseRange.baseRange.touches(diff.originalRange)
700+
baseRange.baseRange.touches(diff.inputRange)
662701
? CompareResult.neitherLessOrGreaterThan
663702
: LineRange.compareByStart(
664703
baseRange.baseRange,
665-
diff.originalRange
704+
diff.inputRange
666705
)
667706
);
668707

669708
for (const m of baseRangeWithStoreAndTouchingDiffs) {
670709
for (const r of m.rights) {
671-
const range = r.modifiedRange;
710+
const range = r.outputRange;
672711

673712
const state = m.left ? model.getState(m.left).read(reader) : undefined;
674713

@@ -687,8 +726,11 @@ class ResultCodeEditorView extends CodeEditorView {
687726
if (state.input2 && !state.input1) {
688727
return 'merge-editor-modified-base-range-input2';
689728
}
729+
if (state.input1 && state.input2) {
730+
return 'merge-editor-modified-base-range-combination';
731+
}
690732
}
691-
return 'merge-editor-modified-base-range-combination';
733+
return 'merge-editor-modified-base-range';
692734
})(),
693735
description: 'Result Diff'
694736
}

0 commit comments

Comments
 (0)