@opensumi/ide-editor 2.27.3-next-1714461024.0 → 2.27.3-next-1714464252.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.
- package/lib/browser/doc-model/editor-document-model-service.d.ts +3 -1
- package/lib/browser/doc-model/editor-document-model-service.d.ts.map +1 -1
- package/lib/browser/doc-model/editor-document-model-service.js +6 -1
- package/lib/browser/doc-model/editor-document-model-service.js.map +1 -1
- package/lib/browser/doc-model/types.d.ts +1 -0
- package/lib/browser/doc-model/types.d.ts.map +1 -1
- package/lib/browser/doc-model/types.js.map +1 -1
- package/lib/browser/editor-collection.service.d.ts +6 -6
- package/lib/browser/editor-collection.service.d.ts.map +1 -1
- package/lib/browser/editor-collection.service.js.map +1 -1
- package/lib/browser/editor-electron.contribution.d.ts +2 -2
- package/lib/browser/editor-electron.contribution.d.ts.map +1 -1
- package/lib/browser/editor-electron.contribution.js +1 -1
- package/lib/browser/editor-electron.contribution.js.map +1 -1
- package/lib/browser/editor.decoration.service.d.ts.map +1 -1
- package/lib/browser/editor.decoration.service.js +6 -4
- package/lib/browser/editor.decoration.service.js.map +1 -1
- package/lib/browser/editor.module.less +0 -54
- package/lib/browser/hooks/useEditor.d.ts +4 -0
- package/lib/browser/hooks/useEditor.d.ts.map +1 -0
- package/lib/browser/hooks/useEditor.js +31 -0
- package/lib/browser/hooks/useEditor.js.map +1 -0
- package/lib/browser/hooks/useInMergeChanges.d.ts +3 -0
- package/lib/browser/hooks/useInMergeChanges.d.ts.map +1 -0
- package/lib/browser/hooks/useInMergeChanges.js +27 -0
- package/lib/browser/hooks/useInMergeChanges.js.map +1 -0
- package/lib/browser/merge-conflict/conflict-parser.d.ts +46 -0
- package/lib/browser/merge-conflict/conflict-parser.d.ts.map +1 -0
- package/lib/browser/merge-conflict/conflict-parser.js +209 -0
- package/lib/browser/merge-conflict/conflict-parser.js.map +1 -0
- package/lib/browser/merge-conflict/index.d.ts +3 -0
- package/lib/browser/merge-conflict/index.d.ts.map +1 -0
- package/lib/browser/merge-conflict/index.js +6 -0
- package/lib/browser/merge-conflict/index.js.map +1 -0
- package/lib/browser/merge-conflict/types.d.ts +60 -0
- package/lib/browser/merge-conflict/types.d.ts.map +1 -0
- package/lib/browser/merge-conflict/types.js +15 -0
- package/lib/browser/merge-conflict/types.js.map +1 -0
- package/lib/browser/merge-editor/MergeEditorFloatComponents.d.ts.map +1 -1
- package/lib/browser/merge-editor/MergeEditorFloatComponents.js +75 -49
- package/lib/browser/merge-editor/MergeEditorFloatComponents.js.map +1 -1
- package/lib/browser/merge-editor/merge-editor.module.less +71 -0
- package/lib/browser/merge-editor/merge-editor.provider.d.ts.map +1 -1
- package/lib/browser/merge-editor/merge-editor.provider.js +3 -0
- package/lib/browser/merge-editor/merge-editor.provider.js.map +1 -1
- package/lib/common/editor.d.ts +1 -1
- package/package.json +14 -14
- package/src/browser/doc-model/editor-document-model-service.ts +11 -1
- package/src/browser/doc-model/types.ts +4 -0
- package/src/browser/editor-collection.service.ts +7 -7
- package/src/browser/editor-electron.contribution.ts +1 -2
- package/src/browser/editor.decoration.service.ts +6 -4
- package/src/browser/editor.module.less +0 -54
- package/src/browser/hooks/useEditor.ts +34 -0
- package/src/browser/hooks/useInMergeChanges.ts +30 -0
- package/src/browser/merge-conflict/conflict-parser.ts +323 -0
- package/src/browser/merge-conflict/index.ts +2 -0
- package/src/browser/merge-conflict/types.ts +73 -0
- package/src/browser/merge-editor/MergeEditorFloatComponents.tsx +109 -74
- package/src/browser/merge-editor/merge-editor.module.less +71 -0
- package/src/browser/merge-editor/merge-editor.provider.ts +3 -0
- package/src/common/editor.ts +1 -1
|
@@ -0,0 +1,323 @@
|
|
|
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
|
+
// Some code copied and modified from https://github.com/microsoft/vscode/blob/main/extensions/merge-conflict/src/mergeConflictParser.ts
|
|
6
|
+
|
|
7
|
+
import { Injectable } from '@opensumi/di';
|
|
8
|
+
import { Disposable, LRUCache, uuid } from '@opensumi/ide-core-common';
|
|
9
|
+
import * as monaco from '@opensumi/ide-monaco';
|
|
10
|
+
|
|
11
|
+
import { ICacheDocumentMergeConflict, IDocumentMergeConflictDescriptor, IMergeRegion } from './types';
|
|
12
|
+
|
|
13
|
+
const startHeaderMarker = '<<<<<<<';
|
|
14
|
+
const commonAncestorsMarker = '|||||||';
|
|
15
|
+
const splitterMarker = '=======';
|
|
16
|
+
const endFooterMarker = '>>>>>>>';
|
|
17
|
+
|
|
18
|
+
interface IScanMergedConflict {
|
|
19
|
+
startHeader: TextLine;
|
|
20
|
+
commonAncestors: TextLine[];
|
|
21
|
+
splitter?: TextLine;
|
|
22
|
+
endFooter?: TextLine;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface IConflictCache {
|
|
26
|
+
id: string;
|
|
27
|
+
range: monaco.Range;
|
|
28
|
+
text: string;
|
|
29
|
+
isResolved: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class TextLine {
|
|
33
|
+
lineNumber: number;
|
|
34
|
+
text: string;
|
|
35
|
+
range: monaco.Range;
|
|
36
|
+
rangeIncludingLineBreak: monaco.Range;
|
|
37
|
+
firstNonWhitespaceCharacterIndex: number;
|
|
38
|
+
isEmptyOrWhitespace: boolean;
|
|
39
|
+
constructor(document: monaco.editor.ITextModel, line: number) {
|
|
40
|
+
if (typeof line !== 'number' || line <= 0 || line > document.getLineCount()) {
|
|
41
|
+
throw new Error('Illegal value for `line`');
|
|
42
|
+
}
|
|
43
|
+
this.text = document.getLineContent(line);
|
|
44
|
+
this.firstNonWhitespaceCharacterIndex = /^(\s*)/.exec(this.text)![1].length;
|
|
45
|
+
this.range = new monaco.Range(line, 1, line, this.text.length + 1);
|
|
46
|
+
this.rangeIncludingLineBreak =
|
|
47
|
+
line <= document.getLineCount() ? new monaco.Range(line, 1, line + 1, 1) : this.range;
|
|
48
|
+
this.lineNumber = line;
|
|
49
|
+
this.isEmptyOrWhitespace = this.firstNonWhitespaceCharacterIndex === this.text.length;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Injectable()
|
|
54
|
+
export class MergeConflictParser extends Disposable {
|
|
55
|
+
cache = new LRUCache<string, DocumentMergeConflict[]>(100);
|
|
56
|
+
|
|
57
|
+
private _conflictTextCaches = new Map<string, string>();
|
|
58
|
+
|
|
59
|
+
private _conflictRangeCaches = new Map<string, IConflictCache[]>();
|
|
60
|
+
|
|
61
|
+
private static createCacheKey(document: monaco.editor.ITextModel) {
|
|
62
|
+
return `${document.uri.toString()}-${document.getAlternativeVersionId()}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
scanDocument(document: monaco.editor.ITextModel) {
|
|
66
|
+
const cacheKey = MergeConflictParser.createCacheKey(document);
|
|
67
|
+
if (this.cache.has(cacheKey)) {
|
|
68
|
+
return this.cache.get(cacheKey)!;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Scan each line in the document, we already know there is at least a <<<<<<< and
|
|
72
|
+
// >>>>>> marker within the document, we need to group these into conflict ranges.
|
|
73
|
+
// We initially build a scan match, that references the lines of the header, splitter
|
|
74
|
+
// and footer. This is then converted into a full descriptor containing all required
|
|
75
|
+
// ranges.
|
|
76
|
+
|
|
77
|
+
let currentConflict: IScanMergedConflict | null = null;
|
|
78
|
+
const conflictDescriptors: IDocumentMergeConflictDescriptor[] = [];
|
|
79
|
+
const cacheConflictDescriptors = this._conflictTextCaches.get(document.uri.toString());
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < document.getLineCount(); i++) {
|
|
82
|
+
const line = new TextLine(document, i + 1);
|
|
83
|
+
// Ignore empty lines
|
|
84
|
+
if (!line || line.isEmptyOrWhitespace) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Is this a start line? <<<<<<<
|
|
89
|
+
if (line.text.startsWith(startHeaderMarker)) {
|
|
90
|
+
if (currentConflict !== null) {
|
|
91
|
+
// Error, we should not see a startMarker before we've seen an endMarker
|
|
92
|
+
currentConflict = null;
|
|
93
|
+
|
|
94
|
+
// Give up parsing, anything matched up this to this point will be decorated
|
|
95
|
+
// anything after will not
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Create a new conflict starting at this line
|
|
100
|
+
currentConflict = { startHeader: line, commonAncestors: [] };
|
|
101
|
+
}
|
|
102
|
+
// Are we within a conflict block and is this a common ancestors marker? |||||||
|
|
103
|
+
else if (currentConflict && !currentConflict.splitter && line.text.startsWith(commonAncestorsMarker)) {
|
|
104
|
+
currentConflict.commonAncestors.push(line);
|
|
105
|
+
}
|
|
106
|
+
// Are we within a conflict block and is this a splitter? =======
|
|
107
|
+
else if (currentConflict && !currentConflict.splitter && line.text === splitterMarker) {
|
|
108
|
+
currentConflict.splitter = line;
|
|
109
|
+
}
|
|
110
|
+
// Are we within a conflict block and is this a footer? >>>>>>>
|
|
111
|
+
else if (currentConflict && line.text.startsWith(endFooterMarker)) {
|
|
112
|
+
currentConflict.endFooter = line;
|
|
113
|
+
|
|
114
|
+
// Create a full descriptor from the lines that we matched. This can return
|
|
115
|
+
// null if the descriptor could not be completed.
|
|
116
|
+
const completeDescriptor = scanItemTolMergeConflictDescriptor(document, currentConflict);
|
|
117
|
+
|
|
118
|
+
if (completeDescriptor !== null) {
|
|
119
|
+
conflictDescriptors.push(completeDescriptor);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Reset the current conflict to be empty, so we can match the next
|
|
123
|
+
// starting header marker.
|
|
124
|
+
currentConflict = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!cacheConflictDescriptors && conflictDescriptors.length) {
|
|
128
|
+
this._conflictTextCaches.set(document.uri.toString(), document.getValue());
|
|
129
|
+
const conflictRanges: IConflictCache[] = [];
|
|
130
|
+
conflictDescriptors.filter(Boolean).forEach((descriptor) => {
|
|
131
|
+
const range = descriptor.range;
|
|
132
|
+
conflictRanges.push({
|
|
133
|
+
id: uuid(),
|
|
134
|
+
range,
|
|
135
|
+
text: document.getValueInRange(range),
|
|
136
|
+
isResolved: false,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
this._conflictRangeCaches.set(document.uri.toString(), conflictRanges);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const result = conflictDescriptors?.filter(Boolean).map((descriptor) => new DocumentMergeConflict(descriptor));
|
|
143
|
+
this.cache.set(cacheKey, result);
|
|
144
|
+
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
getConflictText(uri: string) {
|
|
148
|
+
return this._conflictTextCaches.get(uri);
|
|
149
|
+
}
|
|
150
|
+
getAllConflictsByUri(uri: string) {
|
|
151
|
+
return this._conflictRangeCaches.get(uri);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
getAllConflicts() {
|
|
155
|
+
return this._conflictRangeCaches;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
setConflictResolved(uri: string, id: string) {
|
|
159
|
+
const conflictRanges = this._conflictRangeCaches.get(uri);
|
|
160
|
+
if (conflictRanges) {
|
|
161
|
+
const conflictRange = conflictRanges.find((item) => item.id === id);
|
|
162
|
+
if (conflictRange) {
|
|
163
|
+
conflictRange.isResolved = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
deleteConflictText(uri: string) {
|
|
169
|
+
this._conflictTextCaches.delete(uri);
|
|
170
|
+
}
|
|
171
|
+
dispose() {
|
|
172
|
+
this._conflictTextCaches.clear();
|
|
173
|
+
this._conflictRangeCaches.clear();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function scanItemTolMergeConflictDescriptor(
|
|
178
|
+
document: monaco.editor.ITextModel,
|
|
179
|
+
scanned: IScanMergedConflict,
|
|
180
|
+
): IDocumentMergeConflictDescriptor | null {
|
|
181
|
+
// Validate we have all the required lines within the scan item.
|
|
182
|
+
if (!scanned.startHeader || !scanned.splitter || !scanned.endFooter) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const tokenAfterCurrentBlock: TextLine = scanned.commonAncestors[0] || scanned.splitter;
|
|
187
|
+
|
|
188
|
+
// Assume that descriptor.current.header, descriptor.incoming.header and descriptor.splitter
|
|
189
|
+
// have valid ranges, fill in content and total ranges from these parts.
|
|
190
|
+
// NOTE: We need to shift the decorator range back one character so the splitter does not end up with
|
|
191
|
+
// two decoration colors (current and splitter), if we take the new line from the content into account
|
|
192
|
+
// the decorator will wrap to the next line.
|
|
193
|
+
return {
|
|
194
|
+
current: {
|
|
195
|
+
header: scanned.startHeader.range,
|
|
196
|
+
decoratorContent: new monaco.Range(
|
|
197
|
+
scanned.startHeader.rangeIncludingLineBreak.endLineNumber,
|
|
198
|
+
scanned.startHeader.rangeIncludingLineBreak.endColumn,
|
|
199
|
+
shiftBackOneCharacter(
|
|
200
|
+
document,
|
|
201
|
+
tokenAfterCurrentBlock.range.getStartPosition(),
|
|
202
|
+
scanned.startHeader.rangeIncludingLineBreak.getEndPosition(),
|
|
203
|
+
).lineNumber,
|
|
204
|
+
shiftBackOneCharacter(
|
|
205
|
+
document,
|
|
206
|
+
tokenAfterCurrentBlock.range.getStartPosition(),
|
|
207
|
+
scanned.startHeader.rangeIncludingLineBreak.getEndPosition(),
|
|
208
|
+
).column,
|
|
209
|
+
),
|
|
210
|
+
// Current content is range between header (shifted for linebreak) and splitter or common ancestors mark start
|
|
211
|
+
content: new monaco.Range(
|
|
212
|
+
scanned.startHeader.rangeIncludingLineBreak.endLineNumber,
|
|
213
|
+
scanned.startHeader.rangeIncludingLineBreak.endColumn,
|
|
214
|
+
tokenAfterCurrentBlock.range.startLineNumber,
|
|
215
|
+
tokenAfterCurrentBlock.range.startColumn,
|
|
216
|
+
),
|
|
217
|
+
name: scanned.startHeader.text.substring(startHeaderMarker.length + 1),
|
|
218
|
+
},
|
|
219
|
+
commonAncestors: scanned.commonAncestors.map((currentTokenLine, index, commonAncestors) => {
|
|
220
|
+
const nextTokenLine = commonAncestors[index + 1] || scanned.splitter;
|
|
221
|
+
return {
|
|
222
|
+
header: currentTokenLine.range,
|
|
223
|
+
decoratorContent: new monaco.Range(
|
|
224
|
+
currentTokenLine.rangeIncludingLineBreak.endLineNumber,
|
|
225
|
+
currentTokenLine.rangeIncludingLineBreak.endColumn,
|
|
226
|
+
|
|
227
|
+
shiftBackOneCharacter(
|
|
228
|
+
document,
|
|
229
|
+
nextTokenLine.range.getStartPosition(),
|
|
230
|
+
currentTokenLine.rangeIncludingLineBreak.getEndPosition(),
|
|
231
|
+
).lineNumber,
|
|
232
|
+
shiftBackOneCharacter(
|
|
233
|
+
document,
|
|
234
|
+
nextTokenLine.range.getStartPosition(),
|
|
235
|
+
currentTokenLine.rangeIncludingLineBreak.getEndPosition(),
|
|
236
|
+
).lineNumber,
|
|
237
|
+
),
|
|
238
|
+
// Each common ancestors block is range between one common ancestors token
|
|
239
|
+
// (shifted for linebreak) and start of next common ancestors token or splitter
|
|
240
|
+
content: new monaco.Range(
|
|
241
|
+
currentTokenLine.rangeIncludingLineBreak.endLineNumber,
|
|
242
|
+
currentTokenLine.rangeIncludingLineBreak.endColumn,
|
|
243
|
+
nextTokenLine.range.startLineNumber,
|
|
244
|
+
nextTokenLine.range.startColumn,
|
|
245
|
+
),
|
|
246
|
+
name: currentTokenLine.text.substring(commonAncestorsMarker.length + 1),
|
|
247
|
+
};
|
|
248
|
+
}),
|
|
249
|
+
splitter: scanned.splitter.range,
|
|
250
|
+
incoming: {
|
|
251
|
+
header: scanned.endFooter.range,
|
|
252
|
+
decoratorContent: new monaco.Range(
|
|
253
|
+
scanned.splitter.rangeIncludingLineBreak.endLineNumber,
|
|
254
|
+
scanned.splitter.rangeIncludingLineBreak.endColumn,
|
|
255
|
+
shiftBackOneCharacter(
|
|
256
|
+
document,
|
|
257
|
+
scanned.endFooter.range.getStartPosition(),
|
|
258
|
+
scanned.splitter.rangeIncludingLineBreak.getEndPosition(),
|
|
259
|
+
).lineNumber,
|
|
260
|
+
shiftBackOneCharacter(
|
|
261
|
+
document,
|
|
262
|
+
scanned.endFooter.range.getStartPosition(),
|
|
263
|
+
scanned.splitter.rangeIncludingLineBreak.getEndPosition(),
|
|
264
|
+
).column,
|
|
265
|
+
),
|
|
266
|
+
// Incoming content is range between splitter (shifted for linebreak) and footer start
|
|
267
|
+
content: new monaco.Range(
|
|
268
|
+
scanned.splitter.rangeIncludingLineBreak.endLineNumber,
|
|
269
|
+
scanned.splitter.rangeIncludingLineBreak.endColumn,
|
|
270
|
+
scanned.endFooter.range.startLineNumber,
|
|
271
|
+
scanned.endFooter.range.startColumn,
|
|
272
|
+
),
|
|
273
|
+
name: scanned.endFooter.text.substring(endFooterMarker.length + 1),
|
|
274
|
+
},
|
|
275
|
+
// Entire range is between current header start and incoming header end (including line break)
|
|
276
|
+
range: new monaco.Range(
|
|
277
|
+
scanned.startHeader.range.startLineNumber,
|
|
278
|
+
scanned.startHeader.range.startColumn,
|
|
279
|
+
scanned.endFooter.range.endLineNumber,
|
|
280
|
+
scanned.endFooter.range.endColumn,
|
|
281
|
+
),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function shiftBackOneCharacter(
|
|
286
|
+
document: monaco.editor.ITextModel,
|
|
287
|
+
range: monaco.Position,
|
|
288
|
+
unlessEqual: monaco.Position,
|
|
289
|
+
): monaco.Position {
|
|
290
|
+
if (range.equals(unlessEqual)) {
|
|
291
|
+
return range;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let line = range.lineNumber;
|
|
295
|
+
let character = range.column - 1;
|
|
296
|
+
|
|
297
|
+
if (character < 0) {
|
|
298
|
+
line--;
|
|
299
|
+
character = new TextLine(document, line).range.endColumn;
|
|
300
|
+
}
|
|
301
|
+
return new monaco.Position(line, character);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export class DocumentMergeConflict implements ICacheDocumentMergeConflict {
|
|
305
|
+
public range: monaco.Range;
|
|
306
|
+
public current: IMergeRegion;
|
|
307
|
+
public incoming: IMergeRegion;
|
|
308
|
+
public commonAncestors: IMergeRegion[];
|
|
309
|
+
public splitter: monaco.Range;
|
|
310
|
+
public incomingContent: string;
|
|
311
|
+
public currentContent: string;
|
|
312
|
+
public bothContent: string;
|
|
313
|
+
public aiContent?: string;
|
|
314
|
+
public defaultContent: string;
|
|
315
|
+
private applied = false;
|
|
316
|
+
constructor(descriptor: IDocumentMergeConflictDescriptor) {
|
|
317
|
+
this.range = descriptor.range;
|
|
318
|
+
this.current = descriptor.current;
|
|
319
|
+
this.incoming = descriptor.incoming;
|
|
320
|
+
this.commonAncestors = descriptor.commonAncestors;
|
|
321
|
+
this.splitter = descriptor.splitter;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as monaco from '@opensumi/ide-monaco';
|
|
2
|
+
|
|
3
|
+
export interface IMergeRegion {
|
|
4
|
+
name: string;
|
|
5
|
+
header: monaco.Range;
|
|
6
|
+
content: monaco.Range;
|
|
7
|
+
decoratorContent?: monaco.Range;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const enum CommitType {
|
|
11
|
+
Current,
|
|
12
|
+
Incoming,
|
|
13
|
+
Both,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface IExtensionConfiguration {
|
|
17
|
+
enableCodeLens: boolean;
|
|
18
|
+
enableDecorations: boolean;
|
|
19
|
+
enableEditorOverview: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IDocumentMergeConflict extends IDocumentMergeConflictDescriptor {
|
|
23
|
+
commitEdit(type: CommitType, editor: monaco.editor.ITextModel): Thenable<boolean>;
|
|
24
|
+
applyEdit(
|
|
25
|
+
type: CommitType,
|
|
26
|
+
document: monaco.editor.ITextModel,
|
|
27
|
+
edit: { replace(range: monaco.Range, newText: string): void },
|
|
28
|
+
): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface IDocumentMergeConflictDescriptor {
|
|
32
|
+
[x: string]: any;
|
|
33
|
+
range: monaco.Range;
|
|
34
|
+
current: IMergeRegion;
|
|
35
|
+
incoming: IMergeRegion;
|
|
36
|
+
commonAncestors: IMergeRegion[];
|
|
37
|
+
splitter: monaco.Range;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 存储初始化内容
|
|
41
|
+
export interface ICacheDocumentMergeConflict extends IDocumentMergeConflictDescriptor {
|
|
42
|
+
incomingContent: string;
|
|
43
|
+
currentContent: string;
|
|
44
|
+
bothContent: string;
|
|
45
|
+
aiContent?: string;
|
|
46
|
+
defaultContent: string;
|
|
47
|
+
}
|
|
48
|
+
export interface IDocumentMergeConflictTracker {
|
|
49
|
+
getConflicts(document: monaco.editor.ITextModel): PromiseLike<IDocumentMergeConflict[]>;
|
|
50
|
+
isPending(document: monaco.editor.ITextModel): boolean;
|
|
51
|
+
forget(document: monaco.editor.ITextModel): void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface IDocumentMergeConflictTrackerService {
|
|
55
|
+
createTracker(origin: string): IDocumentMergeConflictTracker;
|
|
56
|
+
forget(document: monaco.editor.ITextModel): void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface IDocumentMergeConflictNavigationResults {
|
|
60
|
+
canNavigate: boolean;
|
|
61
|
+
conflict?: IDocumentMergeConflict;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export enum NavigationDirection {
|
|
65
|
+
Forwards,
|
|
66
|
+
Backwards,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export namespace AI_COMMAND {
|
|
70
|
+
const MERGE_CONFLICT = 'merge-conflict';
|
|
71
|
+
export const ACCEPT = `${MERGE_CONFLICT}.ai.accept`;
|
|
72
|
+
export const REVERT = `${MERGE_CONFLICT}.ai.revert`;
|
|
73
|
+
}
|
|
@@ -5,43 +5,64 @@ import {
|
|
|
5
5
|
AINativeConfigService,
|
|
6
6
|
CommandRegistry,
|
|
7
7
|
CommandService,
|
|
8
|
-
|
|
8
|
+
DisposableStore,
|
|
9
|
+
MERGE_CONFLICT_COMMANDS,
|
|
9
10
|
SCM_COMMANDS,
|
|
10
11
|
URI,
|
|
11
|
-
Uri,
|
|
12
12
|
localize,
|
|
13
13
|
useInjectable,
|
|
14
14
|
} from '@opensumi/ide-core-browser';
|
|
15
|
+
import { MergeConflictCommands } from '@opensumi/ide-core-common/lib/commands/git';
|
|
15
16
|
|
|
16
|
-
import
|
|
17
|
+
import { useEditorDocumentModelRef } from '../hooks/useEditor';
|
|
18
|
+
import { useInMergeChanges } from '../hooks/useInMergeChanges';
|
|
19
|
+
import { DocumentMergeConflict, MergeConflictParser } from '../merge-conflict';
|
|
17
20
|
import { ReactEditorComponent } from '../types';
|
|
18
21
|
|
|
22
|
+
import styles from './merge-editor.module.less';
|
|
23
|
+
|
|
19
24
|
export const MergeEditorFloatComponents: ReactEditorComponent<{ uri: URI }> = ({ resource }) => {
|
|
20
25
|
const aiNativeConfigService = useInjectable<AINativeConfigService>(AINativeConfigService);
|
|
21
26
|
const commandService = useInjectable<CommandService>(CommandService);
|
|
22
27
|
const commandRegistry = useInjectable<CommandRegistry>(CommandRegistry);
|
|
23
|
-
const
|
|
28
|
+
const mergeConflictParser: MergeConflictParser = useInjectable(MergeConflictParser);
|
|
29
|
+
|
|
30
|
+
const editorModel = useEditorDocumentModelRef(resource.uri);
|
|
24
31
|
|
|
25
32
|
const [isVisiable, setIsVisiable] = useState(false);
|
|
33
|
+
const [conflicts, setConflicts] = useState<DocumentMergeConflict[]>([]);
|
|
26
34
|
|
|
27
|
-
const
|
|
35
|
+
const inMergeChanges = useInMergeChanges(resource.uri.toString());
|
|
28
36
|
|
|
29
37
|
useEffect(() => {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
const disposables = new DisposableStore();
|
|
39
|
+
|
|
40
|
+
if (editorModel) {
|
|
41
|
+
const { instance } = editorModel;
|
|
42
|
+
const run = () => {
|
|
43
|
+
const conflicts = mergeConflictParser.scanDocument(instance.getMonacoModel());
|
|
44
|
+
if (conflicts.length > 0) {
|
|
45
|
+
setIsVisiable(true);
|
|
46
|
+
setConflicts(conflicts);
|
|
47
|
+
} else {
|
|
48
|
+
setIsVisiable(false);
|
|
49
|
+
setConflicts([]);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
disposables.add(
|
|
54
|
+
editorModel.instance.getMonacoModel().onDidChangeContent(() => {
|
|
55
|
+
run();
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
run();
|
|
59
|
+
return () => {
|
|
60
|
+
disposables.dispose();
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}, [editorModel]);
|
|
43
64
|
|
|
44
|
-
const [
|
|
65
|
+
const [isAIResolving, setIsAIResolving] = useState(false);
|
|
45
66
|
const handleOpenMergeEditor = useCallback(async () => {
|
|
46
67
|
const { uri } = resource;
|
|
47
68
|
|
|
@@ -52,31 +73,35 @@ export const MergeEditorFloatComponents: ReactEditorComponent<{ uri: URI }> = ({
|
|
|
52
73
|
});
|
|
53
74
|
}, [resource]);
|
|
54
75
|
|
|
55
|
-
const
|
|
76
|
+
const isSupportAIResolve = useCallback(
|
|
56
77
|
() => aiNativeConfigService.capabilities.supportsConflictResolve,
|
|
57
78
|
[aiNativeConfigService],
|
|
58
79
|
);
|
|
59
80
|
|
|
60
|
-
const handlePrev = () => {
|
|
61
|
-
commandService.tryExecuteCommand(
|
|
62
|
-
|
|
81
|
+
const handlePrev = useCallback(() => {
|
|
82
|
+
commandService.tryExecuteCommand(MergeConflictCommands.Previous).then(() => {
|
|
83
|
+
// TODO: 编辑器向上滚动一行
|
|
84
|
+
});
|
|
85
|
+
}, []);
|
|
63
86
|
|
|
64
|
-
const handleNext = () => {
|
|
65
|
-
commandService.tryExecuteCommand(
|
|
66
|
-
|
|
87
|
+
const handleNext = useCallback(() => {
|
|
88
|
+
commandService.tryExecuteCommand(MergeConflictCommands.Next).then(() => {
|
|
89
|
+
// TODO: 编辑器向上滚动一行
|
|
90
|
+
});
|
|
91
|
+
}, []);
|
|
67
92
|
|
|
68
93
|
const handleAIResolve = useCallback(async () => {
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
await commandService.executeCommand(
|
|
94
|
+
setIsAIResolving(true);
|
|
95
|
+
if (isAIResolving) {
|
|
96
|
+
await commandService.executeCommand(MERGE_CONFLICT_COMMANDS.AI_ALL_ACCEPT_STOP.id, resource.uri);
|
|
72
97
|
} else {
|
|
73
|
-
await commandService.executeCommand(
|
|
98
|
+
await commandService.executeCommand(MERGE_CONFLICT_COMMANDS.AI_ALL_ACCEPT.id, resource.uri);
|
|
74
99
|
}
|
|
75
|
-
|
|
76
|
-
}, [resource,
|
|
100
|
+
setIsAIResolving(false);
|
|
101
|
+
}, [resource, isAIResolving]);
|
|
77
102
|
|
|
78
103
|
const handleReset = useCallback(() => {
|
|
79
|
-
commandService.executeCommand(
|
|
104
|
+
commandService.executeCommand(MERGE_CONFLICT_COMMANDS.ALL_RESET.id, resource.uri);
|
|
80
105
|
}, [resource]);
|
|
81
106
|
|
|
82
107
|
if (!isVisiable) {
|
|
@@ -85,50 +110,60 @@ export const MergeEditorFloatComponents: ReactEditorComponent<{ uri: URI }> = ({
|
|
|
85
110
|
|
|
86
111
|
return (
|
|
87
112
|
<div className={styles.merge_editor_float_container}>
|
|
88
|
-
<div
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
{isSupportAiResolve() && (
|
|
113
|
+
<div className={styles.merge_editor_float_container_info}>剩余未解决冲突 {conflicts.length} 处</div>
|
|
114
|
+
<div className={styles.merge_editor_float_container_operation_bar}>
|
|
115
|
+
<div id='merge.editor.action.button.nav'>
|
|
116
|
+
<Button className={styles.merge_conflict_bottom_btn} size='default' onClick={handlePrev}>
|
|
117
|
+
<Icon icon={'left'} />
|
|
118
|
+
<span>{localize('mergeEditor.conflict.prev')}</span>
|
|
119
|
+
</Button>
|
|
120
|
+
<Button className={styles.merge_conflict_bottom_btn} size='default' onClick={handleNext}>
|
|
121
|
+
<span>{localize('mergeEditor.conflict.next')}</span>
|
|
122
|
+
<Icon icon={'right'} />
|
|
123
|
+
</Button>
|
|
124
|
+
</div>
|
|
125
|
+
<span className={styles.line_vertical}></span>
|
|
126
|
+
{inMergeChanges && (
|
|
127
|
+
<Button
|
|
128
|
+
id='merge.editor.open.tradition'
|
|
129
|
+
className={styles.merge_conflict_bottom_btn}
|
|
130
|
+
size='default'
|
|
131
|
+
onClick={handleOpenMergeEditor}
|
|
132
|
+
>
|
|
133
|
+
<Icon icon={'swap'} />
|
|
134
|
+
<span>{localize('mergeEditor.open.3way')}</span>
|
|
135
|
+
</Button>
|
|
136
|
+
)}
|
|
113
137
|
<Button
|
|
114
|
-
id='merge.editor.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
onClick={
|
|
138
|
+
id='merge.editor.rest'
|
|
139
|
+
className={styles.merge_conflict_bottom_btn}
|
|
140
|
+
size='default'
|
|
141
|
+
onClick={handleReset}
|
|
118
142
|
>
|
|
119
|
-
{
|
|
120
|
-
|
|
121
|
-
<Icon icon={'circle-pause'} />
|
|
122
|
-
<span>{localize('mergeEditor.conflict.resolve.all.stop')}</span>
|
|
123
|
-
</>
|
|
124
|
-
) : (
|
|
125
|
-
<>
|
|
126
|
-
<Icon icon={'magic-wand'} />
|
|
127
|
-
<span>{localize('mergeEditor.conflict.resolve.all')}</span>
|
|
128
|
-
</>
|
|
129
|
-
)}
|
|
143
|
+
<Icon icon={'discard'} />
|
|
144
|
+
<span>{localize('mergeEditor.reset')}</span>
|
|
130
145
|
</Button>
|
|
131
|
-
|
|
146
|
+
{isSupportAIResolve() && (
|
|
147
|
+
<Button
|
|
148
|
+
id='merge.editor.conflict.resolve.all'
|
|
149
|
+
size='default'
|
|
150
|
+
className={`${styles.merge_conflict_bottom_btn} ${styles.magic_btn}`}
|
|
151
|
+
onClick={handleAIResolve}
|
|
152
|
+
>
|
|
153
|
+
{isAIResolving ? (
|
|
154
|
+
<>
|
|
155
|
+
<Icon icon={'circle-pause'} />
|
|
156
|
+
<span>{localize('mergeEditor.conflict.ai.resolve.all.stop')}</span>
|
|
157
|
+
</>
|
|
158
|
+
) : (
|
|
159
|
+
<>
|
|
160
|
+
<Icon icon={'magic-wand'} />
|
|
161
|
+
<span>{localize('mergeEditor.conflict.ai.resolve.all')}</span>
|
|
162
|
+
</>
|
|
163
|
+
)}
|
|
164
|
+
</Button>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
132
167
|
</div>
|
|
133
168
|
);
|
|
134
169
|
};
|