mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 21:09:43 +00:00
Merge pull request #151508 from microsoft/3wm
This commit is contained in:
commit
de13d28ee4
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
229
src/vs/workbench/contrib/mergeEditor/browser/textModelDiffs.ts
Normal file
229
src/vs/workbench/contrib/mergeEditor/browser/textModelDiffs.ts
Normal 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));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue