All files / src/ui historyMenu.ts

84.28% Statements 59/70
45.23% Branches 19/42
100% Functions 5/5
84.28% Lines 59/70

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107                              2x   2x   2x 2x       2x                 2x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x 2x 2x 2x 1x 1x     2x 7x 7x 4x 4x 4x 7x   7x 2x 7x   7x 1x 7x   7x   7x   7x   7x         7x   7x 7x           2x 6x 6x 6x 6x 5x 6x 6x 6x 6x 6x 6x 6x 6x   1x 1x 1x  
/**
 * The header's list of steps back (§5.5): what each step behind the one on
 * screen was, read off the hash it wrote.
 *
 * A step is a hash and nothing else, because that is the whole of what an
 * entry of this window's history is (§5.5) — so what it is called is worked
 * out from the view state when the list opens rather than recorded when the
 * step was taken, and two steps a list cannot tell apart are two states that
 * differ only in something the hash does not carry. Each row names the place,
 * as the breadcrumb's last crumb or the view's heading would, and beside it,
 * quietly, the file and what the step set on it — the resolution, the O/E, a
 * comparison — which is how a row that names the same window as the one above
 * it says what changed.
 */
 
import { pairLabel, regionLabel } from '../genome/assembly'
import type { StepBack } from '../state/historyJournal'
import { parseViewState } from '../state/ViewState'
import type { ViewState } from '../types'
import { resolutionLabel } from '../units'
import { entryLabel, fileBaseLabel } from './labels'
import type { MenuItem } from './Menu'
 
/** How many steps back the list shows, nearest first: a menu longer than this is a page. */
export const HISTORY_ROWS = 20
 
/**
 * The rows of the list, each going back to its step.
 *
 * `here` is the file on screen: a step in it does not say so, since every row
 * of a list that does reads the same name down its right-hand side and the
 * one row on another file is the one that has to stand out.
 */
export function historyItems(
  back: readonly StepBack[],
  go: (delta: number) => void,
  here?: string,
): MenuItem[] {
  return back.slice(0, HISTORY_ROWS).map(({ step, delta }) => {
    const state = step.hash === '' ? null : parseViewState(step.hash)
    const label = state === null ? 'Nothing open' : stepPlace(state)
    const hint = state === null ? '' : stepDetail(state, here)
    return {
      label,
      ...(hint === '' ? {} : { hint }),
      title: `${-delta} step${delta === -1 ? '' : 's'} back${hint === '' ? '' : ` · ${hint}`}`,
      run: () => go(delta),
    }
  })
}
 
/** Where a step was: the window, the table or the picture it opened. */
export function stepPlace(state: ViewState): string {
  switch (state.view) {
    case 'region':
      if (state.region === undefined) return 'A window'
      if (state.region2 !== undefined) return pairLabel(state.region, state.region2)
      return regionLabel(state.focus ?? state.region)
    case 'browser':
      return state.locus ?? 'Whole genome'
    case 'whole':
      return 'Whole genome'
    case 'overview':
      return 'Whole metagenome'
    case 'contigs':
      return state.genome !== undefined ? `${state.genome} · contigs` : isMetacool(state.uri) ? 'Contigs' : 'Chromosomes'
    case 'genomes':
      return 'Genomes'
    case 'cells':
      return 'Cells'
    case 'genome':
      return state.genome ?? 'A genome'
    case 'pair':
      return state.genome !== undefined && state.genome2 !== undefined ? `${state.genome} × ${state.genome2}` : 'Two genomes'
    case 'selection': {
      const names = (state.regions ?? state.genomes ?? '').split(',').map((name) => name.trim()).filter((name) => name !== '')
      if (names.length === 0) return 'A selection'
      return names.length <= 3 ? names.join(' × ') : `${names.length} ${state.genomes !== undefined ? 'genomes' : 'sequences'}`
    }
    default:
      return state.view
  }
}
 
/**
 * The file a step was on — unless it is `here`, the file on screen — and what
 * it set there that a place does not say.
 */
export function stepDetail(state: ViewState, here?: string): string {
  const parts = here !== undefined && fileBaseLabel(here) === fileBaseLabel(state.uri) ? [] : [fileBaseLabel(state.uri)]
  const entry = entryLabel(state.uri)
  const rung = entry !== undefined && /^\d+$/.test(entry) ? Number(entry) : null
  if (state.coarsen !== undefined) parts.push(`${resolutionLabel(state.coarsen)}*`)
  else if (rung !== null) parts.push(resolutionLabel(rung))
  if (state.view === 'browser') parts.push('tracks')
  if (state.score !== undefined) parts.push(state.score)
  if (state.oe === true) parts.push('O/E')
  if (state.op !== undefined) parts.push(state.op)
  if (state.flip === true) parts.push('turned')
  if (state.smooth !== undefined && state.smooth !== '0') parts.push('smoothed')
  return parts.join(' · ')
}
 
function isMetacool(uri: string): boolean {
  return /\.m?metacool(?:$|::|[?#])/i.test(uri)
}