v0.7.2 71ebe61

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

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