Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/merge-conflict/src/mergeConflictParser.ts
3296 views
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
import * as vscode from 'vscode';
6
import * as interfaces from './interfaces';
7
import { DocumentMergeConflict } from './documentMergeConflict';
8
import TelemetryReporter from '@vscode/extension-telemetry';
9
10
const startHeaderMarker = '<<<<<<<';
11
const commonAncestorsMarker = '|||||||';
12
const splitterMarker = '=======';
13
const endFooterMarker = '>>>>>>>';
14
15
interface IScanMergedConflict {
16
startHeader: vscode.TextLine;
17
commonAncestors: vscode.TextLine[];
18
splitter?: vscode.TextLine;
19
endFooter?: vscode.TextLine;
20
}
21
22
export class MergeConflictParser {
23
24
static scanDocument(document: vscode.TextDocument, telemetryReporter: TelemetryReporter): interfaces.IDocumentMergeConflict[] {
25
26
// Scan each line in the document, we already know there is at least a <<<<<<< and
27
// >>>>>> marker within the document, we need to group these into conflict ranges.
28
// We initially build a scan match, that references the lines of the header, splitter
29
// and footer. This is then converted into a full descriptor containing all required
30
// ranges.
31
32
let currentConflict: IScanMergedConflict | null = null;
33
const conflictDescriptors: interfaces.IDocumentMergeConflictDescriptor[] = [];
34
35
for (let i = 0; i < document.lineCount; i++) {
36
const line = document.lineAt(i);
37
38
// Ignore empty lines
39
if (!line || line.isEmptyOrWhitespace) {
40
continue;
41
}
42
43
// Is this a start line? <<<<<<<
44
if (line.text.startsWith(startHeaderMarker)) {
45
if (currentConflict !== null) {
46
// Error, we should not see a startMarker before we've seen an endMarker
47
currentConflict = null;
48
49
// Give up parsing, anything matched up this to this point will be decorated
50
// anything after will not
51
break;
52
}
53
54
// Create a new conflict starting at this line
55
currentConflict = { startHeader: line, commonAncestors: [] };
56
}
57
// Are we within a conflict block and is this a common ancestors marker? |||||||
58
else if (currentConflict && !currentConflict.splitter && line.text.startsWith(commonAncestorsMarker)) {
59
currentConflict.commonAncestors.push(line);
60
}
61
// Are we within a conflict block and is this a splitter? =======
62
else if (currentConflict && !currentConflict.splitter && line.text === splitterMarker) {
63
currentConflict.splitter = line;
64
}
65
// Are we within a conflict block and is this a footer? >>>>>>>
66
else if (currentConflict && line.text.startsWith(endFooterMarker)) {
67
currentConflict.endFooter = line;
68
69
// Create a full descriptor from the lines that we matched. This can return
70
// null if the descriptor could not be completed.
71
const completeDescriptor = MergeConflictParser.scanItemTolMergeConflictDescriptor(document, currentConflict);
72
73
if (completeDescriptor !== null) {
74
conflictDescriptors.push(completeDescriptor);
75
}
76
77
// Reset the current conflict to be empty, so we can match the next
78
// starting header marker.
79
currentConflict = null;
80
}
81
}
82
83
return conflictDescriptors
84
.filter(Boolean)
85
.map(descriptor => new DocumentMergeConflict(descriptor, telemetryReporter));
86
}
87
88
private static scanItemTolMergeConflictDescriptor(document: vscode.TextDocument, scanned: IScanMergedConflict): interfaces.IDocumentMergeConflictDescriptor | null {
89
// Validate we have all the required lines within the scan item.
90
if (!scanned.startHeader || !scanned.splitter || !scanned.endFooter) {
91
return null;
92
}
93
94
const tokenAfterCurrentBlock: vscode.TextLine = scanned.commonAncestors[0] || scanned.splitter;
95
96
// Assume that descriptor.current.header, descriptor.incoming.header and descriptor.splitter
97
// have valid ranges, fill in content and total ranges from these parts.
98
// NOTE: We need to shift the decorator range back one character so the splitter does not end up with
99
// two decoration colors (current and splitter), if we take the new line from the content into account
100
// the decorator will wrap to the next line.
101
return {
102
current: {
103
header: scanned.startHeader.range,
104
decoratorContent: new vscode.Range(
105
scanned.startHeader.rangeIncludingLineBreak.end,
106
MergeConflictParser.shiftBackOneCharacter(document, tokenAfterCurrentBlock.range.start, scanned.startHeader.rangeIncludingLineBreak.end)),
107
// Current content is range between header (shifted for linebreak) and splitter or common ancestors mark start
108
content: new vscode.Range(
109
scanned.startHeader.rangeIncludingLineBreak.end,
110
tokenAfterCurrentBlock.range.start),
111
name: scanned.startHeader.text.substring(startHeaderMarker.length + 1)
112
},
113
commonAncestors: scanned.commonAncestors.map((currentTokenLine, index, commonAncestors) => {
114
const nextTokenLine = commonAncestors[index + 1] || scanned.splitter;
115
return {
116
header: currentTokenLine.range,
117
decoratorContent: new vscode.Range(
118
currentTokenLine.rangeIncludingLineBreak.end,
119
MergeConflictParser.shiftBackOneCharacter(document, nextTokenLine.range.start, currentTokenLine.rangeIncludingLineBreak.end)),
120
// Each common ancestors block is range between one common ancestors token
121
// (shifted for linebreak) and start of next common ancestors token or splitter
122
content: new vscode.Range(
123
currentTokenLine.rangeIncludingLineBreak.end,
124
nextTokenLine.range.start),
125
name: currentTokenLine.text.substring(commonAncestorsMarker.length + 1)
126
};
127
}),
128
splitter: scanned.splitter.range,
129
incoming: {
130
header: scanned.endFooter.range,
131
decoratorContent: new vscode.Range(
132
scanned.splitter.rangeIncludingLineBreak.end,
133
MergeConflictParser.shiftBackOneCharacter(document, scanned.endFooter.range.start, scanned.splitter.rangeIncludingLineBreak.end)),
134
// Incoming content is range between splitter (shifted for linebreak) and footer start
135
content: new vscode.Range(
136
scanned.splitter.rangeIncludingLineBreak.end,
137
scanned.endFooter.range.start),
138
name: scanned.endFooter.text.substring(endFooterMarker.length + 1)
139
},
140
// Entire range is between current header start and incoming header end (including line break)
141
range: new vscode.Range(scanned.startHeader.range.start, scanned.endFooter.rangeIncludingLineBreak.end)
142
};
143
}
144
145
static containsConflict(document: vscode.TextDocument): boolean {
146
if (!document) {
147
return false;
148
}
149
150
const text = document.getText();
151
return text.includes(startHeaderMarker) && text.includes(endFooterMarker);
152
}
153
154
private static shiftBackOneCharacter(document: vscode.TextDocument, range: vscode.Position, unlessEqual: vscode.Position): vscode.Position {
155
if (range.isEqual(unlessEqual)) {
156
return range;
157
}
158
159
let line = range.line;
160
let character = range.character - 1;
161
162
if (character < 0) {
163
line--;
164
character = document.lineAt(line).range.end.character;
165
}
166
167
return new vscode.Position(line, character);
168
}
169
}
170
171