All files / src/edit adjust.ts

96.96% Statements 64/66
79.06% Branches 34/43
100% Functions 5/5
96.96% Lines 64/66

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                                2x 2x 2x     2x 15x 15x           2x 10x 10x 10x 10x               2x 9x 9x 3x 3x 3x 1x 3x 1x 1x 6x 9x 5x 5x 9x 4x 9x 4x 9x 2x 2x 2x 2x       2x 2x 2x 2x 2x 1x 2x     3x 9x 9x 9x 9x   4x 4x 4x 4x 4x 4x 4x 4x 4x 4x     4x 4x 4x 4x     4x 4x 4x 4x  
/**
 * A suggestion with one of its ends dragged (SPECS §27.6): in review, before
 * answering it, the stretch it turns or moves begun or ended elsewhere, or the
 * point it splits at moved — snapped, as every point on the stage is.
 *
 * The step, the words and the marks follow the end; the confidence stays as
 * the method gave it, since the method weighed the suggestion and not the
 * hand's version of it, and `adjusted` is added to what it rests on so the
 * history says whose ends they were. The key stays too: an answer to the
 * adjusted suggestion is an answer to the suggestion, and taught as one.
 *
 * Pure, over the lens: it is tested without a stage.
 */
 
import type { EditStep } from '../genome/edit'
import type { LensGrid } from './grid'
import { stretchBetween, stretchUnits } from './gestures'
import { edgeBp, type Suggestion } from './suggest'
import { stepText } from './words'
 
/** Whether a suggestion has ends a hand can drag: a stretch turned or moved, or a split's point. */
export function adjustable(suggestion: Suggestion): boolean {
  return suggestion.kind === 'split' || (suggestion.stretch !== null && (suggestion.step.op === 'invert' || suggestion.step.op === 'move'))
}
 
/**
 * The ends a drag can take hold of, in units of the edited axis: a stretch's
 * two ends, or a split's one point.
 */
export function adjustableEnds(suggestion: Suggestion): number[] {
  if (!adjustable(suggestion)) return []
  if (suggestion.kind === 'split') return suggestion.breaks.slice(0, 1)
  return suggestion.stretch === null ? [] : [suggestion.stretch[0], suggestion.stretch[1]]
}
 
/**
 * The suggestion with its end `end` — `0` the low one, `1` the high one; a
 * split's only one is `0` — at `unit`, or `null` where that names no step: a
 * stretch of nothing, a split at a sequence's edge, a move whose place in its
 * own sequence would be inside the stretch it moves.
 */
export function adjustSuggestion(grid: LensGrid, suggestion: Suggestion, end: 0 | 1, unit: number): Suggestion | null {
  const step = suggestion.step
  if (suggestion.kind === 'split' && step.op === 'split') {
    const at = suggestion.breaks[0]
    const sequence = at === undefined ? null : grid.sequenceAt(Math.min(grid.n - 1, Math.max(0, at)))
    if (sequence === null || !(unit > sequence.from && unit < sequence.to)) return null
    const bp = edgeBp(grid, sequence, unit)
    if (!(bp > 0 && bp < sequence.length)) return null
    return adjusted(suggestion, { ...step, at: bp }, { breaks: [unit], extent: [unit, unit], stretch: null })
  }
  const held = suggestion.stretch
  if (held === null || !adjustable(suggestion)) return null
  const other = held[1 - end]
  const stretch = stretchBetween(grid, other, unit)
  if (stretch === null) return null
  const units = stretchUnits(grid, stretch)
  if (units === null) return null
  let next: EditStep
  if (step.op === 'invert') {
    next = { op: 'invert', sequence: stretch.sequence, from: stretch.from, to: stretch.to }
  } else if (step.op === 'move') {
    if (stretch.sequence !== step.sequence) return null
    let at = step.at
    // In its own sequence, the place it goes is measured with the stretch
    // taken out: kept where it was on the sequence as it stands, and refused
    // where the stretch would now hold it.
    if (step.into === step.sequence) {
      const place = step.at < step.from ? step.at : step.at + (step.to - step.from)
      if (place > stretch.from && place < stretch.to) return null
      at = place <= stretch.from ? place : place - (stretch.to - stretch.from)
    }
    next = { ...step, from: stretch.from, to: stretch.to, at }
  } else {
    return null
  }
  const target = suggestion.target
  const lo = Math.min(units[0], target ?? units[0])
  const hi = Math.max(units[1], target ?? units[1])
  return adjusted(suggestion, next, { breaks: [units[0], units[1]], extent: [lo, hi], stretch: units })
}
 
function adjusted(
  suggestion: Suggestion,
  step: EditStep,
  where: Pick<Suggestion, 'breaks' | 'extent' | 'stretch'>,
): Suggestion {
  return {
    ...suggestion,
    ...where,
    step,
    words: stepText(step),
    // Where the ends it would join now are is the hand's to say, not the
    // method's corners, which were for the ends it found.
    corners: [],
    evidence: handled(suggestion.evidence),
  }
}
 
/** What an adjusted suggestion rests on: its ends are the hand's now, not the finer rungs' (§27.5). */
function handled(evidence: readonly string[]): string[] {
  const kept = evidence.filter((chip) => !chip.startsWith('refined at '))
  return kept.includes('adjusted') ? kept : [...kept, 'adjusted']
}