v0.1.0 47aaa0f

visual.go RawHistoryFolder

1package text
2
3import (
4	"fmt"
5	"sort"
6
7	"github.com/bnema/nefergui/internal/bidi"
8	"github.com/go-text/typesetting/di"
9	"github.com/go-text/typesetting/shaping"
10)
11
12func fullBreak(text []rune) []int {
13	if len(text) > 0 {
14		return []int{len(text)}
15	}
16	return nil
17}
18
19// splitLevels prevents a single shaping run from crossing resolved paragraph
20// levels (not merely direction parity). The wrapper can subsequently split
21// further at UAX#14 break opportunities.
22func splitLevels(items []shaping.Input, levels []uint8) []shaping.Input {
23	var out []shaping.Input
24	for _, item := range items {
25		if item.RunStart >= item.RunEnd {
26			continue
27		}
28		start := item.RunStart
29		for i := start + 1; i < item.RunEnd; i++ {
30			if levels[i] != levels[start] {
31				part := item
32				part.RunStart = start
33				part.RunEnd = i
34				part.Direction = directionFor(levels[start])
35				out = append(out, part)
36				start = i
37			}
38		}
39		item.RunStart = start
40		item.Direction = directionFor(levels[start])
41		out = append(out, item)
42	}
43	return out
44}
45func directionFor(level uint8) di.Direction {
46	if level&1 != 0 {
47		return di.DirectionRTL
48	}
49	return di.DirectionLTR
50}
51
52type runItem struct {
53	output shaping.Output
54	level  uint8
55	pos    int
56}
57
58// visualRuns resets trailing whitespace to the paragraph level (L1) by
59// resolving levels against the *actual wrapped line breaks*, then applies L2
60// to complete runs. The shaper already emits glyphs in visual order within an
61// odd-level run: never reverse its glyph slice a second time.
62func visualRuns(text []rune, line shaping.Line, base bidi.BaseDirection) (shaping.Line, map[int]uint8, error) {
63	if len(line) == 0 {
64		return nil, nil, nil
65	}
66	start, end := line[0].Runes.Offset, line[len(line)-1].Runes.Offset+line[len(line)-1].Runes.Count
67	if start < 0 || end > len(text) || end <= start {
68		return nil, nil, fmt.Errorf("invalid line range %d:%d", start, end)
69	}
70	// Include earlier lines so weak and neutral resolution use paragraph context.
71	breaks := []int{}
72	if start > 0 {
73		breaks = append(breaks, start)
74	}
75	breaks = append(breaks, end)
76	if end < len(text) {
77		breaks = append(breaks, len(text))
78	}
79	levels, _, err := bidi.Resolve(text, base, breaks)
80	if err != nil {
81		return nil, nil, err
82	}
83	// The wrapper's runs are in logical order. A level change within one output
84	// requires reshaping at that boundary; splitLevels does this before wrapping.
85	runs := make([]runItem, 0, len(line))
86	for _, o := range line {
87		s, e := o.Runes.Offset, o.Runes.Offset+o.Runes.Count
88		if s < start || e > end || s >= e {
89			return nil, nil, fmt.Errorf("invalid shaped run %d:%d", s, e)
90		}
91		// L1 may reset a trailing space in a shaped RTL run. Split the output at
92		// cluster boundaries, preserving its glyph metrics and HarfBuzz order.
93		parts, err := splitWrappedLevel(o, levels)
94		if err != nil {
95			return nil, nil, err
96		}
97		for _, part := range parts {
98			runs = append(runs, runItem{output: part, level: levels[part.Runes.Offset], pos: part.Runes.Offset})
99		}
100	}
101	// L2 on run levels: from maximum down to the minimum odd level, reverse
102	// contiguous sequences whose levels are >= the current level.
103	highest, lowestOdd := uint8(0), uint8(255)
104	for _, run := range runs {
105		if run.level > highest {
106			highest = run.level
107		}
108		if run.level&1 == 1 && run.level < lowestOdd {
109			lowestOdd = run.level
110		}
111	}
112	if lowestOdd != 255 {
113		for current := int(highest); current >= int(lowestOdd); current-- {
114			for i := 0; i < len(runs); {
115				if int(runs[i].level) < current {
116					i++
117					continue
118				}
119				j := i + 1
120				for j < len(runs) && int(runs[j].level) >= current {
121					j++
122				}
123				for a, b := i, j-1; a < b; a, b = a+1, b-1 {
124					runs[a], runs[b] = runs[b], runs[a]
125				}
126				i = j
127			}
128		}
129	}
130	out := make(shaping.Line, 0, len(runs))
131	lookup := make(map[int]uint8, len(runs))
132	for _, run := range runs {
133		out = append(out, run.output)
134		lookup[run.pos] = run.level
135	}
136	return out, lookup, nil
137}
138
139// splitWrappedLevel divides only at glyph cluster boundaries; UAX#14 wraps
140// at these same boundaries. Empty/format glyphs stay attached to their cluster.
141func splitWrappedLevel(o shaping.Output, levels []uint8) ([]shaping.Output, error) {
142	start, end := o.Runes.Offset, o.Runes.Offset+o.Runes.Count
143	if start < 0 || end > len(levels) || start >= end {
144		return nil, fmt.Errorf("invalid output range")
145	}
146	same := true
147	for i := start + 1; i < end; i++ {
148		if levels[i] != levels[start] {
149			same = false
150			break
151		}
152	}
153	if same {
154		return []shaping.Output{o}, nil
155	}
156	// Group glyphs by lowest text cluster index, then restore HB visual order.
157	clusters := map[int][]shaping.Glyph{}
158	var keys []int
159	for _, g := range o.Glyphs {
160		idx := g.TextIndex()
161		if idx < start || idx >= end {
162			return nil, fmt.Errorf("glyph cluster outside run")
163		}
164		if _, ok := clusters[idx]; !ok {
165			keys = append(keys, idx)
166		}
167		clusters[idx] = append(clusters[idx], g)
168	}
169	sort.Ints(keys)
170	if len(keys) == 0 {
171		return []shaping.Output{o}, nil
172	}
173	var parts []shaping.Output
174	for i, key := range keys {
175		limit := end
176		if i+1 < len(keys) {
177			limit = keys[i+1]
178		}
179		if levels[key] != levels[limit-1] {
180			return nil, fmt.Errorf("bidi level changes within glyph cluster %d", key)
181		}
182		part := o
183		part.Runes = shaping.Range{Offset: key, Count: limit - key}
184		part.Glyphs = append([]shaping.Glyph(nil), clusters[key]...)
185		part.Direction = directionFor(levels[key])
186		part.RecomputeAdvance()
187		parts = append(parts, part)
188	}
189	// Wrapper may have absorbed leading non-glyph format runes.
190	if parts[0].Runes.Offset > start {
191		parts[0].Runes.Count += parts[0].Runes.Offset - start
192		parts[0].Runes.Offset = start
193	}
194	// Recombine adjacent same-level parts without changing glyph order.
195	var merged []shaping.Output
196	for _, part := range parts {
197		if n := len(merged); n > 0 && levels[merged[n-1].Runes.Offset] == levels[part.Runes.Offset] {
198			prev := &merged[n-1]
199			prev.Runes.Count = part.Runes.Offset + part.Runes.Count - prev.Runes.Offset
200			if o.Direction == di.DirectionRTL {
201				prev.Glyphs = append(part.Glyphs, prev.Glyphs...)
202			} else {
203				prev.Glyphs = append(prev.Glyphs, part.Glyphs...)
204			}
205			prev.RecomputeAdvance()
206			continue
207		}
208		merged = append(merged, part)
209	}
210	return merged, nil
211}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 12

↑↓ moveenter openesc close