Skip to content

Commit 34d8743

Browse files
authored
Improves merge editor code-lens (microsoft#161830)
* Disable diff projection * Improves merge editor code-lens
1 parent bcbf0d8 commit 34d8743

File tree

8 files changed

+697
-529
lines changed

8 files changed

+697
-529
lines changed

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import { EditorModel } from 'vs/workbench/common/editor/editorModel';
2323
import { MergeEditorInputData } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput';
2424
import { MergeDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer';
2525
import { InputData, MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel';
26-
import { ProjectedDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/projectedDocumentDiffProvider';
2726
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
2827
import { ITextFileEditorModel, ITextFileSaveOptions, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
2928

@@ -109,7 +108,7 @@ export class TempFileMergeEditorModeFactory implements IMergeEditorInputModelFac
109108
input2Data,
110109
temporaryResultModel,
111110
this._instantiationService.createInstance(MergeDiffComputer, diffProvider),
112-
this._instantiationService.createInstance(MergeDiffComputer, this._instantiationService.createInstance(ProjectedDiffComputer, diffProvider)),
111+
this._instantiationService.createInstance(MergeDiffComputer, diffProvider),
113112
{
114113
resetResult: true,
115114
}
@@ -312,7 +311,7 @@ export class WorkspaceMergeEditorModeFactory implements IMergeEditorInputModelFa
312311
input2Data,
313312
result.object.textEditorModel,
314313
this._instantiationService.createInstance(MergeDiffComputer, diffProvider),
315-
this._instantiationService.createInstance(MergeDiffComputer, this._instantiationService.createInstance(ProjectedDiffComputer, diffProvider)),
314+
this._instantiationService.createInstance(MergeDiffComputer, diffProvider),
316315
{
317316
resetResult: true
318317
}
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
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 { h, $, reset, createStyleSheet, isInShadowDOM } from 'vs/base/browser/dom';
7+
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
8+
import { hash } from 'vs/base/common/hash';
9+
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
10+
import { autorun, derived, IObservable, transaction } from 'vs/base/common/observable';
11+
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
12+
import { EditorOption, EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions';
13+
import { localize } from 'vs/nls';
14+
import { ModifiedBaseRange, ModifiedBaseRangeState } from 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange';
15+
import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel';
16+
17+
export class ConflictActionsFactory extends Disposable {
18+
private id = 0;
19+
private readonly _styleClassName: string;
20+
private readonly _styleElement: HTMLStyleElement;
21+
22+
constructor(private readonly _editor: ICodeEditor) {
23+
super();
24+
25+
this._register(this._editor.onDidChangeConfiguration((e) => {
26+
if (e.hasChanged(EditorOption.fontInfo) || e.hasChanged(EditorOption.codeLensFontSize) || e.hasChanged(EditorOption.codeLensFontFamily)) {
27+
this._updateLensStyle();
28+
}
29+
}));
30+
31+
this._styleClassName = '_conflictActionsFactory_' + hash(this._editor.getId()).toString(16);
32+
this._styleElement = createStyleSheet(
33+
isInShadowDOM(this._editor.getContainerDomNode())
34+
? this._editor.getContainerDomNode()
35+
: undefined
36+
);
37+
38+
this._register(toDisposable(() => {
39+
this._styleElement.remove();
40+
}));
41+
42+
this._updateLensStyle();
43+
}
44+
45+
private _updateLensStyle(): void {
46+
47+
const { codeLensHeight, fontSize } = this._getLayoutInfo();
48+
const fontFamily = this._editor.getOption(EditorOption.codeLensFontFamily);
49+
const editorFontInfo = this._editor.getOption(EditorOption.fontInfo);
50+
51+
const fontFamilyVar = `--codelens-font-family${this._styleClassName}`;
52+
const fontFeaturesVar = `--codelens-font-features${this._styleClassName}`;
53+
54+
let newStyle = `
55+
.${this._styleClassName} { line-height: ${codeLensHeight}px; font-size: ${fontSize}px; padding-right: ${Math.round(fontSize * 0.5)}px; font-feature-settings: var(${fontFeaturesVar}) }
56+
.monaco-workbench .${this._styleClassName} span.codicon { line-height: ${codeLensHeight}px; font-size: ${fontSize}px; }
57+
`;
58+
if (fontFamily) {
59+
newStyle += `${this._styleClassName} { font-family: var(${fontFamilyVar}), ${EDITOR_FONT_DEFAULTS.fontFamily}}`;
60+
}
61+
this._styleElement.textContent = newStyle;
62+
this._editor.getContainerDomNode().style.setProperty(fontFamilyVar, fontFamily ?? 'inherit');
63+
this._editor.getContainerDomNode().style.setProperty(fontFeaturesVar, editorFontInfo.fontFeatureSettings);
64+
}
65+
66+
private _getLayoutInfo() {
67+
const lineHeightFactor = Math.max(1.3, this._editor.getOption(EditorOption.lineHeight) / this._editor.getOption(EditorOption.fontSize));
68+
let fontSize = this._editor.getOption(EditorOption.codeLensFontSize);
69+
if (!fontSize || fontSize < 5) {
70+
fontSize = (this._editor.getOption(EditorOption.fontSize) * .9) | 0;
71+
}
72+
return {
73+
fontSize,
74+
codeLensHeight: (fontSize * lineHeightFactor) | 0,
75+
};
76+
}
77+
78+
createContentWidget(lineNumber: number, viewModel: MergeEditorViewModel, modifiedBaseRange: ModifiedBaseRange, inputNumber: 1 | 2): IContentWidget {
79+
80+
function command(title: string, action: () => Promise<void>): IContentWidgetAction {
81+
return {
82+
text: title,
83+
action
84+
};
85+
}
86+
87+
const items = derived('items', reader => {
88+
const state = viewModel.model.getState(modifiedBaseRange).read(reader);
89+
const handled = viewModel.model.isHandled(modifiedBaseRange).read(reader);
90+
const model = viewModel.model;
91+
92+
const result: IContentWidgetAction[] = [];
93+
94+
const inputData = inputNumber === 1 ? viewModel.model.input1 : viewModel.model.input2;
95+
const showNonConflictingChanges = viewModel.showNonConflictingChanges.read(reader);
96+
97+
if (!modifiedBaseRange.isConflicting && handled && !showNonConflictingChanges) {
98+
return [];
99+
}
100+
101+
const otherInputNumber = inputNumber === 1 ? 2 : 1;
102+
103+
if (!state.conflicting && !state.isInputIncluded(inputNumber)) {
104+
result.push(
105+
!state.isInputIncluded(inputNumber)
106+
? command(localize('accept', "$(pass) Accept {0}", inputData.title), async () => {
107+
transaction((tx) => {
108+
model.setState(
109+
modifiedBaseRange,
110+
state.withInputValue(inputNumber, true),
111+
true,
112+
tx
113+
);
114+
});
115+
})
116+
: command(localize('remove', "$(error) Remove ${0}", inputData.title), async () => {
117+
transaction((tx) => {
118+
model.setState(
119+
modifiedBaseRange,
120+
state.withInputValue(inputNumber, false),
121+
true,
122+
tx
123+
);
124+
});
125+
}),
126+
);
127+
128+
if (modifiedBaseRange.canBeCombined && state.isEmpty) {
129+
result.push(
130+
state.input1 && state.input2
131+
? command(localize('removeBoth', "$(error) Remove Both"), async () => {
132+
transaction((tx) => {
133+
model.setState(
134+
modifiedBaseRange,
135+
ModifiedBaseRangeState.default,
136+
true,
137+
tx
138+
);
139+
});
140+
})
141+
: command(localize('acceptBoth', "$(pass) Accept Both"), async () => {
142+
transaction((tx) => {
143+
model.setState(
144+
modifiedBaseRange,
145+
state
146+
.withInputValue(inputNumber, true)
147+
.withInputValue(otherInputNumber, true),
148+
true,
149+
tx
150+
);
151+
});
152+
}),
153+
);
154+
}
155+
}
156+
return result;
157+
});
158+
return new ActionsContentWidget((this.id++).toString(), this._styleClassName, lineNumber, items);
159+
}
160+
161+
createResultWidget(lineNumber: number, viewModel: MergeEditorViewModel, modifiedBaseRange: ModifiedBaseRange): IContentWidget {
162+
163+
function command(title: string, action: () => Promise<void>): IContentWidgetAction {
164+
return {
165+
text: title,
166+
action
167+
};
168+
}
169+
170+
const items = derived('items', reader => {
171+
const state = viewModel.model.getState(modifiedBaseRange).read(reader);
172+
const model = viewModel.model;
173+
174+
const result: IContentWidgetAction[] = [];
175+
176+
const stateLabel = ((state: ModifiedBaseRangeState): string => {
177+
if (state.conflicting) {
178+
return localize('manualResolution', "Manual Resolution");
179+
} else if (state.isEmpty) {
180+
return localize('noChangesAccepted', 'No Changes Accepted');
181+
} else {
182+
const labels = [];
183+
if (state.input1) {
184+
labels.push(model.input1.title);
185+
}
186+
if (state.input2) {
187+
labels.push(model.input2.title);
188+
}
189+
if (state.input2First) {
190+
labels.reverse();
191+
}
192+
return `${labels.join(' + ')}`;
193+
}
194+
})(state);
195+
196+
result.push({
197+
text: stateLabel,
198+
action: async () => { },
199+
});
200+
201+
202+
const stateToggles: IContentWidgetAction[] = [];
203+
if (state.input1) {
204+
result.push(command(localize('remove', "$(error) Remove ${0}", model.input1.title), async () => {
205+
transaction((tx) => {
206+
model.setState(
207+
modifiedBaseRange,
208+
state.withInputValue(1, false),
209+
true,
210+
tx
211+
);
212+
});
213+
}),
214+
);
215+
}
216+
if (state.input2) {
217+
result.push(command(localize('remove', "$(error) Remove ${0}", model.input2.title), async () => {
218+
transaction((tx) => {
219+
model.setState(
220+
modifiedBaseRange,
221+
state.withInputValue(2, false),
222+
true,
223+
tx
224+
);
225+
});
226+
}),
227+
);
228+
}
229+
if (state.input2First) {
230+
stateToggles.reverse();
231+
}
232+
result.push(...stateToggles);
233+
234+
235+
236+
if (state.conflicting) {
237+
result.push(
238+
command(localize('resetToBase', "$(error) Reset to base"), async () => {
239+
transaction((tx) => {
240+
model.setState(
241+
modifiedBaseRange,
242+
ModifiedBaseRangeState.default,
243+
true,
244+
tx
245+
);
246+
});
247+
})
248+
);
249+
}
250+
return result;
251+
});
252+
return new ActionsContentWidget((this.id++).toString(), this._styleClassName, lineNumber, items);
253+
}
254+
}
255+
256+
257+
interface IContentWidgetAction {
258+
text: string;
259+
hover?: string;
260+
action: () => Promise<void>;
261+
}
262+
263+
class ActionsContentWidget extends Disposable implements IContentWidget {
264+
private readonly _domNode = h('div.merge-editor-conflict-actions').root;
265+
266+
constructor(
267+
private readonly id: string,
268+
className: string,
269+
private readonly lineNumber: number,
270+
items: IObservable<IContentWidgetAction[]>,
271+
) {
272+
super();
273+
274+
this._domNode.classList.add(className);
275+
276+
this._register(autorun('update commands', (reader) => {
277+
const i = items.read(reader);
278+
this.setState(i);
279+
}));
280+
}
281+
282+
private setState(items: IContentWidgetAction[]) {
283+
const children: HTMLElement[] = [];
284+
let isFirst = true;
285+
for (const item of items) {
286+
if (isFirst) {
287+
isFirst = false;
288+
} else {
289+
children.push($('span', undefined, '\u00a0|\u00a0'));
290+
}
291+
const title = renderLabelWithIcons(item.text);
292+
children.push($('a', { title: item.hover, role: 'button', onclick: () => item.action() }, ...title));
293+
}
294+
295+
reset(this._domNode, ...children);
296+
}
297+
298+
getId(): string {
299+
return this.id;
300+
}
301+
302+
getDomNode(): HTMLElement {
303+
return this._domNode;
304+
}
305+
306+
getPosition(): IContentWidgetPosition | null {
307+
return {
308+
position: { lineNumber: this.lineNumber, column: 1, },
309+
preference: [ContentWidgetPositionPreference.BELOW],
310+
};
311+
}
312+
}

src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,6 @@ export abstract class CodeEditorView extends Disposable {
6464
() => /** @description checkboxesVisible */ this.configurationService.getValue('mergeEditor.showCheckboxes') ?? true
6565
);
6666

67-
protected readonly codeLensesVisible = observableFromEvent<boolean>(
68-
this.configurationService.onDidChangeConfiguration,
69-
() => /** @description codeLensesVisible */ this.configurationService.getValue('mergeEditor.showCodeLenses') ?? false
70-
);
71-
7267
public readonly editor = this.instantiationService.createInstance(
7368
CodeEditorWidget,
7469
this.htmlElements.editor,

0 commit comments

Comments
 (0)