Implements tri-state checkbox, some scrolling bug-fixing and checkbox styling improvements.

This commit is contained in:
Henning Dieterichs
2022-05-30 15:10:17 +02:00
parent 6cdb77160b
commit 30aa8cc0fb
6 changed files with 218 additions and 86 deletions
+13 -1
View File
@@ -98,6 +98,7 @@ export class Toggle extends Widget {
readonly onKeyDown: Event<IKeyboardEvent> = this._onKeyDown.event;
private readonly _opts: IToggleOpts;
private _icon: CSSIcon | undefined;
readonly domNode: HTMLElement;
private _checked: boolean;
@@ -110,7 +111,8 @@ export class Toggle extends Widget {
const classes = ['monaco-custom-toggle'];
if (this._opts.icon) {
classes.push(...CSSIcon.asClassNameArray(this._opts.icon));
this._icon = this._opts.icon;
classes.push(...CSSIcon.asClassNameArray(this._icon));
}
if (this._opts.actionClassName) {
classes.push(...this._opts.actionClassName.split(' '));
@@ -174,6 +176,16 @@ export class Toggle extends Widget {
this.applyStyles();
}
setIcon(icon: CSSIcon | undefined): void {
if (this._icon) {
this.domNode.classList.remove(...CSSIcon.asClassNameArray(this._icon));
}
this._icon = icon;
if (this._icon) {
this.domNode.classList.add(...CSSIcon.asClassNameArray(this._icon));
}
}
width(): number {
return 2 /*margin left*/ + 2 /*border*/ + 2 /*padding*/ + 16 /* icon width */;
}
@@ -71,8 +71,19 @@
justify-content: center;
}
.merge-accept-gutter-marker .checkbox .accept-conflict-group.monaco-custom-toggle.monaco-checkbox {
margin: 0;
padding: 0;
.accept-conflict-group.monaco-custom-toggle {
height: 18px;
width: 18px;
border: 1px solid transparent;
border-radius: 3px;
margin-right: 0px;
margin-left: 0px;
padding: 0px;
opacity: 1;
background-size: 16px !important;
background-color: var(--vscode-checkbox-border);
}
.checkbox-background {
background: var(--vscode-editor-background);
}
@@ -106,48 +106,40 @@ export class MergeEditor extends EditorPane {
this._register(keepAlive(input1ResultMapping));
this._register(keepAlive(input2ResultMapping));
this._store.add(this.input1View.editor.onDidScrollChange(c => {
if (c.scrollTopChanged) {
reentrancyBarrier.runExclusively(() => {
const mapping = input1ResultMapping.get();
if (!mapping) {
return;
this._store.add(
this.input1View.editor.onDidScrollChange(
reentrancyBarrier.makeExclusive((c) => {
if (c.scrollTopChanged) {
const mapping = input1ResultMapping.get();
synchronizeScrolling(this.input1View.editor, this.inputResultView.editor, mapping, 1);
this.input2View.editor.setScrollTop(c.scrollTop, ScrollType.Immediate);
}
synchronizeScrolling(this.input1View.editor, this.inputResultView.editor, mapping, 1);
this.input2View.editor.setScrollTop(c.scrollTop, ScrollType.Immediate);
});
}
}));
this._store.add(this.input2View.editor.onDidScrollChange(c => {
if (c.scrollTopChanged) {
reentrancyBarrier.runExclusively(() => {
const mapping = input2ResultMapping.get();
if (!mapping) {
return;
})
)
);
this._store.add(
this.input2View.editor.onDidScrollChange(
reentrancyBarrier.makeExclusive((c) => {
if (c.scrollTopChanged) {
const mapping = input2ResultMapping.get();
synchronizeScrolling(this.input2View.editor, this.inputResultView.editor, mapping, 1);
this.input1View.editor.setScrollTop(c.scrollTop, ScrollType.Immediate);
}
synchronizeScrolling(this.input2View.editor, this.inputResultView.editor, mapping, 1);
this.input1View.editor.setScrollTop(c.scrollTop, ScrollType.Immediate);
});
}
}));
this._store.add(this.inputResultView.editor.onDidScrollChange(c => {
if (c.scrollTopChanged) {
reentrancyBarrier.runExclusively(() => {
const mapping = input1ResultMapping.get();
if (!mapping) {
return;
})
)
);
this._store.add(
this.inputResultView.editor.onDidScrollChange(
reentrancyBarrier.makeExclusive((c) => {
if (c.scrollTopChanged) {
const mapping1 = input1ResultMapping.get();
synchronizeScrolling(this.inputResultView.editor, this.input1View.editor, mapping1, 2);
const mapping2 = input2ResultMapping.get();
synchronizeScrolling(this.inputResultView.editor, this.input2View.editor, mapping2, 2);
}
synchronizeScrolling(this.inputResultView.editor, this.input1View.editor, mapping, 2);
const mapping2 = input2ResultMapping.get();
if (!mapping2) {
return;
}
synchronizeScrolling(this.inputResultView.editor, this.input2View.editor, mapping2, 2);
});
}
}));
})
)
);
// TODO@jrieken make this proper: add menu id and allow extensions to contribute
@@ -307,11 +299,11 @@ export class MergeEditor extends EditorPane {
}
}
function flip(value: 1 | 2): 1 | 2 {
return value === 1 ? 2 : 1;
}
function synchronizeScrolling(scrollingEditor: CodeEditorWidget, targetEditor: CodeEditorWidget, mapping: ModifiedBaseRange[] | undefined, sourceNumber: 1 | 2) {
if (!mapping) {
return;
}
function synchronizeScrolling(scrollingEditor: CodeEditorWidget, targetEditor: CodeEditorWidget, mapping: ModifiedBaseRange[], sourceNumber: 1 | 2) {
const visibleRanges = scrollingEditor.getVisibleRanges();
if (visibleRanges.length === 0) {
return;
@@ -322,21 +314,24 @@ function synchronizeScrolling(scrollingEditor: CodeEditorWidget, targetEditor: C
let sourceRange: LineRange;
let targetRange: LineRange;
const targetNumber = flip(sourceNumber);
const targetNumber = sourceNumber === 1 ? 2 : 1;
if (firstBefore && firstBefore.getInputRange(sourceNumber).contains(topLineNumber)) {
sourceRange = firstBefore.getInputRange(sourceNumber);
targetRange = firstBefore.getInputRange(targetNumber);
} else if (firstBefore && firstBefore.getInputRange(sourceNumber).isEmpty && firstBefore.getInputRange(sourceNumber).startLineNumber === topLineNumber) {
sourceRange = firstBefore.getInputRange(sourceNumber).deltaEnd(1);
targetRange = firstBefore.getInputRange(targetNumber).deltaEnd(1);
const firstBeforeSourceRange = firstBefore?.getInputRange(sourceNumber);
const firstBeforeTargetRange = firstBefore?.getInputRange(targetNumber);
if (firstBeforeSourceRange && firstBeforeSourceRange.contains(topLineNumber)) {
sourceRange = firstBeforeSourceRange;
targetRange = firstBeforeTargetRange!;
} else if (firstBeforeSourceRange && firstBeforeSourceRange.isEmpty && firstBeforeSourceRange.startLineNumber === topLineNumber) {
sourceRange = firstBeforeSourceRange.deltaEnd(1);
targetRange = firstBeforeTargetRange!.deltaEnd(1);
} else {
const delta = firstBefore ? firstBefore.getInputRange(targetNumber).endLineNumberExclusive - firstBefore.getInputRange(sourceNumber).endLineNumberExclusive : 0;
const delta = firstBeforeSourceRange ? firstBeforeTargetRange!.endLineNumberExclusive - firstBeforeSourceRange.endLineNumberExclusive : 0;
sourceRange = new LineRange(topLineNumber, 1);
targetRange = new LineRange(topLineNumber + delta, 1);
}
// sourceRange is not empty!
// sourceRange contains topLineNumber!
const resultStartTopPx = targetEditor.getTopForLineNumber(targetRange.startLineNumber);
const resultEndPx = targetEditor.getTopForLineNumber(targetRange.endLineNumberExclusive);
@@ -344,8 +339,21 @@ function synchronizeScrolling(scrollingEditor: CodeEditorWidget, targetEditor: C
const sourceStartTopPx = scrollingEditor.getTopForLineNumber(sourceRange.startLineNumber);
const sourceEndPx = scrollingEditor.getTopForLineNumber(sourceRange.endLineNumberExclusive);
const factor = (scrollingEditor.getScrollTop() - sourceStartTopPx) / (sourceEndPx - sourceStartTopPx);
const factor = Math.min((scrollingEditor.getScrollTop() - sourceStartTopPx) / (sourceEndPx - sourceStartTopPx), 1);
const resultScrollPosition = resultStartTopPx + (resultEndPx - resultStartTopPx) * factor;
/*
console.log({
topLineNumber,
sourceRange: sourceRange.toString(),
targetRange: targetRange.toString(),
// resultStartTopPx,
// resultEndPx,
// sourceStartTopPx,
// sourceEndPx,
factor,
resultScrollPosition,
top: scrollingEditor.getScrollTop(),
});*/
targetEditor.setScrollTop(resultScrollPosition, ScrollType.Immediate);
}
@@ -468,7 +476,7 @@ class InputCodeEditorView extends CodeEditorView {
if (!model) { return []; }
return model.modifiedBaseRanges
.filter((r) => r.getInputDiffs(this.inputNumber).length > 0)
.map<MergeConflictData>((baseRange, idx) => ({
.map<ModifiedBaseRangeGutterItemInfo>((baseRange, idx) => ({
id: idx.toString(),
additionalHeightInPx: 0,
offsetInPx: 0,
@@ -497,25 +505,31 @@ class InputCodeEditorView extends CodeEditorView {
}
}
interface MergeConflictData extends IGutterItemInfo {
interface ModifiedBaseRangeGutterItemInfo extends IGutterItemInfo {
toggleState: IObservable<boolean | undefined>;
setState(value: boolean, tx: ITransaction | undefined): void;
}
class MergeConflictGutterItemView extends Disposable implements IGutterItemView<MergeConflictData> {
constructor(private item: MergeConflictData, target: HTMLElement) {
class MergeConflictGutterItemView extends Disposable implements IGutterItemView<ModifiedBaseRangeGutterItemInfo> {
constructor(private item: ModifiedBaseRangeGutterItemInfo, private readonly target: HTMLElement) {
super();
target.classList.add('merge-accept-gutter-marker');
target.classList.add(item.range.lineCount > 1 ? 'multi-line' : 'single-line');
// TODO: Tri-State-Toggle, localized title
const checkBox = new Toggle({ isChecked: false, title: 'Accept Merge', icon: Codicon.check, actionClassName: 'monaco-checkbox' });
// TODO: localized title
const checkBox = new Toggle({ isChecked: false, title: 'Accept Merge', icon: Codicon.check });
checkBox.domNode.classList.add('accept-conflict-group');
this._register(
autorun((reader) => {
const value = this.item.toggleState.read(reader);
checkBox.setIcon(
value === true
? Codicon.check
: value === false
? undefined
: Codicon.circleFilled
);
checkBox.checked = value === true;
}, 'Update Toggle State')
);
@@ -524,15 +538,19 @@ class MergeConflictGutterItemView extends Disposable implements IGutterItemView<
this.item.setState(checkBox.checked, undefined);
}));
target.appendChild($('div.background', {}, noBreakWhitespace));
target.appendChild($('div.checkbox', {}, checkBox.domNode));
target.appendChild(n('div.background', [noBreakWhitespace]).root);
target.appendChild(
n('div.checkbox', [n('div.checkbox-background', [checkBox.domNode])]).root
);
}
layout(top: number, height: number, viewTop: number, viewHeight: number): void {
this.target.classList.remove('multi-line');
this.target.classList.remove('single-line');
this.target.classList.add(height > 30 ? 'multi-line' : 'single-line');
}
update(baseRange: MergeConflictData): void {
update(baseRange: ModifiedBaseRangeGutterItemInfo): void {
this.item = baseRange;
}
}
@@ -113,6 +113,8 @@ export class MergeEditorModel extends EditorModel {
this.recomputeState();
});
this.recomputeState();
this.resetUnknown();
}
private recomputeState(): void {
@@ -135,6 +137,16 @@ export class MergeEditorModel extends EditorModel {
});
}
public resetUnknown(): void {
transaction(tx => {
for (const range of this.modifiedBaseRanges) {
if (this.getState(range).get().conflicting) {
this.setState(range, ModifiedBaseRangeState.default, tx);
}
}
});
}
public mergeNonConflictingDiffs(): void {
transaction((tx) => {
for (const m of this.modifiedBaseRanges) {
@@ -215,7 +227,7 @@ export class MergeEditorModel extends EditorModel {
if (!existingState) {
throw new BugIndicatingError('object must be from this instance');
}
existingState.set(state, transaction);
const conflictingDiffs = this.resultEdits.findTouchingDiffs(
baseRange.baseRange
@@ -224,13 +236,73 @@ export class MergeEditorModel extends EditorModel {
this.resultEdits.removeDiffs(conflictingDiffs, transaction);
}
const diff = state.input1
? baseRange.input1CombinedDiff
: state.input2
? baseRange.input2CombinedDiff
: undefined;
if (diff) {
this.resultEdits.applyEditRelativeToOriginal(diff.getLineEdit(), transaction);
function getEdit(baseRange: ModifiedBaseRange, state: ModifiedBaseRangeState): { edit: LineEdit | undefined; effectiveState: ModifiedBaseRangeState } {
interface LineDiffWithInputNumber {
diff: LineDiff;
inputNumber: 1 | 2;
}
const diffs = new Array<LineDiffWithInputNumber>();
if (state.input1) {
if (baseRange.input1CombinedDiff) {
diffs.push({ diff: baseRange.input1CombinedDiff, inputNumber: 1 });
}
}
if (state.input2) {
if (baseRange.input2CombinedDiff) {
diffs.push({ diff: baseRange.input2CombinedDiff, inputNumber: 2 });
}
}
if (state.input2First) {
diffs.reverse();
}
const firstDiff: LineDiffWithInputNumber | undefined = diffs[0];
const secondDiff: LineDiffWithInputNumber | undefined = diffs[1];
diffs.sort(compareBy(d => d.diff.originalRange, LineRange.compareByStart));
if (!firstDiff) {
return { edit: undefined, effectiveState: state };
}
if (!secondDiff) {
return { edit: firstDiff.diff.getLineEdit(), effectiveState: state };
}
// Two inserts
if (
firstDiff.diff.originalRange.lineCount === 0 &&
firstDiff.diff.originalRange.equals(secondDiff.diff.originalRange)
) {
return {
edit: new LineEdit(
firstDiff.diff.originalRange,
firstDiff.diff
.getLineEdit()
.newLines.concat(secondDiff.diff.getLineEdit().newLines)
),
effectiveState: state,
};
}
// Technically non-conflicting diffs
if (diffs.length === 2 && diffs[0].diff.originalRange.endLineNumberExclusive === diffs[1].diff.originalRange.startLineNumber) {
return {
edit: new LineEdit(
LineRange.join(diffs.map(d => d.diff.originalRange))!,
diffs.flatMap(d => d.diff.getLineEdit().newLines)
),
effectiveState: state,
};
}
return { edit: firstDiff.diff.getLineEdit(), effectiveState: state };
}
const { edit, effectiveState } = getEdit(baseRange, state);
existingState.set(effectiveState, transaction);
if (edit) {
this.resultEdits.applyEditRelativeToOriginal(edit, transaction);
}
}
@@ -359,11 +431,11 @@ class ResultEdits {
new LineRange(edit.range.startLineNumber + delta, edit.newLines.length)
));
}
this._diffs.set(newDiffs, transaction);
this.barrier.runExclusivelyOrThrow(() => {
new LineEdit(edit.range.delta(delta), edit.newLines).apply(this.resultTextModel);
});
this._diffs.set(newDiffs, transaction);
}
public findTouchingDiffs(baseRange: LineRange): LineDiff[] {
@@ -371,7 +371,10 @@ export class ModifiedBaseRangeState {
public readonly conflicting: boolean,
) { }
public getInput(inputNumber: 1 | 2): boolean {
public getInput(inputNumber: 1 | 2): boolean | undefined {
if (this.conflicting) {
return undefined;
}
if (inputNumber === 1) {
return this.input1;
} else {
@@ -386,17 +389,17 @@ export class ModifiedBaseRangeState {
public withInput1(value: boolean): ModifiedBaseRangeState {
return new ModifiedBaseRangeState(
value,
false,
value && this.isEmpty ? false : this.input2First,
this.input2,
value !== this.input2 ? this.input2 : this.input2First,
false,
);
}
public withInput2(value: boolean): ModifiedBaseRangeState {
return new ModifiedBaseRangeState(
false,
this.input1,
value,
value && this.isEmpty ? true : this.input2First,
value !== this.input1 ? value : this.input2First,
false
);
}
@@ -14,6 +14,20 @@ import { IDisposable } from 'xterm';
export class ReentrancyBarrier {
private isActive = false;
public makeExclusive<TFunction extends Function>(fn: TFunction): TFunction {
return ((...args: any[]) => {
if (this.isActive) {
return;
}
this.isActive = true;
try {
return fn(...args);
} finally {
this.isActive = false;
}
}) as any;
}
public runExclusively(fn: () => void): void {
if (this.isActive) {
return;
@@ -40,7 +54,7 @@ export class ReentrancyBarrier {
}
export function n<TTag extends string>(tag: TTag): never;
export function n<TTag extends string, T extends any[]>(
export function n<TTag extends string, T extends (HTMLElement | string | Record<string, HTMLElement>)[]>(
tag: TTag,
children: T
): (ArrayToObj<T> & Record<'root', TagToElement<TTag>>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
@@ -48,7 +62,7 @@ export function n<TTag extends string, TId extends string>(
tag: TTag,
attributes: { $: TId }
): Record<TId, TagToElement<TTag>>;
export function n<TTag extends string, TId extends string, T extends any[]>(
export function n<TTag extends string, TId extends string, T extends (HTMLElement | string | Record<string, HTMLElement>)[]>(
tag: TTag,
attributes: { $: TId },
children: T
@@ -77,6 +91,8 @@ export function n(tag: string, ...args: [] | [attributes: { $: string } | Record
for (const c of children) {
if (c instanceof HTMLElement) {
el.appendChild(c);
} else if (typeof c === 'string') {
el.append(c);
} else {
Object.assign(result, c);
el.appendChild(c.root);
@@ -84,8 +100,6 @@ export function n(tag: string, ...args: [] | [attributes: { $: string } | Record
}
}
result['root'] = el;
for (const [key, value] of Object.entries(attributes)) {
if (key === '$') {
result[value] = el;
@@ -94,6 +108,8 @@ export function n(tag: string, ...args: [] | [attributes: { $: string } | Record
el.setAttribute(key, value);
}
result['root'] = el;
return result;
}