diff --git a/src/vs/base/browser/ui/toggle/toggle.ts b/src/vs/base/browser/ui/toggle/toggle.ts index dafff0bf5ff..8d2387dbc26 100644 --- a/src/vs/base/browser/ui/toggle/toggle.ts +++ b/src/vs/base/browser/ui/toggle/toggle.ts @@ -98,6 +98,7 @@ export class Toggle extends Widget { readonly onKeyDown: Event = 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 */; } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css b/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css index 363dffaaf6a..14b0fa9449c 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css +++ b/src/vs/workbench/contrib/mergeEditor/browser/media/mergeEditor.css @@ -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); +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts index 8c95edcc1fa..fd930b99bad 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.ts @@ -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((baseRange, idx) => ({ + .map((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; setState(value: boolean, tx: ITransaction | undefined): void; } -class MergeConflictGutterItemView extends Disposable implements IGutterItemView { - constructor(private item: MergeConflictData, target: HTMLElement) { +class MergeConflictGutterItemView extends Disposable implements IGutterItemView { + 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; } } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts index 2c23f66b502..9e32d53e6cc 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorModel.ts @@ -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(); + 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[] { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model.ts b/src/vs/workbench/contrib/mergeEditor/browser/model.ts index 9ee3176fc92..27329b56105 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model.ts @@ -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 ); } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts index cf5aaa569d7..7dca29bf50b 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts @@ -14,6 +14,20 @@ import { IDisposable } from 'xterm'; export class ReentrancyBarrier { private isActive = false; + public makeExclusive(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(tag: TTag): never; -export function n( +export function n)[]>( tag: TTag, children: T ): (ArrayToObj & Record<'root', TagToElement>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never; @@ -48,7 +62,7 @@ export function n( tag: TTag, attributes: { $: TId } ): Record>; -export function n( +export function n)[]>( 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; }