3.5 KB137 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 {CommitRev} from './commitStackState';
9
10import {List, Record} from 'immutable';
11import {CommitStackState} from './commitStackState';
12
13type ReorderResult = {
14 /** Offset of the move. Positive: move up. Negative: move down. */
15 offset: number;
16
17 /** Reorder result that satisfy dependencies. */
18 order: CommitRev[];
19
20 /** Dependent revs that are also moved. */
21 deps: CommitRev[];
22};
23
24function* range(
25 start: number,
26 end: number,
27 filterFunc?: (i: CommitRev) => boolean,
28): IterableIterator<CommitRev> {
29 for (let i = start; i < end; i++) {
30 if (filterFunc && !filterFunc(i as CommitRev)) {
31 continue;
32 }
33 yield i as CommitRev;
34 }
35}
36
37/**
38 * Reorder 0..n (exclusive) by moving `origRev` by `offset`.
39 * Respect `depMap`.
40 */
41export function reorderWithDeps(
42 n: number,
43 origRev: CommitRev,
44 desiredOffset: number,
45 depMap: Readonly<Map<CommitRev, Set<CommitRev>>>,
46): Readonly<ReorderResult> {
47 const offset =
48 origRev + desiredOffset < 0
49 ? -origRev
50 : origRev + desiredOffset >= n
51 ? n - 1 - origRev
52 : desiredOffset;
53
54 let order: CommitRev[] = [];
55 const deps: CommitRev[] = [origRev];
56 const filterFunc = (i: CommitRev) => !deps.includes(i);
57 if (offset < 0) {
58 // Moved down.
59 const depRevs = new Set(depMap.get(origRev) ?? []);
60 for (let i = -1; i >= offset; i--) {
61 const rev = (origRev + i) as CommitRev;
62 if (depRevs.has(rev)) {
63 deps.push(rev);
64 depMap.get(rev)?.forEach(r => depRevs.add(r));
65 }
66 }
67 deps.reverse();
68 order = [...range(0, origRev + offset), ...deps, ...range(origRev + offset, n, filterFunc)];
69 } else if (offset > 0) {
70 // Moved up.
71 for (let i = 1; i <= offset; i++) {
72 const rev = (origRev + i) as CommitRev;
73 const dep = depMap.get(rev);
74 if (dep && (dep.has(origRev) || deps.some(r => dep.has(r)))) {
75 deps.push(rev);
76 }
77 }
78 order = [
79 ...range(0, origRev + offset + 1, filterFunc),
80 ...deps,
81 ...range(origRev + offset + 1, n, filterFunc),
82 ];
83 } else {
84 // Nothing moved.
85 order = [...range(0, n)];
86 }
87 return {offset, order, deps};
88}
89
90/** State to preview effects of drag-n-drop reorder. */
91export class ReorderState extends Record({
92 offset: 0,
93 commitStack: new CommitStackState([]),
94 reorderRevs: List<CommitRev>(),
95 draggingRevs: List<CommitRev>(),
96 draggingRev: -1 as CommitRev,
97}) {
98 static init(commitStack: CommitStackState, draggingRev: CommitRev): ReorderState {
99 return new ReorderState({
100 offset: 0,
101 commitStack,
102 draggingRev,
103 reorderRevs: List(commitStack.revs()),
104 draggingRevs: List([draggingRev]),
105 });
106 }
107
108 isDragging() {
109 return this.draggingRev >= 0;
110 }
111
112 /** Returns true if the reorder does nothing. */
113 isNoop(): boolean {
114 return this.offset === 0;
115 }
116
117 /**
118 * Calculate reorderRevs and draggingRevs based on the given offset.
119 * `draggingRevs` might change to maintain the dependency map.
120 */
121 withOffset(offset: number): ReorderState {
122 const reordered = reorderWithDeps(
123 this.commitStack.stack.size,
124 this.draggingRev,
125 offset,
126 this.commitStack.calculateDepMap(),
127 );
128
129 // Force match dependency requirements of `rev` by moving dependencies.
130 return this.merge({
131 reorderRevs: List(reordered.order),
132 draggingRevs: List(reordered.deps),
133 offset: reordered.offset,
134 });
135 }
136}
137