8.8 KB295 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type {RecordOf} from 'immutable';
9import type {FlattenLine, LineIdx} from '../linelog';
10import type {FileRev} from './common';
11
12import {List, Record} from 'immutable';
13import {SelfUpdate} from 'shared/immutableExt';
14import {LineLog} from '../linelog';
15import {next} from './revMath';
16
17/**
18 * A stack of file contents with stack editing features.
19 */
20export class FileStackState extends SelfUpdate<FileStackStateRecord> {
21 constructor(value: Source | string[]) {
22 if (Array.isArray(value)) {
23 const contents: string[] = value;
24 const source = Source({
25 type: 'plain',
26 value: List(contents),
27 revLength: contents.length as FileRev,
28 });
29 super(FileStackStateRecord({source}));
30 } else {
31 super(FileStackStateRecord({source: value}));
32 }
33 }
34
35 get source(): Source {
36 return this.inner.source;
37 }
38
39 get revLength(): FileRev {
40 return this.inner.source.revLength;
41 }
42
43 fromLineLog(log: LineLog): FileStackState {
44 return new FileStackState(
45 Source({type: 'linelog', value: log, revLength: (Math.floor(log.maxRev) + 1) as FileRev}),
46 );
47 }
48
49 fromFlattenLines(lines: List<FlattenLine>, revLength: number | undefined): FileStackState {
50 const newRevLength = (revLength ?? lines.map(l => l.revs.max()).max() ?? 0) as FileRev;
51 const source = Source({type: 'flatten', value: lines, revLength: newRevLength});
52 return new FileStackState(source);
53 }
54
55 // Read operations.
56
57 /**
58 * Obtain the content at the given revision.
59 * 0 <= rev < this.revLength
60 */
61 getRev(rev: FileRev): string {
62 const type = this.source.type;
63 if (type === 'linelog') {
64 return this.source.value.checkOut(rev);
65 } else if (type === 'flatten') {
66 return this.source.value
67 .filter(l => l.revs.has(rev))
68 .map(l => l.data)
69 .join('');
70 } else if (type === 'plain') {
71 return this.source.value.get(rev, '');
72 }
73 throw new Error(`unexpected source type ${type}`);
74 }
75
76 /** Array of valid revisions. */
77 revs(): FileRev[] {
78 return [...Array(this.source.revLength).keys()] as FileRev[];
79 }
80
81 /**
82 * Calculate the dependencies of revisions.
83 * For example, `{5: [3, 1]}` means rev 5 depends on rev 3 and rev 1.
84 */
85 calculateDepMap(): Map<FileRev, Set<FileRev>> {
86 return this.convertToLineLog().calculateDepMap() as Map<FileRev, Set<FileRev>>;
87 }
88
89 /** Figure out which `rev` introduces the lines. */
90 blame(rev: FileRev): FileRev[] {
91 const log = this.convertToLineLog();
92 const lines = log.checkOutLines(rev);
93 // Skip the last 'END' line.
94 return lines.slice(0, lines.length - 1).map(l => l.rev as FileRev);
95 }
96
97 // Write operations.
98
99 /**
100 * Edit full text of a rev.
101 * If `updateStack` is true, the rest of the stack will be updated
102 * accordingly. Otherwise, no other revs are updated.
103 */
104 editText(rev: FileRev, text: string, updateStack = true): FileStackState {
105 const revLength = rev >= this.source.revLength ? next(rev) : this.source.revLength;
106 let source = this.source;
107 if (updateStack) {
108 const log = this.convertToLineLog().recordText(text, rev);
109 source = Source({type: 'linelog', value: log, revLength});
110 } else {
111 const plain = this.convertToPlainText().set(rev, text);
112 source = Source({type: 'plain', value: plain, revLength});
113 }
114 return new FileStackState(source);
115 }
116
117 /**
118 * Replace line range `a1` to `a2` at `aRev` with `bLines` from `bRev`.
119 * The rest of the stack will be updated accordingly.
120 *
121 * The `aRev` decides what `a1` and `a2` mean, since line indexes
122 * from different revs are different. The `aRev` is not the rev that
123 * makes the change.
124 *
125 * The `bRev` is the revision that makes the change (deletion, insertion).
126 *
127 * This is useful to implement absorb-like edits at chunk (not full text)
128 * level. For example, absorb runs from the top of a stack, and calculates
129 * the diff between the stack top commit and the current working copy.
130 * So `aRev` is the stack top, since the diff uses line numbers in the
131 * stack top. `bRev` is the revisions that each chunk blames to.
132 */
133 editChunk(
134 aRev: FileRev,
135 a1: LineIdx,
136 a2: LineIdx,
137 bRev: FileRev,
138 bLines: string[],
139 ): FileStackState {
140 const log = this.convertToLineLog().editChunk(aRev, a1, a2, bRev, bLines);
141 return this.fromLineLog(log);
142 }
143
144 /**
145 * Remap the revs. This can be useful for reordering, folding,
146 * and insertion. The callsite is responsible for checking
147 * `revDepMap` to ensure the reordering can be "conflict"-free.
148 */
149 remapRevs(revMap: Map<FileRev, FileRev> | ((rev: FileRev) => FileRev)): FileStackState {
150 const log = this.convertToLineLog().remapRevs(
151 revMap as Map<number, number> | ((rev: number) => number),
152 );
153 return this.fromLineLog(log);
154 }
155
156 /**
157 * Move (or copy) line range `a1` to `a2` at `aRev` to other revs.
158 * Those lines will be included by `includeRevs` and excluded by `excludeRevs`.
159 *
160 * PERF: It would be better to just use linelog to complete the edit.
161 */
162 moveLines(
163 aRev: FileRev,
164 a1: LineIdx,
165 a2: LineIdx,
166 includeRevs?: FileRev[],
167 excludeRevs?: FileRev[],
168 ): FileStackState {
169 let revLineIdx = 0;
170 const editLine = (line: FlattenLine): FlattenLine => {
171 let newLine = line;
172 if (line.revs.has(aRev)) {
173 if (revLineIdx >= a1 && revLineIdx < a2) {
174 const newRevs = line.revs.withMutations(mutRevs => {
175 let revs = mutRevs;
176 if (includeRevs) {
177 revs = revs.union(includeRevs);
178 }
179 if (excludeRevs) {
180 revs = revs.subtract(excludeRevs);
181 }
182 return revs;
183 });
184 newLine = line.set('revs', newRevs);
185 }
186 revLineIdx++;
187 }
188 return newLine;
189 };
190
191 return this.mapAllLines(editLine);
192 }
193
194 /**
195 * Edit lines for all revisions using a callback.
196 * The return type can be an array (like flatMap), to insert or delete lines.
197 */
198 mapAllLines(
199 editLineFunc: (line: FlattenLine, i: number) => FlattenLine | FlattenLine[],
200 ): FileStackState {
201 const lines = this.convertToFlattenLines().flatMap((line, i) => {
202 const mapped = editLineFunc(line, i);
203 return Array.isArray(mapped) ? mapped : [mapped];
204 });
205 return this.fromFlattenLines(lines, this.revLength);
206 }
207
208 /**
209 * Truncate the stack. Drop rev (inclusive) and higher revs.
210 * Note: This is only implemented for linelog.
211 */
212 truncate(rev: FileRev): FileStackState {
213 const log = this.convertToLineLog().truncate(rev);
214 return this.fromLineLog(log);
215 }
216
217 // Internal format conversions.
218
219 /** Convert to LineLog representation on demand. */
220 convertToLineLog(): LineLog {
221 const type = 'linelog';
222 if (this.source.type === type) {
223 return this.source.value;
224 }
225 let log = new LineLog();
226 this.revs().forEach(rev => {
227 const data = this.getRev(rev);
228 log = log.recordText(data, rev);
229 });
230 return log;
231 }
232
233 /** Convert to flatten representation on demand. */
234 convertToFlattenLines(): List<FlattenLine> {
235 const type = 'flatten';
236 if (this.source.type === type) {
237 return this.source.value;
238 }
239 const log = this.convertToLineLog();
240 const lines = log.flatten();
241 return List(lines);
242 }
243
244 /** Convert to plain representation on demand. */
245 convertToPlainText(): List<string> {
246 const type = 'plain';
247 if (this.source.type === type) {
248 return this.source.value;
249 }
250 const contents = this.revs().map(this.getRev.bind(this));
251 return List(contents);
252 }
253}
254
255/**
256 * Depending on the operation, there are different ways to represent the file contents:
257 * - plain: Full text per revision. This is the initial representation.
258 * - linelog: LineLog representation. This is useful for analysis (dependency, target
259 * revs for absorb, line number offset calculation, etc), certain kinds of
260 * editing, and generating the flatten representation.
261 * - flatten: The flatten view of LineLog. This is useful for moving lines between
262 * revisions more explicitly.
263 */
264type SourceProps =
265 | {
266 type: 'linelog';
267 value: LineLog;
268 revLength: FileRev;
269 }
270 | {
271 type: 'plain';
272 value: List<string>;
273 revLength: FileRev;
274 }
275 | {
276 type: 'flatten';
277 value: List<FlattenLine>;
278 revLength: FileRev;
279 };
280
281export const Source = Record<SourceProps>({
282 type: 'plain',
283 value: List([]),
284 revLength: 0 as FileRev,
285});
286type Source = RecordOf<SourceProps>;
287
288type FileStackStateProps = {
289 source: Source;
290};
291const FileStackStateRecord = Record<FileStackStateProps>({source: Source()});
292type FileStackStateRecord = RecordOf<FileStackStateProps>;
293
294export type {FileRev};
295