blob: 8dae154d156ef11ead63cebde3cd3003cf4da8f0 [file] [log] [blame]
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {Observable, combineLatest, from} from 'rxjs';
import {debounceTime, filter, switchMap, withLatestFrom} from 'rxjs/operators';
import {
DiffInfo,
DiffPreferencesInfo,
DiffViewMode,
DisplayLine,
RenderPreferences,
} from '../../../api/diff';
import {define} from '../../../models/dependency';
import {Model} from '../../../models/model';
import {select} from '../../../utils/observable-util';
import {
FullContext,
GrDiffCommentThread,
KeyLocations,
computeContext,
computeKeyLocations,
computeLineLength,
} from '../gr-diff/gr-diff-utils';
import {createDefaultDiffPrefs} from '../../../constants/constants';
import {
GrDiffProcessor,
ProcessingOptions,
} from '../gr-diff-processor/gr-diff-processor';
import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
import {assert} from '../../../utils/common-util';
export interface DiffState {
diff?: DiffInfo;
path?: string;
renderPrefs: RenderPreferences;
diffPrefs: DiffPreferencesInfo;
lineOfInterest?: DisplayLine;
comments: GrDiffCommentThread[];
groups: GrDiffGroup[];
/** how much context to show for large files */
showFullContext: FullContext;
isImageDiff: boolean;
}
export const diffModelToken = define<DiffModel>('diff-model');
export class DiffModel extends Model<DiffState> {
readonly diff$: Observable<DiffInfo> = select(
this.state$.pipe(filter(state => state.diff !== undefined)),
diffState => diffState.diff!
);
readonly path$: Observable<string | undefined> = select(
this.state$,
diffState => diffState.path
);
readonly renderPrefs$: Observable<RenderPreferences> = select(
this.state$,
diffState => diffState.renderPrefs
);
readonly viewMode$: Observable<DiffViewMode> = select(
this.renderPrefs$,
renderPrefs => renderPrefs.view_mode ?? DiffViewMode.SIDE_BY_SIDE
);
readonly diffPrefs$: Observable<DiffPreferencesInfo> = select(
this.state$,
diffState => diffState.diffPrefs
);
readonly context$: Observable<number> = select(this.state$, state =>
computeContext(
state.diffPrefs.context,
state.showFullContext,
createDefaultDiffPrefs().context
)
);
readonly isImageDiff$: Observable<boolean> = select(
this.state$,
diffState => diffState.isImageDiff
);
readonly groups$: Observable<GrDiffGroup[]> = select(
this.state$,
diffState => diffState.groups ?? []
);
readonly lineLength$: Observable<number> = select(this.state$, state =>
computeLineLength(state.diffPrefs, state.path)
);
readonly keyLocations$: Observable<KeyLocations> = select(
this.state$,
diffState =>
computeKeyLocations(diffState.lineOfInterest, diffState.comments ?? [])
);
constructor() {
super({
diffPrefs: createDefaultDiffPrefs(),
renderPrefs: {},
comments: [],
groups: [],
showFullContext: FullContext.UNDECIDED,
isImageDiff: false,
});
this.subscriptions = [this.processDiff()];
}
processDiff() {
return combineLatest([
this.diff$,
this.context$,
this.renderPrefs$,
this.isImageDiff$,
])
.pipe(
withLatestFrom(this.keyLocations$),
debounceTime(1),
switchMap(
([[diff, context, renderPrefs, isImageDiff], keyLocations]) => {
const options: ProcessingOptions = {
context,
keyLocations,
isBinary: !!(isImageDiff || diff.binary),
};
if (renderPrefs?.num_lines_rendered_at_once) {
options.asyncThreshold = renderPrefs.num_lines_rendered_at_once;
}
const processor = new GrDiffProcessor(options);
return from(processor.process(diff.content));
}
)
)
.subscribe(groups => {
this.updateState({groups});
});
}
/**
* Replace a context control group with some expanded groups. Happens when the
* user clicks "+10" or something similar.
*/
replaceGroup(group: GrDiffGroup, newGroups: readonly GrDiffGroup[]) {
assert(
group.type === GrDiffGroupType.CONTEXT_CONTROL,
'gr-diff can only replace context control groups'
);
const groups = [...this.getState().groups];
const i = groups.indexOf(group);
if (i === -1) throw new Error('cannot find context control group');
groups.splice(i, 1, ...newGroups);
this.updateState({groups});
}
}