|
| 1 | +// Package minimapview renders nook's minimap column: it owns the "what a row |
| 2 | +// looks like" half of the overview gutter that the minimap package deliberately |
| 3 | +// leaves to the host. Given a minimap.Model (where each row sits) and the code |
| 4 | +// density of the document's lines (how full each line is), it produces the |
| 5 | +// per-row cells the host paints — the shade ramp that stands in for content and |
| 6 | +// the viewport highlight band. Like minimap itself it is pure geometry over |
| 7 | +// numbers: no I/O, no buffer access, no lipgloss, constant-time per frame, so it |
| 8 | +// stays inside nook's first-paint rule. The host computes a fill per line once |
| 9 | +// from the buffer, maps it onto rows with Density, turns the rows into Cells |
| 10 | +// with Column, and applies its own theme styles to Glyph and InViewport. |
| 11 | +package minimapview |
| 12 | + |
| 13 | +import "github.com/truffle-dev/glyph/cmd/nook/internal/minimap" |
| 14 | + |
| 15 | +// Shade ramp, from empty to full. A row's code density picks one glyph: a blank |
| 16 | +// region reads as space, a dense region as a full block, with three shades in |
| 17 | +// between so the overview has depth instead of an on/off silhouette. |
| 18 | +const ( |
| 19 | + shadeEmpty = ' ' |
| 20 | + shadeLight = '░' // U+2591 |
| 21 | + shadeMedium = '▒' // U+2592 |
| 22 | + shadeDark = '▓' // U+2593 |
| 23 | + shadeFull = '█' // U+2588 |
| 24 | +) |
| 25 | + |
| 26 | +// Cell is the drawable state of one minimap row, ready for the host to style. |
| 27 | +// Glyph is the density shade; InViewport marks the rows under the highlight |
| 28 | +// band; Blank marks rows past the end of a short document, which paint empty |
| 29 | +// regardless of density. |
| 30 | +type Cell struct { |
| 31 | + Glyph rune |
| 32 | + InViewport bool |
| 33 | + Blank bool |
| 34 | +} |
| 35 | + |
| 36 | +// Shade maps a code-density fraction to the overview ramp glyph. Values are |
| 37 | +// clamped to [0,1]; exactly zero is the empty glyph, and the four filled bands |
| 38 | +// split the remainder into quarters so a barely-filled row still shows. |
| 39 | +func Shade(density float64) rune { |
| 40 | + switch { |
| 41 | + case density <= 0: |
| 42 | + return shadeEmpty |
| 43 | + case density <= 0.25: |
| 44 | + return shadeLight |
| 45 | + case density <= 0.5: |
| 46 | + return shadeMedium |
| 47 | + case density <= 0.75: |
| 48 | + return shadeDark |
| 49 | + default: |
| 50 | + return shadeFull |
| 51 | + } |
| 52 | +} |
| 53 | + |
| 54 | +// LineFill returns the code density of a single source line in [0,1]: the |
| 55 | +// fraction of its runes that are not whitespace. A blank or whitespace-only |
| 56 | +// line reads as empty (0), a line with no whitespace as full (1), and typical |
| 57 | +// code somewhere between so indentation and sparse lines read lighter than |
| 58 | +// dense ones. Rune-counted, not byte-counted, so multi-byte source measures |
| 59 | +// the same as ASCII. |
| 60 | +func LineFill(line string) float64 { |
| 61 | + total := 0 |
| 62 | + code := 0 |
| 63 | + for _, r := range line { |
| 64 | + total++ |
| 65 | + if !isSpace(r) { |
| 66 | + code++ |
| 67 | + } |
| 68 | + } |
| 69 | + if total == 0 { |
| 70 | + return 0 |
| 71 | + } |
| 72 | + return float64(code) / float64(total) |
| 73 | +} |
| 74 | + |
| 75 | +func isSpace(r rune) bool { |
| 76 | + return r == ' ' || r == '\t' || r == '\n' || r == '\r' || r == '\v' || r == '\f' |
| 77 | +} |
| 78 | + |
| 79 | +// Density collapses per-line fills onto minimap rows for a Model. fills[i] is |
| 80 | +// the code density of logical line i (see LineFill); the host computes it once |
| 81 | +// from the buffer. Each returned entry is the mean fill of the lines its row |
| 82 | +// covers (minimap.LineRange), so a downsampled row reads as the average density |
| 83 | +// of the block it stands for. The result is always m.MapHeight long (empty when |
| 84 | +// the map is inactive); rows past a short document average to 0. A line index |
| 85 | +// beyond len(fills) counts as 0 so a stale-but-shorter fills slice degrades to |
| 86 | +// a lighter overview rather than panicking. |
| 87 | +func Density(m minimap.Model, fills []float64) []float64 { |
| 88 | + if m.MapHeight <= 0 { |
| 89 | + return nil |
| 90 | + } |
| 91 | + out := make([]float64, m.MapHeight) |
| 92 | + if !m.Active() { |
| 93 | + return out |
| 94 | + } |
| 95 | + for row := 0; row < m.MapHeight; row++ { |
| 96 | + start, end := m.LineRange(row) |
| 97 | + if end <= start { |
| 98 | + continue // past the document: leave 0 |
| 99 | + } |
| 100 | + sum := 0.0 |
| 101 | + for line := start; line < end; line++ { |
| 102 | + if line >= 0 && line < len(fills) { |
| 103 | + sum += fills[line] |
| 104 | + } |
| 105 | + } |
| 106 | + out[row] = sum / float64(end-start) |
| 107 | + } |
| 108 | + return out |
| 109 | +} |
| 110 | + |
| 111 | +// Column builds the per-row cells for the whole minimap. density should be |
| 112 | +// m.MapHeight long (as Density returns); a shorter slice treats the missing |
| 113 | +// rows as empty and a longer one ignores the tail, so the caller never has to |
| 114 | +// size-match exactly. Rows whose LineRange is empty (past a short document) are |
| 115 | +// Blank; rows inside m.ViewportWindow() are InViewport. The result is always |
| 116 | +// m.MapHeight long, or empty when the column has no rows. |
| 117 | +func Column(m minimap.Model, density []float64) []Cell { |
| 118 | + if m.MapHeight <= 0 { |
| 119 | + return nil |
| 120 | + } |
| 121 | + cells := make([]Cell, m.MapHeight) |
| 122 | + vStart, vEnd := m.ViewportWindow() |
| 123 | + for row := 0; row < m.MapHeight; row++ { |
| 124 | + c := Cell{Glyph: shadeEmpty} |
| 125 | + if !m.Active() { |
| 126 | + c.Blank = true |
| 127 | + cells[row] = c |
| 128 | + continue |
| 129 | + } |
| 130 | + start, end := m.LineRange(row) |
| 131 | + if end <= start { |
| 132 | + c.Blank = true |
| 133 | + } else { |
| 134 | + d := 0.0 |
| 135 | + if row < len(density) { |
| 136 | + d = density[row] |
| 137 | + } |
| 138 | + c.Glyph = Shade(d) |
| 139 | + } |
| 140 | + if row >= vStart && row < vEnd { |
| 141 | + c.InViewport = true |
| 142 | + } |
| 143 | + cells[row] = c |
| 144 | + } |
| 145 | + return cells |
| 146 | +} |
| 147 | + |
| 148 | +// Runes returns just the glyph column, one row per line, newline-joined and |
| 149 | +// with no viewport styling. It is the plain-text view of a Column — handy for |
| 150 | +// tests and for a host that paints the overview without color. |
| 151 | +func Runes(cells []Cell) string { |
| 152 | + if len(cells) == 0 { |
| 153 | + return "" |
| 154 | + } |
| 155 | + out := make([]rune, 0, len(cells)*2) |
| 156 | + for i, c := range cells { |
| 157 | + if i > 0 { |
| 158 | + out = append(out, '\n') |
| 159 | + } |
| 160 | + out = append(out, c.Glyph) |
| 161 | + } |
| 162 | + return string(out) |
| 163 | +} |
0 commit comments