addons/isl/src/partialSelection.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 {Hash, RepoPath} from 'shared/types/common';
b69ab319import type {ExportFile, ImportCommit} from 'shared/types/stack';
b69ab3110import type {RepoRelativePath} from './types';
b69ab3111
b69ab3112import Immutable from 'immutable';
b69ab3113import {atom, useAtom, useAtomValue} from 'jotai';
b69ab3114import {RateLimiter} from 'shared/RateLimiter';
b69ab3115import {SelfUpdate} from 'shared/immutableExt';
b69ab3116import clientToServerAPI from './ClientToServerAPI';
b69ab3117import {t} from './i18n';
b69ab3118import {dagWithPreviews, uncommittedChangesWithPreviews} from './previews';
b69ab3119import {atomResetOnCwdChange} from './repositoryData';
b69ab3120import {latestUncommittedChangesTimestamp} from './serverAPIState';
b69ab3121import {ChunkSelectState} from './stackEdit/chunkSelectState';
b69ab3122import {assert} from './utils';
b69ab3123
b69ab3124type SingleFileSelection =
b69ab3125 | false /* not selected */
b69ab3126 | true /* selected, default */
b69ab3127 | ChunkSelectState /* maybe partially selected */;
b69ab3128
b69ab3129type PartialSelectionProps = {
b69ab3130 /** Explicitly set selection. */
b69ab3131 fileMap: Immutable.Map<RepoRelativePath, SingleFileSelection>;
b69ab3132 /** For files not in fileMap, whether they are selected or not. */
b69ab3133 selectByDefault: boolean;
b69ab3134 expanded: Immutable.Set<RepoRelativePath>;
b69ab3135};
b69ab3136const PartialSelectionRecord = Immutable.Record<PartialSelectionProps>({
b69ab3137 fileMap: Immutable.Map(),
b69ab3138 selectByDefault: true,
b69ab3139 expanded: Immutable.Set(),
b69ab3140});
b69ab3141type PartialSelectionRecord = Immutable.RecordOf<PartialSelectionProps>;
b69ab3142
b69ab3143/**
b69ab3144 * Selection of partial changes made by a commit.
b69ab3145 *
b69ab3146 * Intended to be useful for both concrete commits and the `wdir()` virtual commit.
b69ab3147 * This class does not handle the differences between `wdir()` and concrete commits,
b69ab3148 * like how to load the file content, and how to get the list of changed files.
b69ab3149 * Those differences are handled at a higher level.
b69ab3150 */
b69ab3151export class PartialSelection extends SelfUpdate<PartialSelectionRecord> {
b69ab3152 constructor(record: PartialSelectionRecord) {
b69ab3153 super(record);
b69ab3154 }
b69ab3155
b69ab3156 set<K extends keyof PartialSelectionProps>(
b69ab3157 key: K,
b69ab3158 value: PartialSelectionProps[K],
b69ab3159 ): PartialSelection {
b69ab3160 return new PartialSelection(this.inner.set(key, value));
b69ab3161 }
b69ab3162
b69ab3163 /** Empty selection. */
b69ab3164 static empty(props: {selectByDefault?: boolean}): PartialSelection {
b69ab3165 return new PartialSelection(PartialSelectionRecord(props));
b69ab3166 }
b69ab3167
b69ab3168 /** Explicitly select a file. */
b69ab3169 select(path: RepoRelativePath): PartialSelection {
b69ab3170 return new PartialSelection(this.inner.setIn(['fileMap', path], true));
b69ab3171 }
b69ab3172
b69ab3173 /** Explicitly deselect a file. */
b69ab3174 deselect(path: RepoRelativePath): PartialSelection {
b69ab3175 return new PartialSelection(this.inner.setIn(['fileMap', path], false)).toggleExpand(
b69ab3176 path,
b69ab3177 false,
b69ab3178 );
b69ab3179 }
b69ab3180
b69ab3181 /** Reset to the "default" state. Useful for commit/amend. */
b69ab3182 clear(): PartialSelection {
b69ab3183 return this.set('fileMap', Immutable.Map()).set('expanded', Immutable.Set());
b69ab3184 }
b69ab3185
b69ab3186 /** Toggle expansion of a file. */
b69ab3187 toggleExpand(path: RepoRelativePath, select?: boolean): PartialSelection {
b69ab3188 const expanded = this.inner.expanded;
b69ab3189 const newExpanded =
b69ab3190 (select ?? !expanded.has(path)) ? expanded.add(path) : expanded.remove(path);
b69ab3191 return this.set('expanded', newExpanded);
b69ab3192 }
b69ab3193
b69ab3194 /** Test if a file was expanded. */
b69ab3195 isExpanded(path: RepoRelativePath): boolean {
b69ab3196 return this.inner.expanded.has(path);
b69ab3197 }
b69ab3198
b69ab3199 /** Drop "chunk selection" states. */
b69ab31100 discardPartialSelections() {
b69ab31101 const newFileMap = this.inner.fileMap.filter(
b69ab31102 fileSelection => !(fileSelection instanceof ChunkSelectState),
b69ab31103 );
b69ab31104 return new PartialSelection(this.inner.merge({fileMap: newFileMap, expanded: Immutable.Set()}));
b69ab31105 }
b69ab31106
b69ab31107 /** Start chunk selection for the given file. */
b69ab31108 startChunkSelect(
b69ab31109 path: RepoRelativePath,
b69ab31110 a: string,
b69ab31111 b: string,
b69ab31112 selected: boolean | string,
b69ab31113 normalize = false,
b69ab31114 ): PartialSelection {
b69ab31115 const chunkState = ChunkSelectState.fromText(a, b, selected, normalize);
b69ab31116 return new PartialSelection(this.inner.setIn(['fileMap', path], chunkState));
b69ab31117 }
b69ab31118
b69ab31119 /** Edit chunk selection for a file. */
b69ab31120 editChunkSelect(
b69ab31121 path: RepoRelativePath,
b69ab31122 newValue: ((chunkState: ChunkSelectState) => ChunkSelectState) | ChunkSelectState,
b69ab31123 ): PartialSelection {
b69ab31124 const chunkState = this.inner.fileMap.get(path);
b69ab31125 assert(
b69ab31126 chunkState instanceof ChunkSelectState,
b69ab31127 'PartialSelection.editChunkSelect() called without startChunkEdit',
b69ab31128 );
b69ab31129 const newChunkState = typeof newValue === 'function' ? newValue(chunkState) : newValue;
b69ab31130 return new PartialSelection(this.inner.setIn(['fileMap', path], newChunkState));
b69ab31131 }
b69ab31132
b69ab31133 getSelection(path: RepoRelativePath): SingleFileSelection {
b69ab31134 const record = this.inner;
b69ab31135 return record.fileMap.get(path) ?? record.selectByDefault;
b69ab31136 }
b69ab31137
b69ab31138 /**
b69ab31139 * Return true if a file is selected, false if deselected,
b69ab31140 * or a string with the edited content.
b69ab31141 * Even if the file is being chunk edited, this function might
b69ab31142 * still return true or false.
b69ab31143 */
b69ab31144 getSimplifiedSelection(path: RepoRelativePath): boolean | string {
b69ab31145 const selected = this.getSelection(path);
b69ab31146 if (selected === true || selected === false) {
b69ab31147 return selected;
b69ab31148 }
b69ab31149 const chunkState: ChunkSelectState = selected;
b69ab31150 const text = chunkState.getSelectedText();
b69ab31151 if (text === chunkState.a) {
b69ab31152 return false;
b69ab31153 }
b69ab31154 if (text === chunkState.b) {
b69ab31155 return true;
b69ab31156 }
b69ab31157 return text;
b69ab31158 }
b69ab31159
b69ab31160 isFullyOrPartiallySelected(path: RepoRelativePath): boolean {
b69ab31161 return this.getSimplifiedSelection(path) !== false;
b69ab31162 }
b69ab31163
b69ab31164 isPartiallySelected(path: RepoRelativePath): boolean {
b69ab31165 return typeof this.getSimplifiedSelection(path) !== 'boolean';
b69ab31166 }
b69ab31167
b69ab31168 isFullySelected(path: RepoRelativePath): boolean {
b69ab31169 return this.getSimplifiedSelection(path) === true;
b69ab31170 }
b69ab31171
b69ab31172 isDeselected(path: RepoRelativePath): boolean {
b69ab31173 return this.getSimplifiedSelection(path) === false;
b69ab31174 }
b69ab31175
b69ab31176 isEverythingSelected(getAllPaths: () => Array<RepoRelativePath>): boolean {
b69ab31177 const record = this.inner;
b69ab31178 const paths = record.selectByDefault ? record.fileMap.keySeq() : getAllPaths();
b69ab31179 return paths.every(p => this.getSimplifiedSelection(p) === true);
b69ab31180 }
b69ab31181
b69ab31182 isNothingSelected(getAllPaths: () => Array<RepoRelativePath>): boolean {
b69ab31183 const record = this.inner;
b69ab31184 const paths = record.selectByDefault ? getAllPaths() : record.fileMap.keySeq();
b69ab31185 return paths.every(p => this.getSimplifiedSelection(p) === false);
b69ab31186 }
b69ab31187
b69ab31188 /**
b69ab31189 * Produce a `ImportStack['files']` useful for the `debugimportstack` command
b69ab31190 * to create commits.
b69ab31191 *
b69ab31192 * `allPaths` provides extra file paths to be considered. This is useful
b69ab31193 * when we only track "deselected files".
b69ab31194 */
b69ab31195 calculateImportStackFiles(
b69ab31196 allPaths: Array<RepoRelativePath>,
b69ab31197 inverse = false,
b69ab31198 ): ImportCommit['files'] {
b69ab31199 const files: ImportCommit['files'] = {};
b69ab31200 // Process files in the fileMap. Note: this map might only contain the "deselected"
b69ab31201 // files, depending on selectByDefault.
b69ab31202 const fileMap = this.inner.fileMap;
b69ab31203 fileMap.forEach((fileSelection, path) => {
b69ab31204 if (fileSelection instanceof ChunkSelectState) {
b69ab31205 const text = inverse ? fileSelection.getInverseText() : fileSelection.getSelectedText();
b69ab31206 if (inverse || text !== fileSelection.a) {
b69ab31207 // The file is edited. Use the changed content.
b69ab31208 files[path] = {data: text, copyFrom: '.', flags: '.'};
b69ab31209 }
b69ab31210 } else if (fileSelection === true) {
b69ab31211 // '.' can be used for both inverse = true and false.
b69ab31212 // - For inverse = true, '.' is used with the 'write' debugimportstack command.
b69ab31213 // The 'write' command treats '.' as "working parent" to "revert" changes.
b69ab31214 // - For inverse = false, '.' is used with the 'commit' or 'amend' debugimportstack
b69ab31215 // commands. They treat '.' as "working copy" to "commit/amend" changes.
b69ab31216 files[path] = '.';
b69ab31217 }
b69ab31218 });
b69ab31219 // Process files outside the fileMap.
b69ab31220 allPaths.forEach(path => {
b69ab31221 if (!fileMap.has(path) && this.getSimplifiedSelection(path) !== false) {
b69ab31222 files[path] = '.';
b69ab31223 }
b69ab31224 });
b69ab31225 return files;
b69ab31226 }
b69ab31227
b69ab31228 /** If any file is partially selected. */
b69ab31229 hasChunkSelection(): boolean {
b69ab31230 return this.inner.fileMap
b69ab31231 .keySeq()
b69ab31232 .some(p => typeof this.getSimplifiedSelection(p) !== 'boolean');
b69ab31233 }
b69ab31234
b69ab31235 /** Get all paths with chunk selections (regardless of partial or not). */
b69ab31236 chunkSelectionPaths(): Array<RepoRelativePath> {
b69ab31237 return this.inner.fileMap
b69ab31238 .filter((v, _path) => v instanceof ChunkSelectState)
b69ab31239 .keySeq()
b69ab31240 .toArray();
b69ab31241 }
b69ab31242}
b69ab31243
b69ab31244/** Default: select all files. */
b69ab31245const defaultUncommittedPartialSelection = PartialSelection.empty({
b69ab31246 selectByDefault: true,
b69ab31247});
b69ab31248
b69ab31249/** PartialSelection for `wdir()`. See `UseUncommittedSelection` for the public API. */
b69ab31250export const uncommittedSelection = atomResetOnCwdChange<PartialSelection>(
b69ab31251 defaultUncommittedPartialSelection,
b69ab31252);
b69ab31253
b69ab31254const wdirRev = 'wdir()';
b69ab31255
b69ab31256/** PartialSelection for `wdir()` that handles loading file contents. */
b69ab31257export class UseUncommittedSelection {
b69ab31258 // Persist across `UseUncommittedSelection` life cycles.
b69ab31259 // Not an atom so updating the cache does not trigger re-render.
b69ab31260 static fileContentCache: {
b69ab31261 wdirHash: Hash;
b69ab31262 files: Map<RepoPath, ExportFile | null>;
b69ab31263 parentFiles: Map<RepoPath, ExportFile | null>;
b69ab31264 asyncLoadingLock: RateLimiter;
b69ab31265 epoch: number;
b69ab31266 } = {
b69ab31267 wdirHash: '',
b69ab31268 files: new Map(),
b69ab31269 parentFiles: new Map(),
b69ab31270 asyncLoadingLock: new RateLimiter(1),
b69ab31271 epoch: 0,
b69ab31272 };
b69ab31273
b69ab31274 constructor(
b69ab31275 public selection: PartialSelection,
b69ab31276 private setSelection: (_v: PartialSelection) => void,
b69ab31277 wdirHash: Hash,
b69ab31278 private getPaths: () => Array<RepoRelativePath>,
b69ab31279 epoch: number,
b69ab31280 ) {
b69ab31281 const cache = UseUncommittedSelection.fileContentCache;
b69ab31282 if (wdirHash !== cache.wdirHash || epoch !== cache.epoch) {
b69ab31283 // Invalidate existing cache when `.` or epoch changes.
b69ab31284 cache.files.clear();
b69ab31285 cache.parentFiles.clear();
b69ab31286 cache.wdirHash = wdirHash;
b69ab31287 cache.epoch = epoch;
b69ab31288 }
b69ab31289 }
b69ab31290
b69ab31291 /** Explicitly select a file. */
b69ab31292 select(...paths: Array<RepoRelativePath>) {
b69ab31293 let newSelection = this.selection;
b69ab31294 for (const path of paths) {
b69ab31295 newSelection = newSelection.select(path);
b69ab31296 }
b69ab31297 this.setSelection(newSelection);
b69ab31298 }
b69ab31299
b69ab31300 selectAll() {
b69ab31301 const newSelection = defaultUncommittedPartialSelection;
b69ab31302 this.setSelection(newSelection);
b69ab31303 }
b69ab31304
b69ab31305 /** Explicitly deselect a file. Also drops the related file content cache. */
b69ab31306 deselect(...paths: Array<RepoRelativePath>) {
b69ab31307 let newSelection = this.selection;
b69ab31308 const cache = UseUncommittedSelection.fileContentCache;
b69ab31309 for (const path of paths) {
b69ab31310 cache.files.delete(path);
b69ab31311 newSelection = newSelection.deselect(path);
b69ab31312 }
b69ab31313 this.setSelection(newSelection);
b69ab31314 }
b69ab31315
b69ab31316 deselectAll() {
b69ab31317 let newSelection = this.selection;
b69ab31318 this.getPaths().forEach(path => (newSelection = newSelection.deselect(path)));
b69ab31319 this.setSelection(newSelection);
b69ab31320 }
b69ab31321
b69ab31322 /** Toggle a file expansion. */
b69ab31323 toggleExpand(path: RepoRelativePath, select?: boolean) {
b69ab31324 this.setSelection(this.selection.toggleExpand(path, select));
b69ab31325 }
b69ab31326
b69ab31327 /** Test if a path is marked as expanded. */
b69ab31328 isExpanded(path: RepoRelativePath): boolean {
b69ab31329 return this.selection.isExpanded(path);
b69ab31330 }
b69ab31331
b69ab31332 /** Drop "chunk selection" states. Useful to clear states after an wdir-changing operation. */
b69ab31333 discardPartialSelections() {
b69ab31334 return this.setSelection(this.selection.discardPartialSelections());
b69ab31335 }
b69ab31336
b69ab31337 /** Restore to the default selection (select all). */
b69ab31338 clear() {
b69ab31339 const newSelection = this.selection.clear();
b69ab31340 this.setSelection(newSelection);
b69ab31341 }
b69ab31342
b69ab31343 /**
b69ab31344 * Get the chunk select state for the given path.
b69ab31345 * The file content will be loaded on demand.
b69ab31346 *
b69ab31347 * `epoch` is used to invalidate existing caches.
b69ab31348 */
b69ab31349 getChunkSelect(path: RepoRelativePath): ChunkSelectState | Promise<ChunkSelectState> {
b69ab31350 const fileSelection = this.selection.inner.fileMap.get(path);
b69ab31351 const cache = UseUncommittedSelection.fileContentCache;
b69ab31352
b69ab31353 let maybeStaleResult = undefined;
b69ab31354 if (fileSelection instanceof ChunkSelectState) {
b69ab31355 maybeStaleResult = fileSelection;
b69ab31356 if (cache.files.has(path)) {
b69ab31357 // Up to date.
b69ab31358 return maybeStaleResult;
b69ab31359 } else {
b69ab31360 // Cache invalidated by constructor.
b69ab31361 // Trigger a new fetch below.
b69ab31362 // Still return `maybeStaleResult` to avoid flakiness.
b69ab31363 }
b69ab31364 }
b69ab31365
b69ab31366 const maybeReadFromCache = (): ChunkSelectState | null => {
b69ab31367 const file = cache.files.get(path);
b69ab31368 if (file === undefined) {
b69ab31369 return null;
b69ab31370 }
b69ab31371 const parentPath = file?.copyFrom ?? path;
b69ab31372 const parentFile = cache.parentFiles.get(parentPath);
b69ab31373 if (parentFile?.dataBase85 || file?.dataBase85) {
b69ab31374 throw new Error(t('Cannot edit non-utf8 file'));
b69ab31375 }
b69ab31376 const a = parentFile?.data ?? '';
b69ab31377 const b = file?.data ?? '';
b69ab31378 const existing = this.getSelection(path);
b69ab31379 let selected: string | boolean;
b69ab31380 if (existing instanceof ChunkSelectState) {
b69ab31381 if (existing.a === a && existing.b === b) {
b69ab31382 return existing;
b69ab31383 }
b69ab31384 selected = existing.getSelectedText();
b69ab31385 } else {
b69ab31386 selected = existing;
b69ab31387 }
b69ab31388 const newSelection = this.selection.startChunkSelect(path, a, b, selected, true);
b69ab31389 this.setSelection(newSelection);
b69ab31390 const newSelected = newSelection.getSelection(path);
b69ab31391 assert(
b69ab31392 newSelected instanceof ChunkSelectState,
b69ab31393 'startChunkSelect() should provide ChunkSelectState',
b69ab31394 );
b69ab31395 return newSelected;
b69ab31396 };
b69ab31397
b69ab31398 const promise = cache.asyncLoadingLock.enqueueRun(async () => {
b69ab31399 const chunkState = maybeReadFromCache();
b69ab31400 if (chunkState !== null) {
b69ab31401 return chunkState;
b69ab31402 }
b69ab31403
b69ab31404 // Not found in cache. Need to (re)load the file via the server.
b69ab31405
b69ab31406 const revs = wdirRev;
b69ab31407 // Setup event listener before sending the request.
b69ab31408 const iter = clientToServerAPI.iterateMessageOfType('exportedStack');
b69ab31409 // Explicitly ask for the file via assumeTracked. Note this also provides contents
b69ab31410 // of other tracked files.
b69ab31411 clientToServerAPI.postMessage({type: 'exportStack', revs, assumeTracked: [path]});
b69ab31412 for await (const event of iter) {
b69ab31413 if (event.revs !== revs) {
b69ab31414 // Ignore unrelated response.
b69ab31415 continue;
b69ab31416 }
b69ab31417 if (event.error) {
b69ab31418 throw new Error(event.error);
b69ab31419 }
b69ab31420 if (event.stack.some(c => !c.requested && c.node !== cache.wdirHash)) {
b69ab31421 // The wdirHash has changed. Fail the load.
b69ab31422 // The exported stack usually has a non-requested commit that is the parent of
b69ab31423 // the requested "wdir()", which is the "." commit that should match `wdirHash`.
b69ab31424 // Note: for an empty repo there is no such non-requested commit exported so
b69ab31425 // we skip the check in that case.
b69ab31426 throw new Error(t('Working copy has changed'));
b69ab31427 }
b69ab31428
b69ab31429 // Update cache.
b69ab31430 event.stack.forEach(commit => {
b69ab31431 if (commit.requested) {
b69ab31432 mergeObjectToMap(commit.files, cache.files);
b69ab31433 } else {
b69ab31434 mergeObjectToMap(commit.relevantFiles, cache.parentFiles);
b69ab31435 }
b69ab31436 });
b69ab31437
b69ab31438 // Try read from cache again.
b69ab31439 const chunkState = maybeReadFromCache();
b69ab31440 if (chunkState === null) {
b69ab31441 if (event.assumeTracked.includes(path)) {
b69ab31442 // We explicitly requested the file, but the server does not provide
b69ab31443 // it somehow.
b69ab31444 break;
b69ab31445 } else {
b69ab31446 // It's possible that there are multiple export requests.
b69ab31447 // This one does not provide the file we want, continue checking other responses.
b69ab31448 continue;
b69ab31449 }
b69ab31450 } else {
b69ab31451 return chunkState;
b69ab31452 }
b69ab31453 }
b69ab31454
b69ab31455 // Handles the `break` above. Tells tsc that we don't return undefined.
b69ab31456 throw new Error(t('Unable to get file content unexpectedly'));
b69ab31457 });
b69ab31458
b69ab31459 return maybeStaleResult ?? promise;
b69ab31460 }
b69ab31461
b69ab31462 /** Edit chunk selection for a file. */
b69ab31463 editChunkSelect(
b69ab31464 path: RepoRelativePath,
b69ab31465 newValue: ((chunkState: ChunkSelectState) => ChunkSelectState) | ChunkSelectState,
b69ab31466 ) {
b69ab31467 const newSelection = this.selection.editChunkSelect(path, newValue);
b69ab31468 this.setSelection(newSelection);
b69ab31469 }
b69ab31470
b69ab31471 // ---------- Read-only methods below ----------
b69ab31472
b69ab31473 /**
b69ab31474 * Return true if a file is selected (default), false if deselected,
b69ab31475 * or a string with the edited content.
b69ab31476 */
b69ab31477 getSelection(path: RepoRelativePath): SingleFileSelection {
b69ab31478 return this.selection.getSelection(path);
b69ab31479 }
b69ab31480
b69ab31481 isFullyOrPartiallySelected(path: RepoRelativePath): boolean {
b69ab31482 return this.selection.isFullyOrPartiallySelected(path);
b69ab31483 }
b69ab31484
b69ab31485 isPartiallySelected(path: RepoRelativePath): boolean {
b69ab31486 return this.selection.isPartiallySelected(path);
b69ab31487 }
b69ab31488
b69ab31489 isFullySelected(path: RepoRelativePath): boolean {
b69ab31490 return this.selection.isFullySelected(path);
b69ab31491 }
b69ab31492
b69ab31493 isDeselected(path: RepoRelativePath): boolean {
b69ab31494 return this.selection.isDeselected(path);
b69ab31495 }
b69ab31496
b69ab31497 isEverythingSelected(): boolean {
b69ab31498 return this.selection.isEverythingSelected(this.getPaths);
b69ab31499 }
b69ab31500
b69ab31501 isNothingSelected(): boolean {
b69ab31502 return this.selection.isNothingSelected(this.getPaths);
b69ab31503 }
b69ab31504
b69ab31505 hasChunkSelection(): boolean {
b69ab31506 return this.selection.hasChunkSelection();
b69ab31507 }
b69ab31508}
b69ab31509
b69ab31510export const isFullyOrPartiallySelected = atom(get => {
b69ab31511 const sel = get(uncommittedSelection);
b69ab31512 const uncommittedChanges = get(uncommittedChangesWithPreviews);
b69ab31513 const epoch = get(latestUncommittedChangesTimestamp);
b69ab31514 const dag = get(dagWithPreviews);
b69ab31515 const wdirHash = dag.resolve('.')?.hash ?? '';
b69ab31516 const getPaths = () => uncommittedChanges.map(c => c.path);
b69ab31517 const emptyFunction = () => {};
b69ab31518 // NOTE: Usually UseUncommittedSelection is only used in React, but this is
b69ab31519 // a private shortcut path to get the logic outside React.
b69ab31520 const selection = new UseUncommittedSelection(sel, emptyFunction, wdirHash, getPaths, epoch);
b69ab31521 return selection.isFullyOrPartiallySelected.bind(selection);
b69ab31522});
b69ab31523
b69ab31524/** react hook to get the uncommitted selection state. */
b69ab31525export function useUncommittedSelection() {
b69ab31526 const [selection, setSelection] = useAtom(uncommittedSelection);
b69ab31527 const uncommittedChanges = useAtomValue(uncommittedChangesWithPreviews);
b69ab31528 const epoch = useAtomValue(latestUncommittedChangesTimestamp);
b69ab31529 const dag = useAtomValue(dagWithPreviews);
b69ab31530 const wdirHash = dag.resolve('.')?.hash ?? '';
b69ab31531 const getPaths = () => uncommittedChanges.map(c => c.path);
b69ab31532
b69ab31533 return new UseUncommittedSelection(selection, setSelection, wdirHash, getPaths, epoch);
b69ab31534}
b69ab31535
b69ab31536function mergeObjectToMap<V>(obj: {[path: string]: V} | undefined, map: Map<string, V>) {
b69ab31537 if (obj === undefined) {
b69ab31538 return;
b69ab31539 }
b69ab31540 for (const k in obj) {
b69ab31541 const v = obj[k];
b69ab31542 map.set(k, v);
b69ab31543 }
b69ab31544}