Merge pull request #151508 from microsoft/3wm

This commit is contained in:
Henning Dieterichs 2022-06-10 08:59:09 +02:00 committed by GitHub
commit de13d28ee4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 564 additions and 435 deletions

View file

@ -6,7 +6,9 @@
import { Event } from 'vs/base/common/event';
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
export interface IObservable<T> {
export interface IObservable<T, TChange = void> {
_change: TChange;
/**
* Reads the current value.
*
@ -39,7 +41,7 @@ export interface IReader {
*
* Is called by `Observable.read`.
*/
handleBeforeReadObservable<T>(observable: IObservable<T>): void;
handleBeforeReadObservable<T>(observable: IObservable<T, any>): void;
}
export interface IObserver {
@ -61,7 +63,7 @@ export interface IObserver {
* Implementations must not call into other observables!
* The change should be processed when {@link IObserver.endUpdate} is called.
*/
handleChange<T>(observable: IObservable<T>): void;
handleChange<T, TChange>(observable: IObservable<T, TChange>, change: TChange): void;
/**
* Indicates that an update operation has completed.
@ -69,8 +71,8 @@ export interface IObserver {
endUpdate<T>(observable: IObservable<T>): void;
}
export interface ISettable<T> {
set(value: T, transaction: ITransaction | undefined): void;
export interface ISettable<T, TChange = void> {
set(value: T, transaction: ITransaction | undefined, change: TChange): void;
}
export interface ITransaction {
@ -80,12 +82,14 @@ export interface ITransaction {
*/
updateObserver(
observer: IObserver,
observable: IObservable<any>
observable: IObservable<any, any>
): void;
}
// === Base ===
export abstract class ConvenientObservable<T> implements IObservable<T> {
export abstract class ConvenientObservable<T, TChange> implements IObservable<T, TChange> {
get _change(): TChange { return null!; }
public abstract get(): T;
public abstract subscribe(observer: IObserver): void;
public abstract unsubscribe(observer: IObserver): void;
@ -100,7 +104,7 @@ export abstract class ConvenientObservable<T> implements IObservable<T> {
}
}
export abstract class BaseObservable<T> extends ConvenientObservable<T> {
export abstract class BaseObservable<T, TChange = void> extends ConvenientObservable<T, TChange> {
protected readonly observers = new Set<IObserver>();
public subscribe(observer: IObserver): void {
@ -151,9 +155,9 @@ class TransactionImpl implements ITransaction {
}
}
export class ObservableValue<T>
extends BaseObservable<T>
implements ISettable<T>
export class ObservableValue<T, TChange = void>
extends BaseObservable<T, TChange>
implements ISettable<T, TChange>
{
private value: T;
@ -166,14 +170,14 @@ export class ObservableValue<T>
return this.value;
}
public set(value: T, tx: ITransaction | undefined): void {
public set(value: T, tx: ITransaction | undefined, change: TChange): void {
if (this.value === value) {
return;
}
if (!tx) {
transaction((tx) => {
this.set(value, tx);
this.set(value, tx, change);
});
return;
}
@ -182,7 +186,7 @@ export class ObservableValue<T>
for (const observer of this.observers) {
tx.updateObserver(observer, this);
observer.handleChange(this);
observer.handleChange(this, change);
}
}
}
@ -191,7 +195,7 @@ export function constObservable<T>(value: T): IObservable<T> {
return new ConstObservable(value);
}
class ConstObservable<T> extends ConvenientObservable<T> {
class ConstObservable<T> extends ConvenientObservable<T, void> {
constructor(private readonly value: T) {
super();
}
@ -208,11 +212,28 @@ class ConstObservable<T> extends ConvenientObservable<T> {
}
// == autorun ==
export function autorun(
fn: (reader: IReader) => void,
name: string
export function autorun(fn: (reader: IReader) => void, name: string): IDisposable {
return new AutorunObserver(fn, name, undefined);
}
interface IChangeContext {
readonly changedObservable: IObservable<any, any>;
readonly change: unknown;
didChange<T, TChange>(observable: IObservable<T, TChange>): this is { change: TChange };
}
export function autorunHandleChanges(
name: string,
options: {
/**
* Returns if this change should cause a re-run of the autorun.
*/
handleChange: (context: IChangeContext) => boolean;
},
fn: (reader: IReader) => void
): IDisposable {
return new AutorunObserver(fn, name);
return new AutorunObserver(fn, name, options.handleChange);
}
export function autorunWithStore(
@ -252,7 +273,8 @@ export class AutorunObserver implements IObserver, IReader, IDisposable {
constructor(
private readonly runFn: (reader: IReader) => void,
public readonly name: string
public readonly name: string,
private readonly _handleChange: ((context: IChangeContext) => boolean) | undefined
) {
this.runIfNeeded();
}
@ -264,8 +286,13 @@ export class AutorunObserver implements IObserver, IReader, IDisposable {
}
}
public handleChange() {
this.needsToRun = true;
public handleChange<T, TChange>(observable: IObservable<T, TChange>, change: TChange): void {
const shouldReact = this._handleChange ? this._handleChange({
changedObservable: observable,
change,
didChange: o => o === observable as any,
}) : true;
this.needsToRun = this.needsToRun || shouldReact;
if (this.updateCount === 0) {
this.runIfNeeded();
@ -337,7 +364,7 @@ export function autorunDelta<T>(
export function derivedObservable<T>(name: string, computeFn: (reader: IReader) => T): IObservable<T> {
return new LazyDerived(computeFn, name);
}
export class LazyDerived<T> extends ConvenientObservable<T> {
export class LazyDerived<T> extends ConvenientObservable<T, void> {
private readonly observer: LazyDerivedObserver<T>;
constructor(computeFn: (reader: IReader) => T, name: string) {
@ -366,7 +393,7 @@ export class LazyDerived<T> extends ConvenientObservable<T> {
* @internal
*/
class LazyDerivedObserver<T>
extends BaseObservable<T>
extends BaseObservable<T, void>
implements IReader, IObserver {
private hadValue = false;
private hasValue = false;
@ -486,9 +513,8 @@ class LazyDerivedObserver<T>
this.hasValue = true;
if (this.hadValue && oldValue !== this.value) {
//
for (const r of this.observers) {
r.handleChange(this);
r.handleChange(this, undefined);
}
}
}
@ -508,6 +534,20 @@ export function observableFromPromise<T>(promise: Promise<T>): IObservable<{ val
return observable;
}
export function waitForState<T, TState extends T>(observable: IObservable<T>, predicate: (state: T) => state is TState): Promise<TState>;
export function waitForState<T>(observable: IObservable<T>, predicate: (state: T) => boolean): Promise<T>;
export function waitForState<T>(observable: IObservable<T>, predicate: (state: T) => boolean): Promise<T> {
return new Promise(resolve => {
const d = autorun(reader => {
const currentState = observable.read(reader);
if (predicate(currentState)) {
d.dispose();
resolve(currentState);
}
}, 'waitForState');
});
}
export function observableFromEvent<T, TArgs = unknown>(
event: Event<TArgs>,
getValue: (args: TArgs | undefined) => T
@ -540,7 +580,7 @@ class FromEventObservable<TArgs, T> extends BaseObservable<T> {
transaction(tx => {
for (const o of this.observers) {
tx.updateObserver(o, this);
o.handleChange(this);
o.handleChange(this, undefined);
}
});
}
@ -622,3 +662,12 @@ export function keepAlive(observable: IObservable<any>): IDisposable {
observable.read(reader);
}, 'keep-alive');
}
export function derivedObservableWithCache<T>(name: string, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T> {
let lastValue: T | undefined = undefined;
const observable = derivedObservable(name, reader => {
lastValue = computeFn(reader, lastValue);
return lastValue;
});
return observable;
}

View file

@ -30,6 +30,7 @@ import { ITextResourceConfigurationService } from 'vs/editor/common/services/tex
import { localize } from 'vs/nls';
import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IMenuService, MenuId } from 'vs/platform/actions/common/actions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
@ -42,7 +43,7 @@ import { DEFAULT_EDITOR_MAX_DIMENSIONS, DEFAULT_EDITOR_MIN_DIMENSIONS } from 'vs
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { IEditorControl, IEditorOpenContext } from 'vs/workbench/common/editor';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { autorun, derivedObservable, IObservable, ITransaction, keepAlive, ObservableValue } from 'vs/workbench/contrib/audioCues/browser/observable';
import { autorun, autorunWithStore, derivedObservable, IObservable, ITransaction, keepAlive, ObservableValue, transaction } from 'vs/workbench/contrib/audioCues/browser/observable';
import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput';
import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorModel';
import { LineRange, ModifiedBaseRange } from 'vs/workbench/contrib/mergeEditor/browser/model';
@ -61,8 +62,8 @@ export class MergeEditor extends EditorPane {
private _grid!: Grid<IView>;
private readonly input1View = this.instantiation.createInstance(InputCodeEditorView, 1, { readonly: true });
private readonly input2View = this.instantiation.createInstance(InputCodeEditorView, 2, { readonly: true });
private readonly input1View = this.instantiation.createInstance(InputCodeEditorView, 1, { readonly: !this.inputsWritable });
private readonly input2View = this.instantiation.createInstance(InputCodeEditorView, 2, { readonly: !this.inputsWritable });
private readonly inputResultView = this.instantiation.createInstance(ResultCodeEditorView, { readonly: false });
private readonly _ctxIsMergeEditor: IContextKey<boolean>;
@ -71,6 +72,10 @@ export class MergeEditor extends EditorPane {
private _model: MergeEditorModel | undefined;
public get model(): MergeEditorModel | undefined { return this._model; }
private get inputsWritable(): boolean {
return !!this._configurationService.getValue<boolean>('mergeEditor.writableInputs');
}
constructor(
@IInstantiationService private readonly instantiation: IInstantiationService,
@ILabelService private readonly _labelService: ILabelService,
@ -80,6 +85,7 @@ export class MergeEditor extends EditorPane {
@IStorageService storageService: IStorageService,
@IThemeService themeService: IThemeService,
@ITextResourceConfigurationService private readonly textResourceConfigurationService: ITextResourceConfigurationService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
) {
super(MergeEditor.ID, telemetryService, themeService, storageService);
@ -94,7 +100,7 @@ export class MergeEditor extends EditorPane {
return undefined;
}
const resultDiffs = model.resultDiffs.read(reader);
const modifiedBaseRanges = ModifiedBaseRange.fromDiffs(model.base, model.input1, model.input1LinesDiffs, model.result, resultDiffs);
const modifiedBaseRanges = ModifiedBaseRange.fromDiffs(model.base, model.input1, model.input1LinesDiffs.read(reader), model.result, resultDiffs);
return modifiedBaseRanges;
});
const input2ResultMapping = derivedObservable('input2ResultMapping', reader => {
@ -103,7 +109,7 @@ export class MergeEditor extends EditorPane {
return undefined;
}
const resultDiffs = model.resultDiffs.read(reader);
const modifiedBaseRanges = ModifiedBaseRange.fromDiffs(model.base, model.input2, model.input2LinesDiffs, model.result, resultDiffs);
const modifiedBaseRanges = ModifiedBaseRange.fromDiffs(model.base, model.input2, model.input2LinesDiffs.read(reader), model.result, resultDiffs);
return modifiedBaseRanges;
});
@ -228,42 +234,44 @@ export class MergeEditor extends EditorPane {
// TODO: Update editor options!
const input1ViewZoneIds: string[] = [];
const input2ViewZoneIds: string[] = [];
for (const m of model.modifiedBaseRanges) {
const max = Math.max(m.input1Range.lineCount, m.input2Range.lineCount, 1);
this._sessionDisposables.add(autorunWithStore((reader, store) => {
const input1ViewZoneIds: string[] = [];
const input2ViewZoneIds: string[] = [];
for (const m of model.modifiedBaseRanges.read(reader)) {
const max = Math.max(m.input1Range.lineCount, m.input2Range.lineCount, 1);
this.input1View.editor.changeViewZones(a => {
input1ViewZoneIds.push(a.addZone({
afterLineNumber: m.input1Range.endLineNumberExclusive - 1,
heightInLines: max - m.input1Range.lineCount,
domNode: $('div.diagonal-fill'),
}));
});
this.input2View.editor.changeViewZones(a => {
input2ViewZoneIds.push(a.addZone({
afterLineNumber: m.input2Range.endLineNumberExclusive - 1,
heightInLines: max - m.input2Range.lineCount,
domNode: $('div.diagonal-fill'),
}));
});
}
this._sessionDisposables.add({
dispose: () => {
this.input1View.editor.changeViewZones(a => {
for (const zone of input1ViewZoneIds) {
a.removeZone(zone);
}
input1ViewZoneIds.push(a.addZone({
afterLineNumber: m.input1Range.endLineNumberExclusive - 1,
heightInLines: max - m.input1Range.lineCount,
domNode: $('div.diagonal-fill'),
}));
});
this.input2View.editor.changeViewZones(a => {
for (const zone of input2ViewZoneIds) {
a.removeZone(zone);
}
input2ViewZoneIds.push(a.addZone({
afterLineNumber: m.input2Range.endLineNumberExclusive - 1,
heightInLines: max - m.input2Range.lineCount,
domNode: $('div.diagonal-fill'),
}));
});
}
});
store.add({
dispose: () => {
this.input1View.editor.changeViewZones(a => {
for (const zone of input1ViewZoneIds) {
a.removeZone(zone);
}
});
this.input2View.editor.changeViewZones(a => {
for (const zone of input2ViewZoneIds) {
a.removeZone(zone);
}
});
}
});
}, 'update alignment view zones'));
}
protected override setEditorVisible(visible: boolean): void {
@ -448,7 +456,7 @@ class InputCodeEditorView extends CodeEditorView {
return [];
}
const result = new Array<IModelDeltaDecoration>();
for (const m of model.modifiedBaseRanges) {
for (const m of model.modifiedBaseRanges.read(reader)) {
const range = m.getInputRange(this.inputNumber);
if (!range.isEmpty) {
result.push({
@ -478,13 +486,14 @@ class InputCodeEditorView extends CodeEditorView {
getIntersectingGutterItems: (range, reader) => {
const model = this.model.read(reader);
if (!model) { return []; }
return model.modifiedBaseRanges
return model.modifiedBaseRanges.read(reader)
.filter((r) => r.getInputDiffs(this.inputNumber).length > 0)
.map<ModifiedBaseRangeGutterItemInfo>((baseRange, idx) => ({
id: idx.toString(),
additionalHeightInPx: 0,
offsetInPx: 0,
range: baseRange.getInputRange(this.inputNumber),
enabled: model.isUpToDate,
toggleState: derivedObservable('toggle', (reader) =>
model
.getState(baseRange)
@ -510,14 +519,19 @@ class InputCodeEditorView extends CodeEditorView {
}
interface ModifiedBaseRangeGutterItemInfo extends IGutterItemInfo {
enabled: IObservable<boolean>;
toggleState: IObservable<boolean | undefined>;
setState(value: boolean, tx: ITransaction | undefined): void;
setState(value: boolean, tx: ITransaction): void;
}
class MergeConflictGutterItemView extends Disposable implements IGutterItemView<ModifiedBaseRangeGutterItemInfo> {
constructor(private item: ModifiedBaseRangeGutterItemInfo, private readonly target: HTMLElement) {
private readonly item = new ObservableValue<ModifiedBaseRangeGutterItemInfo | undefined>(undefined, 'item');
constructor(item: ModifiedBaseRangeGutterItemInfo, private readonly target: HTMLElement) {
super();
this.item.set(item, undefined);
target.classList.add('merge-accept-gutter-marker');
// TODO: localized title
@ -526,7 +540,8 @@ class MergeConflictGutterItemView extends Disposable implements IGutterItemView<
this._register(
autorun((reader) => {
const value = this.item.toggleState.read(reader);
const item = this.item.read(reader)!;
const value = item.toggleState.read(reader);
checkBox.setIcon(
value === true
? Codicon.check
@ -535,11 +550,19 @@ class MergeConflictGutterItemView extends Disposable implements IGutterItemView<
: Codicon.circleFilled
);
checkBox.checked = value === true;
if (!item.enabled.read(reader)) {
checkBox.disable();
} else {
checkBox.enable();
}
}, 'Update Toggle State')
);
this._register(checkBox.onChange(() => {
this.item.setState(checkBox.checked, undefined);
transaction(tx => {
this.item.get()!.setState(checkBox.checked, tx);
});
}));
target.appendChild(n('div.background', [noBreakWhitespace]).root);
@ -555,7 +578,7 @@ class MergeConflictGutterItemView extends Disposable implements IGutterItemView<
}
update(baseRange: ModifiedBaseRangeGutterItemInfo): void {
this.item = baseRange;
this.item.set(baseRange, undefined);
}
}

View file

@ -14,7 +14,7 @@ import { ILabelService } from 'vs/platform/label/common/label';
import { IUntypedEditorInput, EditorInputCapabilities } from 'vs/workbench/common/editor';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput';
import { MergeEditorModel, MergeEditorModelFactory } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorModel';
import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorModel';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
@ -39,10 +39,9 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput {
private _model?: MergeEditorModel;
private _outTextModel?: ITextFileEditorModel;
private readonly mergeEditorModelFactory = this._instaService.createInstance(MergeEditorModelFactory);
constructor(
private readonly _anchestor: URI,
private readonly _base: URI,
private readonly _input1: MergeEditorInputData,
private readonly _input2: MergeEditorInputData,
private readonly _result: URI,
@ -101,13 +100,14 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput {
if (!this._model) {
const anchestor = await this._textModelService.createModelReference(this._anchestor);
const base = await this._textModelService.createModelReference(this._base);
const input1 = await this._textModelService.createModelReference(this._input1.uri);
const input2 = await this._textModelService.createModelReference(this._input2.uri);
const result = await this._textModelService.createModelReference(this._result);
this._model = await this.mergeEditorModelFactory.create(
anchestor.object.textEditorModel,
this._model = this._instaService.createInstance(
MergeEditorModel,
base.object.textEditorModel,
input1.object.textEditorModel,
this._input1.detail,
this._input1.description,
@ -117,13 +117,13 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput {
result.object.textEditorModel
);
await this._model.onInitialized;
this._store.add(this._model);
this._store.add(anchestor);
this._store.add(base);
this._store.add(input1);
this._store.add(input2);
this._store.add(result);
// result.object.
}
return this._model;
}
@ -132,7 +132,7 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput {
if (!(otherInput instanceof MergeEditorInput)) {
return false;
}
return isEqual(this._anchestor, otherInput._anchestor)
return isEqual(this._base, otherInput._base)
&& isEqual(this._input1.uri, otherInput._input1.uri)
&& isEqual(this._input2.uri, otherInput._input2.uri)
&& isEqual(this._result, otherInput._result);
@ -140,7 +140,7 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput {
toJSON(): MergeEditorInputJSON {
return {
anchestor: this._anchestor,
anchestor: this._base,
inputOne: this._input1,
inputTwo: this._input2,
result: this._result,

View file

@ -3,96 +3,72 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter } from 'vs/base/common/event';
import { compareBy, CompareResult, equals, numberComparator } from 'vs/base/common/arrays';
import { compareBy, CompareResult, equals } from 'vs/base/common/arrays';
import { BugIndicatingError } from 'vs/base/common/errors';
import { ITextModel } from 'vs/editor/common/model';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
import { EditorModel } from 'vs/workbench/common/editor/editorModel';
import { IObservable, ITransaction, ObservableValue, transaction } from 'vs/workbench/contrib/audioCues/browser/observable';
import { ModifiedBaseRange, LineEdit, LineDiff, ModifiedBaseRangeState, LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model';
import { leftJoin, ReentrancyBarrier } from 'vs/workbench/contrib/mergeEditor/browser/utils';
import { autorunHandleChanges, derivedObservable, derivedObservableWithCache, IObservable, ITransaction, keepAlive, ObservableValue, transaction, waitForState } from 'vs/workbench/contrib/audioCues/browser/observable';
import { LineDiff, LineEdit, LineRange, ModifiedBaseRange, ModifiedBaseRangeState } from 'vs/workbench/contrib/mergeEditor/browser/model';
import { EditorWorkerServiceDiffComputer, TextModelDiffChangeReason, TextModelDiffs, TextModelDiffState } from 'vs/workbench/contrib/mergeEditor/browser/textModelDiffs';
import { leftJoin } from 'vs/workbench/contrib/mergeEditor/browser/utils';
export class MergeEditorModelFactory {
constructor(
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService
) {
}
public async create(
base: ITextModel,
input1: ITextModel,
input1Detail: string | undefined,
input1Description: string | undefined,
input2: ITextModel,
input2Detail: string | undefined,
input2Description: string | undefined,
result: ITextModel,
): Promise<MergeEditorModel> {
const baseToInput1DiffPromise = this._editorWorkerService.computeDiff(
base.uri,
input1.uri,
false,
1000
);
const baseToInput2DiffPromise = this._editorWorkerService.computeDiff(
base.uri,
input2.uri,
false,
1000
);
const baseToResultDiffPromise = this._editorWorkerService.computeDiff(
base.uri,
result.uri,
false,
1000
);
const [baseToInput1Diff, baseToInput2Diff, baseToResultDiff] = await Promise.all([
baseToInput1DiffPromise,
baseToInput2DiffPromise,
baseToResultDiffPromise
]);
const changesInput1 =
baseToInput1Diff?.changes.map((c) =>
LineDiff.fromLineChange(c, base, input1)
) || [];
const changesInput2 =
baseToInput2Diff?.changes.map((c) =>
LineDiff.fromLineChange(c, base, input2)
) || [];
const changesResult =
baseToResultDiff?.changes.map((c) =>
LineDiff.fromLineChange(c, base, result)
) || [];
return new MergeEditorModel(
InternalSymbol,
base,
input1,
input1Detail,
input1Description,
input2,
input2Detail,
input2Description,
result,
changesInput1,
changesInput2,
changesResult,
this._editorWorkerService,
);
}
export const enum MergeEditorModelState {
initializing = 1,
upToDate = 2,
updating = 3,
}
const InternalSymbol: unique symbol = null!;
export class MergeEditorModel extends EditorModel {
private resultEdits: ResultEdits;
private readonly diffComputer = new EditorWorkerServiceDiffComputer(this.editorWorkerService);
private readonly input1TextModelDiffs = new TextModelDiffs(this.base, this.input1, this.diffComputer);
private readonly input2TextModelDiffs = new TextModelDiffs(this.base, this.input2, this.diffComputer);
private readonly resultTextModelDiffs = new TextModelDiffs(this.base, this.result, this.diffComputer);
public readonly state = derivedObservable('state', reader => {
const states = [
this.input1TextModelDiffs,
this.input2TextModelDiffs,
this.resultTextModelDiffs,
].map((s) => s.state.read(reader));
if (states.some((s) => s === TextModelDiffState.initializing)) {
return MergeEditorModelState.initializing;
}
if (states.some((s) => s === TextModelDiffState.updating)) {
return MergeEditorModelState.updating;
}
return MergeEditorModelState.upToDate;
});
public readonly isUpToDate = derivedObservable('isUpdating', reader => this.state.read(reader) === MergeEditorModelState.upToDate);
public readonly onInitialized = waitForState(this.state, state => state === MergeEditorModelState.upToDate);
public readonly modifiedBaseRanges = derivedObservableWithCache<ModifiedBaseRange[]>('modifiedBaseRanges', (reader, lastValue) => {
if (this.state.read(reader) !== MergeEditorModelState.upToDate) {
return lastValue || [];
}
const input1Diffs = this.input1TextModelDiffs.diffs.read(reader);
const input2Diffs = this.input2TextModelDiffs.diffs.read(reader);
return ModifiedBaseRange.fromDiffs(this.base, this.input1, input1Diffs, this.input2, input2Diffs);
});
public readonly input1LinesDiffs = this.input1TextModelDiffs.diffs;
public readonly input2LinesDiffs = this.input2TextModelDiffs.diffs;
public readonly resultDiffs = this.resultTextModelDiffs.diffs;
private readonly modifiedBaseRangeStateStores =
derivedObservable('modifiedBaseRangeStateStores', reader => {
const map = new Map(
this.modifiedBaseRanges.read(reader).map(s => ([s, new ObservableValue(ModifiedBaseRangeState.default, 'State')]))
);
return map;
});
constructor(
_symbol: typeof InternalSymbol,
readonly base: ITextModel,
readonly input1: ITextModel,
readonly input1Detail: string | undefined,
@ -101,27 +77,43 @@ export class MergeEditorModel extends EditorModel {
readonly input2Detail: string | undefined,
readonly input2Description: string | undefined,
readonly result: ITextModel,
public readonly input1LinesDiffs: readonly LineDiff[],
public readonly input2LinesDiffs: readonly LineDiff[],
resultDiffs: LineDiff[],
private readonly editorWorkerService: IEditorWorkerService
@IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService
) {
super();
this.resultEdits = new ResultEdits(resultDiffs, this.base, this.result, this.editorWorkerService);
this.resultEdits.onDidChange(() => {
this.recomputeState();
});
this.recomputeState();
this._register(keepAlive(this.modifiedBaseRangeStateStores));
this.resetUnknown();
this._register(
autorunHandleChanges(
'Recompute State',
{
handleChange: (ctx) =>
ctx.didChange(this.resultTextModelDiffs.diffs)
// Ignore non-text changes as we update the state directly
? ctx.change === TextModelDiffChangeReason.textChange
: true,
},
(reader) => {
if (!this.isUpToDate.read(reader)) {
return;
}
const resultDiffs = this.resultTextModelDiffs.diffs.read(reader);
const stores = this.modifiedBaseRangeStateStores.read(reader);
this.recomputeState(resultDiffs, stores);
}
)
);
this.onInitialized.then(() => {
this.resetUnknown();
});
}
private recomputeState(): void {
private recomputeState(resultDiffs: LineDiff[], stores: Map<ModifiedBaseRange, ObservableValue<ModifiedBaseRangeState>>): void {
transaction(tx => {
const baseRangeWithStoreAndTouchingDiffs = leftJoin(
this.modifiedBaseRangeStateStores,
this.resultEdits.diffs.get(),
stores,
resultDiffs,
(baseRange, diff) =>
baseRange[0].baseRange.touches(diff.originalRange)
? CompareResult.neitherLessOrGreaterThan
@ -132,14 +124,14 @@ export class MergeEditorModel extends EditorModel {
);
for (const row of baseRangeWithStoreAndTouchingDiffs) {
row.left[1].set(this.computeState(row.left[0], row.rights), tx);
row.left[1].set(computeState(row.left[0], row.rights), tx);
}
});
}
public resetUnknown(): void {
transaction(tx => {
for (const range of this.modifiedBaseRanges) {
for (const range of this.modifiedBaseRanges.get()) {
if (this.getState(range).get().conflicting) {
this.setState(range, ModifiedBaseRangeState.default, tx);
}
@ -149,7 +141,7 @@ export class MergeEditorModel extends EditorModel {
public mergeNonConflictingDiffs(): void {
transaction((tx) => {
for (const m of this.modifiedBaseRanges) {
for (const m of this.modifiedBaseRanges.get()) {
if (m.isConflicting) {
continue;
}
@ -164,54 +156,8 @@ export class MergeEditorModel extends EditorModel {
});
}
public get resultDiffs(): IObservable<readonly LineDiff[]> {
return this.resultEdits.diffs;
}
public readonly modifiedBaseRanges = ModifiedBaseRange.fromDiffs(
this.base,
this.input1,
this.input1LinesDiffs,
this.input2,
this.input2LinesDiffs
);
private readonly modifiedBaseRangeStateStores: ReadonlyMap<ModifiedBaseRange, ObservableValue<ModifiedBaseRangeState>> = new Map(
this.modifiedBaseRanges.map(s => ([s, new ObservableValue(ModifiedBaseRangeState.default, 'State')]))
);
private computeState(baseRange: ModifiedBaseRange, conflictingDiffs?: LineDiff[]): ModifiedBaseRangeState {
if (!conflictingDiffs) {
conflictingDiffs = this.resultEdits.findTouchingDiffs(
baseRange.baseRange
);
}
if (conflictingDiffs.length === 0) {
return ModifiedBaseRangeState.default;
}
const conflictingEdits = conflictingDiffs.map((d) => d.getLineEdit());
function editsAgreeWithDiffs(diffs: readonly LineDiff[]): boolean {
return equals(
conflictingEdits,
diffs.map((d) => d.getLineEdit()),
(a, b) => a.equals(b)
);
}
if (editsAgreeWithDiffs(baseRange.input1Diffs)) {
return ModifiedBaseRangeState.default.withInput1(true);
}
if (editsAgreeWithDiffs(baseRange.input2Diffs)) {
return ModifiedBaseRangeState.default.withInput2(true);
}
return ModifiedBaseRangeState.conflicting;
}
public getState(baseRange: ModifiedBaseRange): IObservable<ModifiedBaseRangeState> {
const existingState = this.modifiedBaseRangeStateStores.get(baseRange);
const existingState = this.modifiedBaseRangeStateStores.get().get(baseRange);
if (!existingState) {
throw new BugIndicatingError('object must be from this instance');
}
@ -221,240 +167,122 @@ export class MergeEditorModel extends EditorModel {
public setState(
baseRange: ModifiedBaseRange,
state: ModifiedBaseRangeState,
transaction: ITransaction | undefined
transaction: ITransaction
): void {
const existingState = this.modifiedBaseRangeStateStores.get(baseRange);
if (!this.isUpToDate.get()) {
throw new BugIndicatingError('Cannot set state while updating');
}
const existingState = this.modifiedBaseRangeStateStores.get().get(baseRange);
if (!existingState) {
throw new BugIndicatingError('object must be from this instance');
}
const conflictingDiffs = this.resultEdits.findTouchingDiffs(
const conflictingDiffs = this.resultTextModelDiffs.findTouchingDiffs(
baseRange.baseRange
);
if (conflictingDiffs) {
this.resultEdits.removeDiffs(conflictingDiffs, transaction);
this.resultTextModelDiffs.removeDiffs(conflictingDiffs, 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);
const { edit, effectiveState } = getEditForBase(baseRange, state);
existingState.set(effectiveState, transaction);
if (edit) {
this.resultEdits.applyEditRelativeToOriginal(edit, transaction);
this.resultTextModelDiffs.applyEditRelativeToOriginal(edit, transaction);
}
}
public getResultRange(baseRange: LineRange): LineRange {
return this.resultEdits.getResultRange(baseRange);
}
}
class ResultEdits {
private readonly barrier = new ReentrancyBarrier();
private readonly onDidChangeEmitter = new Emitter();
public readonly onDidChange = this.onDidChangeEmitter.event;
function getEditForBase(baseRange: ModifiedBaseRange, state: ModifiedBaseRangeState): { edit: LineEdit | undefined; effectiveState: ModifiedBaseRangeState } {
interface LineDiffWithInputNumber {
diff: LineDiff;
inputNumber: 1 | 2;
}
constructor(
diffs: LineDiff[],
private readonly baseTextModel: ITextModel,
private readonly resultTextModel: ITextModel,
private readonly _editorWorkerService: IEditorWorkerService
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: ModifiedBaseRangeState.default };
}
if (!secondDiff) {
return { edit: firstDiff.diff.getLineEdit(), effectiveState: ModifiedBaseRangeState.default.withInputValue(firstDiff.inputNumber, true) };
}
// Two inserts
if (
firstDiff.diff.originalRange.lineCount === 0 &&
firstDiff.diff.originalRange.equals(secondDiff.diff.originalRange)
) {
diffs.sort(compareBy((d) => d.originalRange.startLineNumber, numberComparator));
this._diffs.set(diffs, undefined);
resultTextModel.onDidChangeContent(e => {
this.barrier.runExclusively(() => {
this._editorWorkerService.computeDiff(
baseTextModel.uri,
resultTextModel.uri,
false,
1000
).then(e => {
const diffs =
e?.changes.map((c) =>
LineDiff.fromLineChange(c, baseTextModel, resultTextModel)
) || [];
this._diffs.set(diffs, undefined);
this.onDidChangeEmitter.fire(undefined);
});
});
});
return {
edit: new LineEdit(
firstDiff.diff.originalRange,
firstDiff.diff
.getLineEdit()
.newLines.concat(secondDiff.diff.getLineEdit().newLines)
),
effectiveState: state,
};
}
private readonly _diffs = new ObservableValue<LineDiff[]>([], 'diffs');
public readonly diffs: IObservable<readonly LineDiff[]> = this._diffs;
public removeDiffs(diffToRemoves: LineDiff[], transaction: ITransaction | undefined): void {
diffToRemoves.sort(compareBy((d) => d.originalRange.startLineNumber, numberComparator));
diffToRemoves.reverse();
let diffs = this._diffs.get();
for (const diffToRemove of diffToRemoves) {
// TODO improve performance
const len = diffs.length;
diffs = diffs.filter((d) => d !== diffToRemove);
if (len === diffs.length) {
throw new BugIndicatingError();
}
this.barrier.runExclusivelyOrThrow(() => {
diffToRemove.getReverseLineEdit().apply(this.resultTextModel);
});
diffs = diffs.map((d) =>
d.modifiedRange.isAfter(diffToRemove.modifiedRange)
? new LineDiff(
d.originalTextModel,
d.originalRange,
d.modifiedTextModel,
d.modifiedRange.delta(
diffToRemove.originalRange.lineCount - diffToRemove.modifiedRange.lineCount
)
)
: d
);
}
this._diffs.set(diffs, transaction);
// 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,
};
}
/**
* Edit must be conflict free.
*/
public applyEditRelativeToOriginal(edit: LineEdit, transaction: ITransaction | undefined): void {
let firstAfter = false;
let delta = 0;
const newDiffs = new Array<LineDiff>();
for (const diff of this._diffs.get()) {
if (diff.originalRange.touches(edit.range)) {
throw new BugIndicatingError('Edit must be conflict free.');
} else if (diff.originalRange.isAfter(edit.range)) {
if (!firstAfter) {
firstAfter = true;
newDiffs.push(new LineDiff(
this.baseTextModel,
edit.range,
this.resultTextModel,
new LineRange(edit.range.startLineNumber + delta, edit.newLines.length)
));
}
newDiffs.push(new LineDiff(
diff.originalTextModel,
diff.originalRange,
diff.modifiedTextModel,
diff.modifiedRange.delta(edit.newLines.length - edit.range.lineCount)
));
} else {
newDiffs.push(diff);
}
if (!firstAfter) {
delta += diff.modifiedRange.lineCount - diff.originalRange.lineCount;
}
}
if (!firstAfter) {
firstAfter = true;
newDiffs.push(new LineDiff(
this.baseTextModel,
edit.range,
this.resultTextModel,
new LineRange(edit.range.startLineNumber + delta, edit.newLines.length)
));
}
this.barrier.runExclusivelyOrThrow(() => {
new LineEdit(edit.range.delta(delta), edit.newLines).apply(this.resultTextModel);
});
this._diffs.set(newDiffs, transaction);
}
public findTouchingDiffs(baseRange: LineRange): LineDiff[] {
return this.diffs.get().filter(d => d.originalRange.touches(baseRange));
}
public getResultRange(baseRange: LineRange): LineRange {
let startOffset = 0;
let lengthOffset = 0;
for (const diff of this.diffs.get()) {
if (diff.originalRange.endLineNumberExclusive <= baseRange.startLineNumber) {
startOffset += diff.resultingDeltaFromOriginalToModified;
} else if (diff.originalRange.startLineNumber <= baseRange.endLineNumberExclusive) {
lengthOffset += diff.resultingDeltaFromOriginalToModified;
} else {
break;
}
}
return new LineRange(baseRange.startLineNumber + startOffset, baseRange.lineCount + lengthOffset);
}
return {
edit: secondDiff.diff.getLineEdit(),
effectiveState: ModifiedBaseRangeState.default.withInputValue(
secondDiff.inputNumber,
true
),
};
}
function computeState(baseRange: ModifiedBaseRange, conflictingDiffs: LineDiff[]): ModifiedBaseRangeState {
if (conflictingDiffs.length === 0) {
return ModifiedBaseRangeState.default;
}
const conflictingEdits = conflictingDiffs.map((d) => d.getLineEdit());
function editsAgreeWithDiffs(diffs: readonly LineDiff[]): boolean {
return equals(
conflictingEdits,
diffs.map((d) => d.getLineEdit()),
(a, b) => a.equals(b)
);
}
if (editsAgreeWithDiffs(baseRange.input1Diffs)) {
return ModifiedBaseRangeState.default.withInput1(true);
}
if (editsAgreeWithDiffs(baseRange.input2Diffs)) {
return ModifiedBaseRangeState.default.withInput2(true);
}
return ModifiedBaseRangeState.conflicting;
}

View file

@ -0,0 +1,229 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { compareBy, numberComparator } from 'vs/base/common/arrays';
import { BugIndicatingError } from 'vs/base/common/errors';
import { Disposable } from 'vs/base/common/lifecycle';
import { ITextModel } from 'vs/editor/common/model';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
import { IObservable, ITransaction, ObservableValue, transaction } from 'vs/workbench/contrib/audioCues/browser/observable';
import { LineDiff, LineEdit, LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model';
import { ReentrancyBarrier } from 'vs/workbench/contrib/mergeEditor/browser/utils';
export class TextModelDiffs extends Disposable {
private updateCount = 0;
private readonly _state = new ObservableValue<TextModelDiffState, TextModelDiffChangeReason>(TextModelDiffState.initializing, 'LiveDiffState');
private readonly _diffs = new ObservableValue<LineDiff[], TextModelDiffChangeReason>([], 'LiveDiffs');
private readonly barrier = new ReentrancyBarrier();
constructor(
private readonly baseTextModel: ITextModel,
private readonly textModel: ITextModel,
private readonly diffComputer: IDiffComputer,
) {
super();
this.update(true);
this._register(baseTextModel.onDidChangeContent(this.barrier.makeExclusive(() => this.update())));
this._register(textModel.onDidChangeContent(this.barrier.makeExclusive(() => this.update())));
}
public get state(): IObservable<TextModelDiffState, TextModelDiffChangeReason> {
return this._state;
}
public get diffs(): IObservable<LineDiff[], TextModelDiffChangeReason> {
return this._diffs;
}
private async update(initializing: boolean = false): Promise<void> {
this.updateCount++;
const currentUpdateCount = this.updateCount;
if (this._state.get() === TextModelDiffState.initializing) {
initializing = true;
}
transaction(tx => {
this._state.set(
initializing ? TextModelDiffState.initializing : TextModelDiffState.updating,
tx,
TextModelDiffChangeReason.other
);
});
const result = await this.diffComputer.computeDiff(this.baseTextModel, this.textModel);
if (currentUpdateCount !== this.updateCount) {
// There is a newer update call
return;
}
transaction(tx => {
if (result) {
this._state.set(TextModelDiffState.upToDate, tx, TextModelDiffChangeReason.textChange);
this._diffs.set(result, tx, TextModelDiffChangeReason.textChange);
} else {
this._state.set(TextModelDiffState.error, tx, TextModelDiffChangeReason.textChange);
}
});
}
private ensureUpToDate(): void {
if (this.state.get() !== TextModelDiffState.upToDate) {
throw new BugIndicatingError('Cannot remove diffs when the model is not up to date');
}
}
public removeDiffs(diffToRemoves: LineDiff[], transaction: ITransaction | undefined): void {
this.ensureUpToDate();
diffToRemoves.sort(compareBy((d) => d.originalRange.startLineNumber, numberComparator));
diffToRemoves.reverse();
let diffs = this._diffs.get();
for (const diffToRemove of diffToRemoves) {
// TODO improve performance
const len = diffs.length;
diffs = diffs.filter((d) => d !== diffToRemove);
if (len === diffs.length) {
throw new BugIndicatingError();
}
this.barrier.runExclusivelyOrThrow(() => {
diffToRemove.getReverseLineEdit().apply(this.textModel);
});
diffs = diffs.map((d) =>
d.modifiedRange.isAfter(diffToRemove.modifiedRange)
? new LineDiff(
d.originalTextModel,
d.originalRange,
d.modifiedTextModel,
d.modifiedRange.delta(
diffToRemove.originalRange.lineCount - diffToRemove.modifiedRange.lineCount
)
)
: d
);
}
this._diffs.set(diffs, transaction, TextModelDiffChangeReason.other);
}
/**
* Edit must be conflict free.
*/
public applyEditRelativeToOriginal(edit: LineEdit, transaction: ITransaction | undefined): void {
this.ensureUpToDate();
let firstAfter = false;
let delta = 0;
const newDiffs = new Array<LineDiff>();
for (const diff of this.diffs.get()) {
if (diff.originalRange.touches(edit.range)) {
throw new BugIndicatingError('Edit must be conflict free.');
} else if (diff.originalRange.isAfter(edit.range)) {
if (!firstAfter) {
firstAfter = true;
newDiffs.push(new LineDiff(
this.baseTextModel,
edit.range,
this.textModel,
new LineRange(edit.range.startLineNumber + delta, edit.newLines.length)
));
}
newDiffs.push(new LineDiff(
diff.originalTextModel,
diff.originalRange,
diff.modifiedTextModel,
diff.modifiedRange.delta(edit.newLines.length - edit.range.lineCount)
));
} else {
newDiffs.push(diff);
}
if (!firstAfter) {
delta += diff.modifiedRange.lineCount - diff.originalRange.lineCount;
}
}
if (!firstAfter) {
firstAfter = true;
newDiffs.push(new LineDiff(
this.baseTextModel,
edit.range,
this.textModel,
new LineRange(edit.range.startLineNumber + delta, edit.newLines.length)
));
}
this.barrier.runExclusivelyOrThrow(() => {
new LineEdit(edit.range.delta(delta), edit.newLines).apply(this.textModel);
});
this._diffs.set(newDiffs, transaction, TextModelDiffChangeReason.other);
}
public findTouchingDiffs(baseRange: LineRange): LineDiff[] {
return this.diffs.get().filter(d => d.originalRange.touches(baseRange));
}
/*
public getResultRange(baseRange: LineRange): LineRange {
let startOffset = 0;
let lengthOffset = 0;
for (const diff of this.diffs.get()) {
if (diff.originalRange.endLineNumberExclusive <= baseRange.startLineNumber) {
startOffset += diff.resultingDeltaFromOriginalToModified;
} else if (diff.originalRange.startLineNumber <= baseRange.endLineNumberExclusive) {
lengthOffset += diff.resultingDeltaFromOriginalToModified;
} else {
break;
}
}
return new LineRange(baseRange.startLineNumber + startOffset, baseRange.lineCount + lengthOffset);
}
*/
}
export const enum TextModelDiffChangeReason {
other = 0,
textChange = 1,
}
export const enum TextModelDiffState {
initializing = 1,
upToDate = 2,
updating = 3,
error = 4,
}
export interface ITextModelDiffsState {
state: TextModelDiffState;
diffs: LineDiff[];
}
export interface IDiffComputer {
computeDiff(textModel1: ITextModel, textModel2: ITextModel): Promise<LineDiff[] | null>;
}
export class EditorWorkerServiceDiffComputer implements IDiffComputer {
constructor(@IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService) { }
async computeDiff(textModel1: ITextModel, textModel2: ITextModel): Promise<LineDiff[] | null> {
//await wait(1000);
const diffs = await this.editorWorkerService.computeDiff(textModel1.uri, textModel2.uri, false, 1000);
if (!diffs || diffs.quitEarly) {
return null;
}
return diffs.changes.map((c) => LineDiff.fromLineChange(c, textModel1, textModel2));
}
}