v0.31.0 bf0756b

schema_provider_test.go RawHistoryFolder

1package config
2
3import (
4	"os"
5	"path/filepath"
6	"regexp"
7	"runtime"
8	"sort"
9	"strings"
10	"testing"
11
12	"github.com/spf13/viper"
13	"github.com/stretchr/testify/require"
14)
15
16func TestConfigurationReferenceCoversSchemaKeys(t *testing.T) {
17	docKeys := configurationReferenceKeys(t)
18
19	var missing []string
20	for _, key := range NewSchemaProvider().GetSchema() {
21		if configurationReferenceCoversKey(docKeys, key.Key) {
22			continue
23		}
24		missing = append(missing, key.Key)
25	}
26
27	sort.Strings(missing)
28	require.Empty(t, missing, "docs/reference/configuration.md should cover every SchemaProvider key")
29}
30
31func configurationReferenceKeys(t *testing.T) map[string]struct{} {
32	t.Helper()
33
34	_, filename, _, ok := runtime.Caller(0)
35	require.True(t, ok, "locate current test file")
36	docPath := filepath.Join(filepath.Dir(filename), "..", "..", "..", "docs", "reference", "configuration.md")
37	data, err := os.ReadFile(docPath)
38	require.NoError(t, err)
39
40	// Keep this split point aligned with the markdown heading so legacy-key rows
41	// stay out of the canonical schema key coverage set.
42	canonicalReference, _, _ := strings.Cut(string(data), "Legacy key migration:")
43	rowRE := regexp.MustCompile(`(?m)^\| ` + "`" + `([^` + "`" + `]+)` + "`" + ` \|`)
44	keys := make(map[string]struct{})
45	for _, match := range rowRE.FindAllStringSubmatch(canonicalReference, -1) {
46		keys[match[1]] = struct{}{}
47	}
48	return keys
49}
50
51func configurationReferenceCoversKey(docKeys map[string]struct{}, schemaKey string) bool {
52	if _, ok := docKeys[schemaKey]; ok {
53		return true
54	}
55
56	schemaSegments := strings.Split(schemaKey, ".")
57	for docKey := range docKeys {
58		if configurationReferencePatternCoversKey(strings.Split(docKey, "."), schemaSegments) {
59			return true
60		}
61	}
62
63	return false
64}
65
66func configurationReferencePatternCoversKey(docSegments, schemaSegments []string) bool {
67	for idx, schemaSegment := range schemaSegments {
68		if schemaSegment == "*" {
69			return idx == len(schemaSegments)-1 && len(docSegments) == idx+1
70		}
71		if idx >= len(docSegments) {
72			return false
73		}
74		if isPlaceholderSegment(schemaSegment) && isPlaceholderSegment(docSegments[idx]) {
75			continue
76		}
77		if docSegments[idx] != schemaSegment {
78			return false
79		}
80	}
81
82	if len(docSegments) == len(schemaSegments) {
83		return true
84	}
85	return configurationReferenceAllowsObjectFields(strings.Join(schemaSegments, ".")) &&
86		len(docSegments) > len(schemaSegments)
87}
88
89func isPlaceholderSegment(segment string) bool {
90	return strings.HasPrefix(segment, "<") && strings.HasSuffix(segment, ">")
91}
92
93func configurationReferenceAllowsObjectFields(schemaKey string) bool {
94	switch schemaKey {
95	case "search_shortcuts.<key>", "workspace.floating_pane.profiles.<name>":
96		return true
97	default:
98		return false
99	}
100}
101
102func TestConfigurationReferenceCoversKeyRequiresPlaceholderSegmentMatches(t *testing.T) {
103	docKeys := map[string]struct{}{
104		"workspace.floating_pane.profiles.<profile>.keys_extra": {},
105	}
106
107	require.False(t, configurationReferenceCoversKey(
108		docKeys,
109		"workspace.floating_pane.profiles.<name>.keys",
110	))
111}
112
113func TestConfigurationReferenceCoversKeyAllowsDocumentedPlaceholderSegments(t *testing.T) {
114	docKeys := map[string]struct{}{
115		"workspace.floating_pane.profiles.<profile>.keys": {},
116	}
117
118	require.True(t, configurationReferenceCoversKey(
119		docKeys,
120		"workspace.floating_pane.profiles.<name>.keys",
121	))
122}
123
124func TestConfigurationReferenceCoversKeyAllowsDocumentedObjectFields(t *testing.T) {
125	docKeys := map[string]struct{}{
126		"search_shortcuts.<name>.url": {},
127	}
128
129	require.True(t, configurationReferenceCoversKey(docKeys, "search_shortcuts.<key>"))
130}
131
132func TestSchemaProviderCoversViperDefaults(t *testing.T) {
133	m := &Manager{viper: viper.New()}
134	m.setDefaults()
135
136	schemaKeys := make(map[string]struct{})
137	for _, key := range NewSchemaProvider().GetSchema() {
138		require.NotEmpty(t, key.Key, "schema key should not be empty")
139		require.NotContains(t, schemaKeys, key.Key, "duplicate schema key %s", key.Key)
140		schemaKeys[key.Key] = struct{}{}
141	}
142
143	defaultsCoveredByAggregateSchemaKeys := map[string]string{
144		"appearance.light_palette":         "appearance.light_palette.*",
145		"appearance.dark_palette":          "appearance.dark_palette.*",
146		"search_shortcuts":                 "search_shortcuts.<key>",
147		"workspace.pane_mode.actions":      "workspace.pane_mode.actions.<action>",
148		"workspace.tab_mode.actions":       "workspace.tab_mode.actions.<action>",
149		"workspace.vim_mode.actions":       "workspace.vim_mode.actions.<action>",
150		"workspace.resize_mode.actions":    "workspace.resize_mode.actions.<action>",
151		"workspace.floating_pane.profiles": "workspace.floating_pane.profiles.<name>",
152		"session.session_mode.actions":     "session.session_mode.actions.<action>",
153	}
154
155	var missing []string
156	for _, key := range m.viper.AllKeys() {
157		if _, ok := schemaKeys[key]; ok {
158			continue
159		}
160		if schemaKey, ok := defaultsCoveredByAggregateSchemaKeys[key]; ok {
161			if _, hasAggregate := schemaKeys[schemaKey]; hasAggregate {
162				continue
163			}
164		}
165		missing = append(missing, key)
166	}
167
168	sort.Strings(missing)
169	require.Empty(t, missing, "every Viper default should have SchemaProvider metadata or an explicit aggregate schema key")
170}

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 8

Tags 42

↑↓ moveenter openesc close