addons/isl/src/stackEdit/fileStackState.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {RecordOf} from 'immutable';
b69ab319import type {FlattenLine, LineIdx} from '../linelog';
b69ab3110import type {FileRev} from './common';
b69ab3111
b69ab3112import {List, Record} from 'immutable';
b69ab3113import {SelfUpdate} from 'shared/immutableExt';
b69ab3114import {LineLog} from '../linelog';
b69ab3115import {next} from './revMath';
b69ab3116
b69ab3117/**
b69ab3118 * A stack of file contents with stack editing features.
b69ab3119 */
b69ab3120export class FileStackState extends SelfUpdate<FileStackStateRecord> {
b69ab3121 constructor(value: Source | string[]) {
b69ab3122 if (Array.isArray(value)) {
b69ab3123 const contents: string[] = value;
b69ab3124 const source = Source({
b69ab3125 type: 'plain',
b69ab3126 value: List(contents),
b69ab3127 revLength: contents.length as FileRev,
b69ab3128 });
b69ab3129 super(FileStackStateRecord({source}));
b69ab3130 } else {
b69ab3131 super(FileStackStateRecord({source: value}));
b69ab3132 }
b69ab3133 }
b69ab3134
b69ab3135 get source(): Source {
b69ab3136 return this.inner.source;
b69ab3137 }
b69ab3138
b69ab3139 get revLength(): FileRev {
b69ab3140 return this.inner.source.revLength;
b69ab3141 }
b69ab3142
b69ab3143 fromLineLog(log: LineLog): FileStackState {
b69ab3144 return new FileStackState(
b69ab3145 Source({type: 'linelog', value: log, revLength: (Math.floor(log.maxRev) + 1) as FileRev}),
b69ab3146 );
b69ab3147 }
b69ab3148
b69ab3149 fromFlattenLines(lines: List<FlattenLine>, revLength: number | undefined): FileStackState {
b69ab3150 const newRevLength = (revLength ?? lines.map(l => l.revs.max()).max() ?? 0) as FileRev;
b69ab3151 const source = Source({type: 'flatten', value: lines, revLength: newRevLength});
b69ab3152 return new FileStackState(source);
b69ab3153 }
b69ab3154
b69ab3155 // Read operations.
b69ab3156
b69ab3157 /**
b69ab3158 * Obtain the content at the given revision.
b69ab3159 * 0 <= rev < this.revLength
b69ab3160 */
b69ab3161 getRev(rev: FileRev): string {
b69ab3162 const type = this.source.type;
b69ab3163 if (type === 'linelog') {
b69ab3164 return this.source.value.checkOut(rev);
b69ab3165 } else if (type === 'flatten') {
b69ab3166 return this.source.value
b69ab3167 .filter(l => l.revs.has(rev))
b69ab3168 .map(l => l.data)
b69ab3169 .join('');
b69ab3170 } else if (type === 'plain') {
b69ab3171 return this.source.value.get(rev, '');
b69ab3172 }
b69ab3173 throw new Error(`unexpected source type ${type}`);
b69ab3174 }
b69ab3175
b69ab3176 /** Array of valid revisions. */
b69ab3177 revs(): FileRev[] {
b69ab3178 return [...Array(this.source.revLength).keys()] as FileRev[];
b69ab3179 }
b69ab3180
b69ab3181 /**
b69ab3182 * Calculate the dependencies of revisions.
b69ab3183 * For example, `{5: [3, 1]}` means rev 5 depends on rev 3 and rev 1.
b69ab3184 */
b69ab3185 calculateDepMap(): Map<FileRev, Set<FileRev>> {
b69ab3186 return this.convertToLineLog().calculateDepMap() as Map<FileRev, Set<FileRev>>;
b69ab3187 }
b69ab3188
b69ab3189 /** Figure out which `rev` introduces the lines. */
b69ab3190 blame(rev: FileRev): FileRev[] {
b69ab3191 const log = this.convertToLineLog();
b69ab3192 const lines = log.checkOutLines(rev);
b69ab3193 // Skip the last 'END' line.
b69ab3194 return lines.slice(0, lines.length - 1).map(l => l.rev as FileRev);
b69ab3195 }
b69ab3196
b69ab3197 // Write operations.
b69ab3198
b69ab3199 /**
b69ab31100 * Edit full text of a rev.
b69ab31101 * If `updateStack` is true, the rest of the stack will be updated
b69ab31102 * accordingly. Otherwise, no other revs are updated.
b69ab31103 */
b69ab31104 editText(rev: FileRev, text: string, updateStack = true): FileStackState {
b69ab31105 const revLength = rev >= this.source.revLength ? next(rev) : this.source.revLength;
b69ab31106 let source = this.source;
b69ab31107 if (updateStack) {
b69ab31108 const log = this.convertToLineLog().recordText(text, rev);
b69ab31109 source = Source({type: 'linelog', value: log, revLength});
b69ab31110 } else {
b69ab31111 const plain = this.convertToPlainText().set(rev, text);
b69ab31112 source = Source({type: 'plain', value: plain, revLength});
b69ab31113 }
b69ab31114 return new FileStackState(source);
b69ab31115 }
b69ab31116
b69ab31117 /**
b69ab31118 * Replace line range `a1` to `a2` at `aRev` with `bLines` from `bRev`.
b69ab31119 * The rest of the stack will be updated accordingly.
b69ab31120 *
b69ab31121 * The `aRev` decides what `a1` and `a2` mean, since line indexes
b69ab31122 * from different revs are different. The `aRev` is not the rev that
b69ab31123 * makes the change.
b69ab31124 *
b69ab31125 * The `bRev` is the revision that makes the change (deletion, insertion).
b69ab31126 *
b69ab31127 * This is useful to implement absorb-like edits at chunk (not full text)
b69ab31128 * level. For example, absorb runs from the top of a stack, and calculates
b69ab31129 * the diff between the stack top commit and the current working copy.
b69ab31130 * So `aRev` is the stack top, since the diff uses line numbers in the
b69ab31131 * stack top. `bRev` is the revisions that each chunk blames to.
b69ab31132 */
b69ab31133 editChunk(
b69ab31134 aRev: FileRev,
b69ab31135 a1: LineIdx,
b69ab31136 a2: LineIdx,
b69ab31137 bRev: FileRev,
b69ab31138 bLines: string[],
b69ab31139 ): FileStackState {
b69ab31140 const log = this.convertToLineLog().editChunk(aRev, a1, a2, bRev, bLines);
b69ab31141 return this.fromLineLog(log);
b69ab31142 }
b69ab31143
b69ab31144 /**
b69ab31145 * Remap the revs. This can be useful for reordering, folding,
b69ab31146 * and insertion. The callsite is responsible for checking
b69ab31147 * `revDepMap` to ensure the reordering can be "conflict"-free.
b69ab31148 */
b69ab31149 remapRevs(revMap: Map<FileRev, FileRev> | ((rev: FileRev) => FileRev)): FileStackState {
b69ab31150 const log = this.convertToLineLog().remapRevs(
b69ab31151 revMap as Map<number, number> | ((rev: number) => number),
b69ab31152 );
b69ab31153 return this.fromLineLog(log);
b69ab31154 }
b69ab31155
b69ab31156 /**
b69ab31157 * Move (or copy) line range `a1` to `a2` at `aRev` to other revs.
b69ab31158 * Those lines will be included by `includeRevs` and excluded by `excludeRevs`.
b69ab31159 *
b69ab31160 * PERF: It would be better to just use linelog to complete the edit.
b69ab31161 */
b69ab31162 moveLines(
b69ab31163 aRev: FileRev,
b69ab31164 a1: LineIdx,
b69ab31165 a2: LineIdx,
b69ab31166 includeRevs?: FileRev[],
b69ab31167 excludeRevs?: FileRev[],
b69ab31168 ): FileStackState {
b69ab31169 let revLineIdx = 0;
b69ab31170 const editLine = (line: FlattenLine): FlattenLine => {
b69ab31171 let newLine = line;
b69ab31172 if (line.revs.has(aRev)) {
b69ab31173 if (revLineIdx >= a1 && revLineIdx < a2) {
b69ab31174 const newRevs = line.revs.withMutations(mutRevs => {
b69ab31175 let revs = mutRevs;
b69ab31176 if (includeRevs) {
b69ab31177 revs = revs.union(includeRevs);
b69ab31178 }
b69ab31179 if (excludeRevs) {
b69ab31180 revs = revs.subtract(excludeRevs);
b69ab31181 }
b69ab31182 return revs;
b69ab31183 });
b69ab31184 newLine = line.set('revs', newRevs);
b69ab31185 }
b69ab31186 revLineIdx++;
b69ab31187 }
b69ab31188 return newLine;
b69ab31189 };
b69ab31190
b69ab31191 return this.mapAllLines(editLine);
b69ab31192 }
b69ab31193
b69ab31194 /**
b69ab31195 * Edit lines for all revisions using a callback.
b69ab31196 * The return type can be an array (like flatMap), to insert or delete lines.
b69ab31197 */
b69ab31198 mapAllLines(
b69ab31199 editLineFunc: (line: FlattenLine, i: number) => FlattenLine | FlattenLine[],
b69ab31200 ): FileStackState {
b69ab31201 const lines = this.convertToFlattenLines().flatMap((line, i) => {
b69ab31202 const mapped = editLineFunc(line, i);
b69ab31203 return Array.isArray(mapped) ? mapped : [mapped];
b69ab31204 });
b69ab31205 return this.fromFlattenLines(lines, this.revLength);
b69ab31206 }
b69ab31207
b69ab31208 /**
b69ab31209 * Truncate the stack. Drop rev (inclusive) and higher revs.
b69ab31210 * Note: This is only implemented for linelog.
b69ab31211 */
b69ab31212 truncate(rev: FileRev): FileStackState {
b69ab31213 const log = this.convertToLineLog().truncate(rev);
b69ab31214 return this.fromLineLog(log);
b69ab31215 }
b69ab31216
b69ab31217 // Internal format conversions.
b69ab31218
b69ab31219 /** Convert to LineLog representation on demand. */
b69ab31220 convertToLineLog(): LineLog {
b69ab31221 const type = 'linelog';
b69ab31222 if (this.source.type === type) {
b69ab31223 return this.source.value;
b69ab31224 }
b69ab31225 let log = new LineLog();
b69ab31226 this.revs().forEach(rev => {
b69ab31227 const data = this.getRev(rev);
b69ab31228 log = log.recordText(data, rev);
b69ab31229 });
b69ab31230 return log;
b69ab31231 }
b69ab31232
b69ab31233 /** Convert to flatten representation on demand. */
b69ab31234 convertToFlattenLines(): List<FlattenLine> {
b69ab31235 const type = 'flatten';
b69ab31236 if (this.source.type === type) {
b69ab31237 return this.source.value;
b69ab31238 }
b69ab31239 const log = this.convertToLineLog();
b69ab31240 const lines = log.flatten();
b69ab31241 return List(lines);
b69ab31242 }
b69ab31243
b69ab31244 /** Convert to plain representation on demand. */
b69ab31245 convertToPlainText(): List<string> {
b69ab31246 const type = 'plain';
b69ab31247 if (this.source.type === type) {
b69ab31248 return this.source.value;
b69ab31249 }
b69ab31250 const contents = this.revs().map(this.getRev.bind(this));
b69ab31251 return List(contents);
b69ab31252 }
b69ab31253}
b69ab31254
b69ab31255/**
b69ab31256 * Depending on the operation, there are different ways to represent the file contents:
b69ab31257 * - plain: Full text per revision. This is the initial representation.
b69ab31258 * - linelog: LineLog representation. This is useful for analysis (dependency, target
b69ab31259 * revs for absorb, line number offset calculation, etc), certain kinds of
b69ab31260 * editing, and generating the flatten representation.
b69ab31261 * - flatten: The flatten view of LineLog. This is useful for moving lines between
b69ab31262 * revisions more explicitly.
b69ab31263 */
b69ab31264type SourceProps =
b69ab31265 | {
b69ab31266 type: 'linelog';
b69ab31267 value: LineLog;
b69ab31268 revLength: FileRev;
b69ab31269 }
b69ab31270 | {
b69ab31271 type: 'plain';
b69ab31272 value: List<string>;
b69ab31273 revLength: FileRev;
b69ab31274 }
b69ab31275 | {
b69ab31276 type: 'flatten';
b69ab31277 value: List<FlattenLine>;
b69ab31278 revLength: FileRev;
b69ab31279 };
b69ab31280
b69ab31281export const Source = Record<SourceProps>({
b69ab31282 type: 'plain',
b69ab31283 value: List([]),
b69ab31284 revLength: 0 as FileRev,
b69ab31285});
b69ab31286type Source = RecordOf<SourceProps>;
b69ab31287
b69ab31288type FileStackStateProps = {
b69ab31289 source: Source;
b69ab31290};
b69ab31291const FileStackStateRecord = Record<FileStackStateProps>({source: Source()});
b69ab31292type FileStackStateRecord = RecordOf<FileStackStateProps>;
b69ab31293
b69ab31294export type {FileRev};