v0.5.1 657f658

levels.go RawHistoryFolder

1package bidi
2
3import "fmt"
4
5// BaseDirection selects the paragraph embedding level. Auto follows P2/P3.
6// nefergui: expose the existing upstream paragraph algorithm's resolved levels.
7type BaseDirection uint8
8
9const (
10	Auto BaseDirection = iota
11	LTR
12	RTL
13)
14
15// Levels returns resolved embedding levels after L1, one per input rune.
16// lineBreaks contains strictly increasing, nonempty, rune offsets, ending in len(text).
17// Removed X9 formatting codes receive a convenience level; call Reorder with
18// the same text to omit those codes from the visual indices.
19// nefergui: adapt upstream newParagraph/getLevels for per-line layout consumers.
20func Levels(text []rune, base BaseDirection, lineBreaks []int) ([]uint8, error) {
21	levels, _, err := Resolve(text, base, lineBreaks)
22	return levels, err
23}
24
25// Resolve returns the paragraph base level and the resolved levels after L1.
26// nefergui: surface existing levels without copying or reclassifying UAX#9 classes.
27func Resolve(text []rune, base BaseDirection, lineBreaks []int) ([]uint8, uint8, error) {
28	if base > RTL {
29		return nil, 0, fmt.Errorf("invalid bidi base %d", base)
30	}
31	if len(text) == 0 {
32		if len(lineBreaks) != 0 {
33			return nil, 0, fmt.Errorf("breaks on empty paragraph")
34		}
35		if base == RTL {
36			return nil, 1, nil
37		}
38		return nil, 0, nil
39	}
40	if len(lineBreaks) == 0 || lineBreaks[len(lineBreaks)-1] != len(text) {
41		return nil, 0, fmt.Errorf("line breaks must end at paragraph length")
42	}
43	prev := 0
44	for _, end := range lineBreaks {
45		if end <= prev || end > len(text) {
46			return nil, 0, fmt.Errorf("invalid line break %d", end)
47		}
48		prev = end
49	}
50	if base != RTL && leftToRightOnly(text) {
51		return make([]uint8, len(text)), 0, nil
52	}
53	return resolveFull(text, base, lineBreaks)
54}
55
56// resolveFull runs the complete UAX#9 algorithm on validated input.
57func resolveFull(text []rune, base BaseDirection, lineBreaks []int) ([]uint8, uint8, error) {
58	classes := make([]Class, len(text))
59	pairTypes := make([]bracketType, len(text))
60	pairValues := make([]rune, len(text))
61	for i, r := range text {
62		prop, _ := LookupRune(r)
63		classes[i] = prop.Class()
64		switch {
65		case prop.IsOpeningBracket():
66			pairTypes[i] = bpOpen
67			pairValues[i] = r
68			// nefergui: U+2329/232A are canonically equivalent to U+3008/3009.
69			if r == 0x2329 {
70				pairValues[i] = 0x3008
71			}
72		case prop.IsBracket():
73			pairTypes[i] = bpClose
74			// nefergui: canonicalize close brackets to the opening identifier
75			// required by upstream bracketPairer.matchOpener.
76			pairValues[i] = prop.reverseBracket(r)
77			if r == 0x232a {
78				pairValues[i] = 0x3008
79			}
80		}
81	}
82	embedding := implicitLevel
83	if base == LTR {
84		embedding = 0
85	} else if base == RTL {
86		embedding = 1
87	}
88	p, err := newParagraph(classes, pairTypes, pairValues, embedding)
89	if err != nil {
90		return nil, 0, err
91	}
92	// nefergui: exercise the upstream L2 entry point as a differential oracle;
93	// levels below are the same L1 result used by that entry point.
94	_ = p.getReordering(lineBreaks)
95	raw := p.getLevels(lineBreaks)
96	out := make([]uint8, len(raw))
97	for i, l := range raw {
98		out[i] = uint8(l)
99	}
100	return out, uint8(p.embeddingLevel), nil
101}
102
103// leftToRightOnly reports whether every rune resolves to level 0 in a level-0
104// paragraph: with no R, AL, AN, explicit formatting or BN, P2/P3 pick level 0,
105// W7 turns EN into L (sos is L), neutrals resolve to L and I1 raises nothing.
106// B is left to the full algorithm, which validates its position.
107func leftToRightOnly(text []rune) bool {
108	for _, r := range text {
109		if r < 0x80 && r >= 0x20 && r != 0x7f {
110			continue // printable ASCII: L, EN, ES, ET, CS, WS or ON
111		}
112		prop, _ := LookupRune(r)
113		switch prop.Class() {
114		case R, AL, AN, B, BN, LRO, RLO, LRE, RLE, PDF, LRI, RLI, FSI, PDI:
115			return false
116		}
117	}
118	return true
119}
120
121// Reorder returns a visual-to-logical rune map per line using L2; X9-removed
122// formatting characters are excluded from the result. Levels must be the
123// per-line L1 output of Resolve. Consumers may group neighboring indices into
124// shaped runs while preserving the shaper's glyph order within each run.
125// nefergui: reuse upstream multiline L2 rather than infer levels from parity.
126func Reorder(text []rune, levels []uint8, breaks []int) ([][]int, error) {
127	if len(text) != len(levels) {
128		return nil, fmt.Errorf("level count mismatch")
129	}
130	if len(text) == 0 {
131		return nil, nil
132	}
133	if len(breaks) == 0 || breaks[len(breaks)-1] != len(text) {
134		return nil, fmt.Errorf("invalid breaks")
135	}
136	var out [][]int
137	start := 0
138	for _, end := range breaks {
139		if end <= start || end > len(text) {
140			return nil, fmt.Errorf("invalid break %d", end)
141		}
142		lv := make([]level, end-start)
143		for i, v := range levels[start:end] {
144			lv[i] = level(v)
145		}
146		// nefergui: call upstream's multiline L2 helper to keep its code live.
147		order := computeMultilineReordering(lv, []int{len(lv)})
148		line := make([]int, 0, len(order))
149		for _, i := range order {
150			r := text[start+i]
151			cls, _ := LookupRune(r)
152			if !isRemovedByX9(cls.Class()) {
153				line = append(line, start+i)
154			}
155		}
156		out = append(out, line)
157		start = end
158	}
159	return out, nil
160}

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