Skip to content

Commit 6fa43c8

Browse files
authored
Removes conflict markers before diffing to improve dealing with conflict markers (microsoft#158283)
* Removes conflict markers before diffing to improve dealing with conflict markers * Fixes compile errors * Fixes test
1 parent 1b3f0b0 commit 6fa43c8

File tree

7 files changed

+345
-22
lines changed

7 files changed

+345
-22
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
2222
import { ILanguageSupport, ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
2323
import { autorun } from 'vs/base/common/observable';
2424
import { WorkerBasedDocumentDiffProvider } from 'vs/editor/browser/widget/workerBasedDocumentDiffProvider';
25+
import { ProjectedDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/projectedDocumentDiffProvider';
2526

2627
export class MergeEditorInputData {
2728
constructor(
@@ -125,13 +126,15 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements
125126
this._store.add(base);
126127
this._store.add(result);
127128

129+
const diffProvider = this._instaService.createInstance(WorkerBasedDocumentDiffProvider);
128130
this._model = this._instaService.createInstance(
129131
MergeEditorModel,
130132
base.object.textEditorModel,
131133
input1Data,
132134
input2Data,
133135
result.object.textEditorModel,
134-
this._instaService.createInstance(MergeDiffComputer, this._instaService.createInstance(WorkerBasedDocumentDiffProvider)),
136+
this._instaService.createInstance(MergeDiffComputer, diffProvider),
137+
this._instaService.createInstance(MergeDiffComputer, this._instaService.createInstance(ProjectedDiffComputer, diffProvider)),
135138
{
136139
resetUnknownOnInitialization: false
137140
},

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ export class LineRangeMapping {
5959
);
6060
}
6161

62+
public addInputLineDelta(delta: number): LineRangeMapping {
63+
return new LineRangeMapping(
64+
this.inputRange.delta(delta),
65+
this.outputRange
66+
);
67+
}
68+
6269
public getRange(direction: MappingDirection): LineRange {
6370
return direction === MappingDirection.input ? this.inputRange : this.outputRange;
6471
}
@@ -215,6 +222,16 @@ export class DetailedLineRangeMapping extends LineRangeMapping {
215222
);
216223
}
217224

225+
public override addInputLineDelta(delta: number): DetailedLineRangeMapping {
226+
return new DetailedLineRangeMapping(
227+
this.inputRange.delta(delta),
228+
this.inputTextModel,
229+
this.outputRange,
230+
this.outputTextModel,
231+
this.rangeMappings.map(d => d.addInputLineDelta(delta))
232+
);
233+
}
234+
218235
public override join(other: DetailedLineRangeMapping): DetailedLineRangeMapping {
219236
return new DetailedLineRangeMapping(
220237
this.inputRange.join(other.inputRange),
@@ -265,4 +282,16 @@ export class RangeMapping {
265282
)
266283
);
267284
}
285+
286+
addInputLineDelta(deltaLines: number): RangeMapping {
287+
return new RangeMapping(
288+
new Range(
289+
this.inputRange.startLineNumber + deltaLines,
290+
this.inputRange.startColumn,
291+
this.inputRange.endLineNumber + deltaLines,
292+
this.inputRange.endColumn
293+
),
294+
this.outputRange,
295+
);
296+
}
268297
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { CompareResult, equals } from 'vs/base/common/arrays';
77
import { BugIndicatingError } from 'vs/base/common/errors';
8-
import { ISettableObservable, derived, waitForState, observableValue, keepAlive, autorunHandleChanges, transaction, IReader, ITransaction, IObservable } from 'vs/base/common/observable';
8+
import { autorunHandleChanges, derived, IObservable, IReader, ISettableObservable, ITransaction, keepAlive, observableValue, transaction, waitForState } from 'vs/base/common/observable';
99
import { ILanguageService } from 'vs/editor/common/languages/language';
1010
import { ITextModel, ITextSnapshot } from 'vs/editor/common/model';
1111
import { IModelService } from 'vs/editor/common/services/model';
@@ -20,7 +20,7 @@ import { ModifiedBaseRange, ModifiedBaseRangeState } from './modifiedBaseRange';
2020
export class MergeEditorModel extends EditorModel {
2121
private readonly input1TextModelDiffs = this._register(new TextModelDiffs(this.base, this.input1.textModel, this.diffComputer));
2222
private readonly input2TextModelDiffs = this._register(new TextModelDiffs(this.base, this.input2.textModel, this.diffComputer));
23-
private readonly resultTextModelDiffs = this._register(new TextModelDiffs(this.base, this.resultTextModel, this.diffComputer));
23+
private readonly resultTextModelDiffs = this._register(new TextModelDiffs(this.base, this.resultTextModel, this.diffComputerConflictProjection));
2424

2525
public readonly state = derived('state', reader => {
2626
const states = [
@@ -138,9 +138,10 @@ export class MergeEditorModel extends EditorModel {
138138
readonly input2: InputData,
139139
readonly resultTextModel: ITextModel,
140140
private readonly diffComputer: IMergeDiffComputer,
141+
private readonly diffComputerConflictProjection: IMergeDiffComputer,
141142
options: { resetUnknownOnInitialization: boolean },
142143
@IModelService private readonly modelService: IModelService,
143-
@ILanguageService private readonly languageService: ILanguageService,
144+
@ILanguageService private readonly languageService: ILanguageService
144145
) {
145146
super();
146147

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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 { Position } from 'vs/editor/common/core/position';
7+
import { Range } from 'vs/editor/common/core/range';
8+
import { IDocumentDiff, IDocumentDiffProvider, IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider';
9+
import { LineRange, LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/linesDiffComputer';
10+
import { ITextModel } from 'vs/editor/common/model';
11+
import { IModelService } from 'vs/editor/common/services/model';
12+
import { TextModelProjection } from 'vs/workbench/contrib/mergeEditor/browser/model/textModelProjection';
13+
14+
export class ProjectedDiffComputer implements IDocumentDiffProvider {
15+
private readonly projectedTextModel = new Map<ITextModel, TextModelProjection>();
16+
17+
constructor(
18+
private readonly underlyingDiffComputer: IDocumentDiffProvider,
19+
@IModelService private readonly modelService: IModelService,
20+
) {
21+
22+
}
23+
24+
async computeDiff(
25+
textModel1: ITextModel,
26+
textModel2: ITextModel,
27+
options: IDocumentDiffProviderOptions
28+
): Promise<IDocumentDiff> {
29+
let proj = this.projectedTextModel.get(textModel2);
30+
if (!proj) {
31+
proj = TextModelProjection.create(textModel2, {
32+
blockToRemoveStartLinePrefix: '<<<<<<<',
33+
blockToRemoveEndLinePrefix: '>>>>>>>',
34+
}, this.modelService);
35+
this.projectedTextModel.set(textModel2, proj);
36+
}
37+
38+
const result = await this.underlyingDiffComputer.computeDiff(textModel1, proj.targetDocument, options);
39+
40+
const transformer = proj.createMonotonousReverseTransformer();
41+
42+
return {
43+
identical: result.identical,
44+
quitEarly: result.quitEarly,
45+
46+
changes: result.changes.map(d => {
47+
const start = transformer.transform(new Position(d.modifiedRange.startLineNumber, 1)).lineNumber;
48+
49+
const innerChanges = d.innerChanges?.map(m => {
50+
const start = transformer.transform(m.modifiedRange.getStartPosition());
51+
const end = transformer.transform(m.modifiedRange.getEndPosition());
52+
return new RangeMapping(m.originalRange, Range.fromPositions(start, end));
53+
});
54+
55+
const end = transformer.transform(new Position(d.modifiedRange.endLineNumberExclusive, 1)).lineNumber;
56+
57+
return new LineRangeMapping(
58+
d.originalRange,
59+
new LineRange(start, end),
60+
innerChanges
61+
);
62+
})
63+
};
64+
}
65+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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 { ArrayQueue } from 'vs/base/common/arrays';
7+
import { BugIndicatingError } from 'vs/base/common/errors';
8+
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
9+
import { URI } from 'vs/base/common/uri';
10+
import { Position } from 'vs/editor/common/core/position';
11+
import { ITextModel } from 'vs/editor/common/model';
12+
import { IModelService } from 'vs/editor/common/services/model';
13+
import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange';
14+
15+
export class TextModelProjection extends Disposable {
16+
private static counter: number = 0;
17+
18+
public static create(
19+
sourceDocument: ITextModel,
20+
projectionConfiguration: ProjectionConfiguration,
21+
modelService: IModelService
22+
): TextModelProjection {
23+
const textModel = TextModelProjection.createModelReference(
24+
modelService
25+
);
26+
return new TextModelProjection(textModel, sourceDocument, { dispose: () => { } }, projectionConfiguration);
27+
}
28+
29+
public static createForTargetDocument(
30+
sourceDocument: ITextModel,
31+
projectionConfiguration: ProjectionConfiguration,
32+
targetDocument: ITextModel,
33+
): TextModelProjection {
34+
return new TextModelProjection(targetDocument, sourceDocument, new DisposableStore(), projectionConfiguration);
35+
}
36+
37+
private static createModelReference(
38+
modelService: IModelService
39+
): ITextModel {
40+
const uri = URI.from({
41+
scheme: 'projected-text-model',
42+
path: `/projection${TextModelProjection.counter++}`,
43+
});
44+
45+
return modelService.createModel('', null, uri, false);
46+
}
47+
48+
private currentBlocks: Block[];
49+
50+
constructor(
51+
public readonly targetDocument: ITextModel,
52+
sourceDocument: ITextModel,
53+
disposable: IDisposable,
54+
projectionConfiguration: ProjectionConfiguration
55+
) {
56+
super();
57+
58+
this._register(disposable);
59+
60+
const result = getBlocks(sourceDocument, projectionConfiguration);
61+
this.currentBlocks = result.blocks;
62+
targetDocument.setValue(result.transformedContent);
63+
64+
this._register(
65+
sourceDocument.onDidChangeContent((c) => {
66+
// TODO improve this
67+
const result = getBlocks(sourceDocument, projectionConfiguration);
68+
this.currentBlocks = result.blocks;
69+
targetDocument.setValue(result.transformedContent);
70+
})
71+
);
72+
}
73+
74+
/**
75+
* The created transformer can only be called with monotonically increasing positions.
76+
*/
77+
createMonotonousReverseTransformer(): Transformer {
78+
let lineDelta = 0;
79+
const blockQueue = new ArrayQueue(this.currentBlocks);
80+
let lastLineNumber = 0;
81+
return {
82+
transform(position) {
83+
if (position.lineNumber < lastLineNumber) {
84+
throw new BugIndicatingError();
85+
}
86+
lastLineNumber = position.lineNumber;
87+
88+
while (true) {
89+
const next = blockQueue.peek();
90+
if (!next) {
91+
break;
92+
}
93+
if (position.lineNumber + lineDelta > next.lineRange.startLineNumber) {
94+
blockQueue.dequeue();
95+
lineDelta += next.lineRange.lineCount - 1;
96+
} else {
97+
break;
98+
}
99+
}
100+
101+
// Column number never changes
102+
return new Position(position.lineNumber + lineDelta, position.column);
103+
},
104+
};
105+
}
106+
}
107+
108+
function getBlocks(document: ITextModel, configuration: ProjectionConfiguration): { blocks: Block[]; transformedContent: string } {
109+
const blocks: Block[] = [];
110+
const transformedContent: string[] = [];
111+
112+
let inBlock = false;
113+
let startLineNumber = -1;
114+
let curLine = 0;
115+
116+
for (const line of document.getLinesContent()) {
117+
curLine++;
118+
if (!inBlock) {
119+
if (line.startsWith(configuration.blockToRemoveStartLinePrefix)) {
120+
inBlock = true;
121+
startLineNumber = curLine;
122+
} else {
123+
transformedContent.push(line);
124+
}
125+
} else {
126+
if (line.startsWith(configuration.blockToRemoveEndLinePrefix)) {
127+
inBlock = false;
128+
blocks.push(new Block(new LineRange(startLineNumber, curLine - startLineNumber + 1)));
129+
// We add a (hopefully) unique symbol so that diffing recognizes the deleted block (HEXAGRAM FOR CONFLICT)
130+
// allow-any-unicode-next-line
131+
transformedContent.push('䷅');
132+
}
133+
}
134+
}
135+
136+
return {
137+
blocks,
138+
transformedContent: transformedContent.join('\n')
139+
};
140+
}
141+
142+
class Block {
143+
constructor(public readonly lineRange: LineRange) { }
144+
}
145+
146+
interface ProjectionConfiguration {
147+
blockToRemoveStartLinePrefix: string;
148+
blockToRemoveEndLinePrefix: string;
149+
}
150+
151+
interface Transformer {
152+
transform(position: Position): Position;
153+
}

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

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,25 @@ class MergeModelInterface extends Disposable {
257257
const input2TextModel = this._register(createTextModel(options.input2, options.languageId));
258258
const baseTextModel = this._register(createTextModel(options.base, options.languageId));
259259
const resultTextModel = this._register(createTextModel(options.result, options.languageId));
260+
261+
const diffComputer = instantiationService.createInstance(MergeDiffComputer,
262+
{
263+
// Don't go through the webworker to improve unit test performance & reduce dependencies
264+
async computeDiff(textModel1, textModel2) {
265+
const result = linesDiffComputers.smart.computeDiff(
266+
textModel1.getLinesContent(),
267+
textModel2.getLinesContent(),
268+
{ ignoreTrimWhitespace: false, maxComputationTime: 10000 }
269+
);
270+
return {
271+
changes: result.changes,
272+
quitEarly: result.quitEarly,
273+
identical: result.changes.length === 0
274+
};
275+
},
276+
}
277+
);
278+
260279
this.mergeModel = this._register(instantiationService.createInstance(MergeEditorModel,
261280
baseTextModel,
262281
{
@@ -272,24 +291,9 @@ class MergeModelInterface extends Disposable {
272291
title: '',
273292
},
274293
resultTextModel,
275-
instantiationService.createInstance(MergeDiffComputer,
276-
{
277-
// Don't go through the webworker to improve unit test performance & reduce dependencies
278-
async computeDiff(textModel1, textModel2) {
279-
const result = linesDiffComputers.smart.computeDiff(
280-
textModel1.getLinesContent(),
281-
textModel2.getLinesContent(),
282-
{ ignoreTrimWhitespace: false, maxComputationTime: 10000 }
283-
);
284-
return {
285-
changes: result.changes,
286-
quitEarly: result.quitEarly,
287-
identical: result.changes.length === 0
288-
};
289-
},
290-
}), {
291-
resetUnknownOnInitialization: false
292-
}
294+
diffComputer,
295+
diffComputer,
296+
{ resetUnknownOnInitialization: false }
293297
));
294298
}
295299

0 commit comments

Comments
 (0)