Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/merge-conflict/src/mergeDecorator.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
8
9
export default class MergeDecorator implements vscode.Disposable {
10
11
private decorations: { [key: string]: vscode.TextEditorDecorationType } = {};
12
13
private decorationUsesWholeLine: boolean = true; // Useful for debugging, set to false to see exact match ranges
14
15
private config?: interfaces.IExtensionConfiguration;
16
private tracker: interfaces.IDocumentMergeConflictTracker;
17
private updating = new Map<vscode.TextEditor, boolean>();
18
19
constructor(private context: vscode.ExtensionContext, trackerService: interfaces.IDocumentMergeConflictTrackerService) {
20
this.tracker = trackerService.createTracker('decorator');
21
}
22
23
begin(config: interfaces.IExtensionConfiguration) {
24
this.config = config;
25
this.registerDecorationTypes(config);
26
27
// Check if we already have a set of active windows, attempt to track these.
28
vscode.window.visibleTextEditors.forEach(e => this.applyDecorations(e));
29
30
vscode.workspace.onDidOpenTextDocument(event => {
31
this.applyDecorationsFromEvent(event);
32
}, null, this.context.subscriptions);
33
34
vscode.workspace.onDidChangeTextDocument(event => {
35
this.applyDecorationsFromEvent(event.document);
36
}, null, this.context.subscriptions);
37
38
vscode.window.onDidChangeVisibleTextEditors((e) => {
39
// Any of which could be new (not just the active one).
40
e.forEach(e => this.applyDecorations(e));
41
}, null, this.context.subscriptions);
42
}
43
44
configurationUpdated(config: interfaces.IExtensionConfiguration) {
45
this.config = config;
46
this.registerDecorationTypes(config);
47
48
// Re-apply the decoration
49
vscode.window.visibleTextEditors.forEach(e => {
50
this.removeDecorations(e);
51
this.applyDecorations(e);
52
});
53
}
54
55
private registerDecorationTypes(config: interfaces.IExtensionConfiguration) {
56
57
// Dispose of existing decorations
58
Object.keys(this.decorations).forEach(k => this.decorations[k].dispose());
59
this.decorations = {};
60
61
// None of our features are enabled
62
if (!config.enableDecorations || !config.enableEditorOverview) {
63
return;
64
}
65
66
// Create decorators
67
if (config.enableDecorations || config.enableEditorOverview) {
68
this.decorations['current.content'] = vscode.window.createTextEditorDecorationType(
69
this.generateBlockRenderOptions('merge.currentContentBackground', 'editorOverviewRuler.currentContentForeground', config)
70
);
71
72
this.decorations['incoming.content'] = vscode.window.createTextEditorDecorationType(
73
this.generateBlockRenderOptions('merge.incomingContentBackground', 'editorOverviewRuler.incomingContentForeground', config)
74
);
75
76
this.decorations['commonAncestors.content'] = vscode.window.createTextEditorDecorationType(
77
this.generateBlockRenderOptions('merge.commonContentBackground', 'editorOverviewRuler.commonContentForeground', config)
78
);
79
}
80
81
if (config.enableDecorations) {
82
this.decorations['current.header'] = vscode.window.createTextEditorDecorationType({
83
isWholeLine: this.decorationUsesWholeLine,
84
backgroundColor: new vscode.ThemeColor('merge.currentHeaderBackground'),
85
color: new vscode.ThemeColor('editor.foreground'),
86
outlineStyle: 'solid',
87
outlineWidth: '1pt',
88
outlineColor: new vscode.ThemeColor('merge.border'),
89
after: {
90
contentText: ' ' + vscode.l10n.t("(Current Change)"),
91
color: new vscode.ThemeColor('descriptionForeground')
92
}
93
});
94
95
this.decorations['commonAncestors.header'] = vscode.window.createTextEditorDecorationType({
96
isWholeLine: this.decorationUsesWholeLine,
97
backgroundColor: new vscode.ThemeColor('merge.commonHeaderBackground'),
98
color: new vscode.ThemeColor('editor.foreground'),
99
outlineStyle: 'solid',
100
outlineWidth: '1pt',
101
outlineColor: new vscode.ThemeColor('merge.border')
102
});
103
104
this.decorations['splitter'] = vscode.window.createTextEditorDecorationType({
105
color: new vscode.ThemeColor('editor.foreground'),
106
outlineStyle: 'solid',
107
outlineWidth: '1pt',
108
outlineColor: new vscode.ThemeColor('merge.border'),
109
isWholeLine: this.decorationUsesWholeLine,
110
});
111
112
this.decorations['incoming.header'] = vscode.window.createTextEditorDecorationType({
113
backgroundColor: new vscode.ThemeColor('merge.incomingHeaderBackground'),
114
color: new vscode.ThemeColor('editor.foreground'),
115
outlineStyle: 'solid',
116
outlineWidth: '1pt',
117
outlineColor: new vscode.ThemeColor('merge.border'),
118
isWholeLine: this.decorationUsesWholeLine,
119
after: {
120
contentText: ' ' + vscode.l10n.t("(Incoming Change)"),
121
color: new vscode.ThemeColor('descriptionForeground')
122
}
123
});
124
}
125
}
126
127
dispose() {
128
129
// TODO: Replace with Map<string, T>
130
Object.keys(this.decorations).forEach(name => {
131
this.decorations[name].dispose();
132
});
133
134
this.decorations = {};
135
}
136
137
private generateBlockRenderOptions(backgroundColor: string, overviewRulerColor: string, config: interfaces.IExtensionConfiguration): vscode.DecorationRenderOptions {
138
139
const renderOptions: vscode.DecorationRenderOptions = {};
140
141
if (config.enableDecorations) {
142
renderOptions.backgroundColor = new vscode.ThemeColor(backgroundColor);
143
renderOptions.isWholeLine = this.decorationUsesWholeLine;
144
}
145
146
if (config.enableEditorOverview) {
147
renderOptions.overviewRulerColor = new vscode.ThemeColor(overviewRulerColor);
148
renderOptions.overviewRulerLane = vscode.OverviewRulerLane.Full;
149
}
150
151
return renderOptions;
152
}
153
154
private applyDecorationsFromEvent(eventDocument: vscode.TextDocument) {
155
for (const editor of vscode.window.visibleTextEditors) {
156
if (editor.document === eventDocument) {
157
// Attempt to apply
158
this.applyDecorations(editor);
159
}
160
}
161
}
162
163
private async applyDecorations(editor: vscode.TextEditor) {
164
if (!editor || !editor.document) { return; }
165
166
if (!this.config || (!this.config.enableDecorations && !this.config.enableEditorOverview)) {
167
return;
168
}
169
170
// If we have a pending scan from the same origin, exit early. (Cannot use this.tracker.isPending() because decorations are per editor.)
171
if (this.updating.get(editor)) {
172
return;
173
}
174
175
try {
176
this.updating.set(editor, true);
177
178
const conflicts = await this.tracker.getConflicts(editor.document);
179
if (vscode.window.visibleTextEditors.indexOf(editor) === -1) {
180
return;
181
}
182
183
if (conflicts.length === 0) {
184
this.removeDecorations(editor);
185
return;
186
}
187
188
// Store decorations keyed by the type of decoration, set decoration wants a "style"
189
// to go with it, which will match this key (see constructor);
190
const matchDecorations: { [key: string]: vscode.Range[] } = {};
191
192
const pushDecoration = (key: string, d: vscode.Range) => {
193
matchDecorations[key] = matchDecorations[key] || [];
194
matchDecorations[key].push(d);
195
};
196
197
conflicts.forEach(conflict => {
198
// TODO, this could be more effective, just call getMatchPositions once with a map of decoration to position
199
if (!conflict.current.decoratorContent.isEmpty) {
200
pushDecoration('current.content', conflict.current.decoratorContent);
201
}
202
if (!conflict.incoming.decoratorContent.isEmpty) {
203
pushDecoration('incoming.content', conflict.incoming.decoratorContent);
204
}
205
206
conflict.commonAncestors.forEach(commonAncestorsRegion => {
207
if (!commonAncestorsRegion.decoratorContent.isEmpty) {
208
pushDecoration('commonAncestors.content', commonAncestorsRegion.decoratorContent);
209
}
210
});
211
212
if (this.config!.enableDecorations) {
213
pushDecoration('current.header', conflict.current.header);
214
pushDecoration('splitter', conflict.splitter);
215
pushDecoration('incoming.header', conflict.incoming.header);
216
217
conflict.commonAncestors.forEach(commonAncestorsRegion => {
218
pushDecoration('commonAncestors.header', commonAncestorsRegion.header);
219
});
220
}
221
});
222
223
// For each match we've generated, apply the generated decoration with the matching decoration type to the
224
// editor instance. Keys in both matches and decorations should match.
225
Object.keys(matchDecorations).forEach(decorationKey => {
226
const decorationType = this.decorations[decorationKey];
227
228
if (decorationType) {
229
editor.setDecorations(decorationType, matchDecorations[decorationKey]);
230
}
231
});
232
233
} finally {
234
this.updating.delete(editor);
235
}
236
}
237
238
private removeDecorations(editor: vscode.TextEditor) {
239
// Remove all decorations, there might be none
240
Object.keys(this.decorations).forEach(decorationKey => {
241
242
// Race condition, while editing the settings, it's possible to
243
// generate regions before the configuration has been refreshed
244
const decorationType = this.decorations[decorationKey];
245
246
if (decorationType) {
247
editor.setDecorations(decorationType, []);
248
}
249
});
250
}
251
}
252
253