/ ink / optimizer.ts
optimizer.ts
 1  import type { Diff } from './frame.js'
 2  
 3  /**
 4   * Optimize a diff by applying all optimization rules in a single pass.
 5   * This reduces the number of patches that need to be written to the terminal.
 6   *
 7   * Rules applied:
 8   * - Remove empty stdout patches
 9   * - Merge consecutive cursorMove patches
10   * - Remove no-op cursorMove (0,0) patches
11   * - Concat adjacent style patches (transition diffs — can't drop either)
12   * - Dedupe consecutive hyperlinks with same URI
13   * - Cancel cursor hide/show pairs
14   * - Remove clear patches with count 0
15   */
16  export function optimize(diff: Diff): Diff {
17    if (diff.length <= 1) {
18      return diff
19    }
20  
21    const result: Diff = []
22    let len = 0
23  
24    for (const patch of diff) {
25      const type = patch.type
26  
27      // Skip no-ops
28      if (type === 'stdout') {
29        if (patch.content === '') continue
30      } else if (type === 'cursorMove') {
31        if (patch.x === 0 && patch.y === 0) continue
32      } else if (type === 'clear') {
33        if (patch.count === 0) continue
34      }
35  
36      // Try to merge with previous patch
37      if (len > 0) {
38        const lastIdx = len - 1
39        const last = result[lastIdx]!
40        const lastType = last.type
41  
42        // Merge consecutive cursorMove
43        if (type === 'cursorMove' && lastType === 'cursorMove') {
44          result[lastIdx] = {
45            type: 'cursorMove',
46            x: last.x + patch.x,
47            y: last.y + patch.y,
48          }
49          continue
50        }
51  
52        // Collapse consecutive cursorTo (only the last one matters)
53        if (type === 'cursorTo' && lastType === 'cursorTo') {
54          result[lastIdx] = patch
55          continue
56        }
57  
58        // Concat adjacent style patches. styleStr is a transition diff
59        // (computed by diffAnsiCodes(from, to)), not a setter — dropping
60        // the first is only sound if its undo-codes are a subset of the
61        // second's, which is NOT guaranteed. e.g. [\e[49m, \e[2m]: dropping
62        // the bg reset leaks it into the next \e[2J/\e[2K via BCE.
63        if (type === 'styleStr' && lastType === 'styleStr') {
64          result[lastIdx] = { type: 'styleStr', str: last.str + patch.str }
65          continue
66        }
67  
68        // Dedupe hyperlinks
69        if (
70          type === 'hyperlink' &&
71          lastType === 'hyperlink' &&
72          patch.uri === last.uri
73        ) {
74          continue
75        }
76  
77        // Cancel cursor hide/show pairs
78        if (
79          (type === 'cursorShow' && lastType === 'cursorHide') ||
80          (type === 'cursorHide' && lastType === 'cursorShow')
81        ) {
82          result.pop()
83          len--
84          continue
85        }
86      }
87  
88      result.push(patch)
89      len++
90    }
91  
92    return result
93  }