From d32f50b6f7dd15f60ea51928c814fdc4ac7738ed Mon Sep 17 00:00:00 2001 From: Vladislav Deryabkin <53311479+evermake@users.noreply.github.com> Date: Mon, 12 Aug 2024 13:00:05 +0300 Subject: [PATCH] fix: sync navigation with rendering in interactive mode (#133) --- src/commands/check/interactive.ts | 83 ++++++++++++++++++++----------- src/commands/check/render.ts | 9 ++-- src/types.ts | 10 +++- 3 files changed, 67 insertions(+), 35 deletions(-) diff --git a/src/commands/check/interactive.ts b/src/commands/check/interactive.ts index 69abd48..295281f 100644 --- a/src/commands/check/interactive.ts +++ b/src/commands/check/interactive.ts @@ -18,30 +18,33 @@ export async function promptInteractive(pkgs: PackageMeta[], options: CheckOptio group = true, } = options - pkgs.forEach((i) => { - i.interactiveChecked = true - i.resolved.forEach((i) => { - i.interactiveChecked = i.update - if (i.latestVersionAvailable && !i.update) { - i.interactiveChecked = false - i.update = true - updateTargetVersion(i, i.latestVersionAvailable, undefined, options.includeLocked) + const checked = new Set() + + pkgs.forEach((pkg) => { + pkg.resolved.forEach((dep) => { + if (dep.update) { + checked.add(dep) + } + else if (dep.latestVersionAvailable) { + // Set `update` flag to true to render option in the list, + // but don't check it by default. + dep.update = true + updateTargetVersion(dep, dep.latestVersionAvailable, undefined, options.includeLocked) } }) - i.resolved = sortDepChanges(i.resolved, sort, group) }) - if (!pkgs.some(i => i.resolved.some(i => i.update))) + if (flatDeps().length === 0) return [] const promise = createControlledPromise() - const listRenderer = createListRenderer() - let renderer: InteractiveRenderer = listRenderer + sortDeps() + let renderer: InteractiveRenderer = createListRenderer() registerInput() - renderer.render() + return await promise .finally(() => { renderer = { @@ -52,13 +55,26 @@ export async function promptInteractive(pkgs: PackageMeta[], options: CheckOptio // ==== functions ==== - function createListRenderer(): InteractiveRenderer { - const deps = pkgs.flatMap(i => i.resolved.filter(i => i.update)) + function flatDeps() { + return pkgs.flatMap(pkg => pkg.resolved.filter(dep => dep.update)) + } + + function sortDeps() { + pkgs.forEach((pkg) => { + pkg.resolved = sortDepChanges(pkg.resolved, sort, group) + }) + } + + function createListRenderer(initialSelected?: ResolvedDepChange): InteractiveRenderer { + const deps = flatDeps() + let index = 0 + if (initialSelected) + index = Math.max(0, deps.findIndex(dep => dep === initialSelected)) + const ctx: InteractiveContext = { - isSelected(dep) { - return dep === deps[index] - }, + isChecked: dep => checked.has(dep), + isSelected: dep => dep === deps[index], } return { @@ -77,17 +93,15 @@ export async function promptInteractive(pkgs: PackageMeta[], options: CheckOptio sr.render(index) }, onKey(key) { - const allInteractiveChecked = deps.every(d => d.interactiveChecked) - switch (key.name) { case 'escape': process.exit() case 'enter': case 'return': console.clear() - pkgs.forEach((i) => { - i.resolved.forEach((i) => { - i.update = !!i.interactiveChecked + pkgs.forEach((pkg) => { + pkg.resolved.forEach((dep) => { + dep.update = ctx.isChecked(dep) }) }) promise.resolve(pkgs) @@ -100,15 +114,23 @@ export async function promptInteractive(pkgs: PackageMeta[], options: CheckOptio case 'j': index = (index + 1) % deps.length return true - case 'space': - deps[index].interactiveChecked = !deps[index].interactiveChecked + case 'space': { + const dep = deps[index] + if (checked.has(dep)) + checked.delete(dep) + else + checked.add(dep) return true + } case 'right': case 'l': renderer = createVersionSelectRender(deps[index]) return true case 'a': - deps.forEach(d => d.interactiveChecked = !allInteractiveChecked) + if (deps.every(dep => checked.has(dep))) + checked.clear() + else + deps.forEach(dep => checked.add(dep)) return true } }, @@ -160,7 +182,7 @@ export async function promptInteractive(pkgs: PackageMeta[], options: CheckOptio onKey(key) { switch (key.name) { case 'escape': - renderer = listRenderer + renderer = createListRenderer(dep) return true case 'up': case 'k': @@ -178,7 +200,12 @@ export async function promptInteractive(pkgs: PackageMeta[], options: CheckOptio case 'h': case 'l': updateTargetVersion(dep, versions[index].version, undefined, options.includeLocked) - renderer = listRenderer + + // Order may have changed so we need to sort to keep navigation + // in sync with the rendering. + sortDeps() + + renderer = createListRenderer(dep) return true } }, diff --git a/src/commands/check/render.ts b/src/commands/check/render.ts index c5dcbd6..c1a9e7c 100644 --- a/src/commands/check/render.ts +++ b/src/commands/check/render.ts @@ -18,12 +18,11 @@ export function renderChange( interactive?: InteractiveContext, grouped = false, ) { - const update = change.update && (!interactive || change.interactiveChecked) - const isSelected = interactive && interactive.isSelected(change) + const update = change.update && (!interactive || interactive.isChecked(change)) const pre = interactive ? [ - isSelected ? FIG_POINTER : FIG_NO_POINTER, - change.interactiveChecked ? FIG_CHECK : FIG_UNCHECK, + interactive.isSelected(change) ? FIG_POINTER : FIG_NO_POINTER, + interactive.isChecked(change) ? FIG_CHECK : FIG_UNCHECK, ].join('') : ' ' @@ -70,7 +69,7 @@ export function renderChanges( if (changes.length) { const diffCounts: Record = {} changes - .filter(i => !interactive || i.interactiveChecked) + .filter(dep => !interactive || interactive.isChecked(dep)) .forEach(({ diff }) => { if (!diff) return diff --git a/src/types.ts b/src/types.ts index e5d7a18..90b9ad5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -52,7 +52,6 @@ export interface ResolvedDepChange extends RawDep { diff: DiffType pkgData: PackageData resolveError?: Error | string | null - interactiveChecked?: boolean aliasName?: string } @@ -149,12 +148,19 @@ export interface PackageMeta { * Resolved dependencies */ resolved: ResolvedDepChange[] - interactiveChecked?: boolean } export type DependencyFilter = (dep: RawDep) => boolean | Promise export type DependencyResolvedCallback = (packageName: string | null, depName: string, progress: number, total: number) => void export interface InteractiveContext { + /** + * Whether the dependency is selected with cursor in the interactive list. + */ isSelected: (dep: RawDep) => boolean + + /** + * Whether the dependency is marked as checked in the interactive list. + */ + isChecked: (dep: RawDep) => boolean }