Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion infrastructure/storage/json_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type AppData struct {
RecentCharts []domain.RecentChart `json:"recentCharts"`
RecentValues []domain.RecentValuesFile `json:"recentValues"`
RecentValuesEntries []domain.RecentValuesEntry `json:"recentValuesEntries,omitempty"`
ShowComments *bool `json:"showComments,omitempty"`
ShowDocs *bool `json:"showDocs,omitempty"`
ChartUIStates map[string]domain.ChartUIState `json:"chartUiStates,omitempty"`
}

Expand Down
18 changes: 9 additions & 9 deletions service/recent_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,29 +115,29 @@ func (s *RecentService) RemoveRecentValues(ctx context.Context, idx int) error {
)
}

// LoadShowComments returns the persisted "show comments" preference.
// LoadShowDocs returns the persisted "show docs" preference.
// Returns false when the preference has never been saved.
func (s *RecentService) LoadShowComments(ctx context.Context) (bool, error) {
func (s *RecentService) LoadShowDocs(ctx context.Context) (bool, error) {
data, err := s.store.Load(ctx)
if err != nil {
return false, fmt.Errorf("load show comments: %w", err)
return false, fmt.Errorf("load show docs: %w", err)
}

if data.ShowComments == nil {
if data.ShowDocs == nil {
return false, nil
}

return *data.ShowComments, nil
return *data.ShowDocs, nil
}

// SaveShowComments persists the "show comments" preference.
func (s *RecentService) SaveShowComments(ctx context.Context, show bool) error {
// SaveShowDocs persists the "show docs" preference.
func (s *RecentService) SaveShowDocs(ctx context.Context, show bool) error {
if err := s.store.Update(ctx, func(data *storage.AppData) error {
data.ShowComments = &show
data.ShowDocs = &show

return nil
}); err != nil {
return fmt.Errorf("save show comments: %w", err)
return fmt.Errorf("save show docs: %w", err)
}

return nil
Expand Down
67 changes: 45 additions & 22 deletions service/values_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ const (
typeUnknown = "unknown"
typeMap = "map"
typeList = "list"

// emptyMapValue / emptyListValue are the placeholder Values flattenValues
// emits for empty containers, distinguishing them from populated section
// headers (Value=""). IsEmptyContainer uses these to detect a chart `{}` /
// `[]` that an overlay may later populate.
emptyMapValue = "{}"
emptyListValue = "[]"
)

type stackItem struct {
Expand Down Expand Up @@ -509,11 +516,12 @@ func newFlatValues(vf *domain.ValuesFile) *FlatValues {
entries := make([]FlatValueEntry, len(vf.Entries))
for i, e := range vf.Entries {
entries[i] = FlatValueEntry{
Key: string(e.Key),
Value: e.Value,
Type: e.Type,
Depth: e.Key.Depth(),
Comment: e.Comment,
Key: string(e.Key),
Value: e.Value,
Type: e.Type,
Depth: e.Key.Depth(),
Comment: e.Comment,
DefaultComment: e.Comment,
}
}

Expand Down Expand Up @@ -633,11 +641,15 @@ func buildFlatKeyPositions(root *yaml.Node) map[string]int {
}

// SortByFilePositions reorders entries to match the source file's DFS layout.
// Entries whose flat key has no recorded position (custom-only keys not in
// the chart-defaults file, or keys merged in via `<<: *base`) sort to the
// end, ordered alphabetically among themselves so the result stays
// deterministic. Stable sort preserves relative order for ties — important
// when the same position would otherwise produce a flap between frames.
// When an entry's flat key has no recorded position — overlay-only keys, or
// keys merged in via `<<: *base` that buildFlatKeyPositions never visited —
// the lookup walks up the parent chain so the entry sorts adjacent to its
// nearest positioned ancestor. Without that walk, an overlay-added child
// would land in the alphabetic tail far from its section header. Entries
// whose entire ancestor chain is unknown still fall back to "end of file"
// and order alphabetically among themselves. Stable sort preserves relative
// order for ties — important when the same position would otherwise produce
// a flap between frames.
//
// Empty positions or empty entries are no-ops; the caller can pass nil
// without checking.
Expand All @@ -649,16 +661,8 @@ func SortByFilePositions(entries []FlatValueEntry, positions map[string]int) {
end := len(positions)

slices.SortStableFunc(entries, func(a, b FlatValueEntry) int {
pa, hasA := positions[a.Key]
pb, hasB := positions[b.Key]

if !hasA {
pa = end
}

if !hasB {
pb = end
}
pa := effectivePosition(a.Key, positions, end)
pb := effectivePosition(b.Key, positions, end)

if pa != pb {
return pa - pb
Expand All @@ -668,6 +672,25 @@ func SortByFilePositions(entries []FlatValueEntry, positions map[string]int) {
})
}

// effectivePosition returns key's recorded position, falling back to the
// nearest positioned ancestor (parent, grandparent, …). Returns end when
// neither key nor any ancestor has a position. Used by SortByFilePositions
// so overlay-added descendants of a known section sort beside their parent
// rather than getting flushed to the alphabetic tail.
func effectivePosition(key string, positions map[string]int, end int) int {
if p, ok := positions[key]; ok {
return p
}

for parent := domain.FlatKey(key).Parent(); parent != ""; parent = parent.Parent() {
if p, ok := positions[string(parent)]; ok {
return p
}
}

return end
}

// flattenValues converts a nested map[string]any to a sorted flat list.
// Uses a stack-based iterative approach. All sorting uses slices.SortFunc (pdqsort).
func flattenValues(source string, vals map[string]any) *domain.ValuesFile {
Expand All @@ -686,7 +709,7 @@ func flattenValues(source string, vals map[string]any) *domain.ValuesFile {
case map[string]any:
if len(typedVal) == 0 {
vf.Entries = append(vf.Entries, domain.ValuesEntry{
Key: domain.FlatKey(item.prefix), Value: "{}", Type: typeMap,
Key: domain.FlatKey(item.prefix), Value: emptyMapValue, Type: typeMap,
})

continue
Expand Down Expand Up @@ -715,7 +738,7 @@ func flattenValues(source string, vals map[string]any) *domain.ValuesFile {
case []any:
if len(typedVal) == 0 {
vf.Entries = append(vf.Entries, domain.ValuesEntry{
Key: domain.FlatKey(item.prefix), Value: "[]", Type: typeList,
Key: domain.FlatKey(item.prefix), Value: emptyListValue, Type: typeList,
})

continue
Expand Down
18 changes: 18 additions & 0 deletions service/values_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ type FlatValueEntry struct {
Comment string
Kind EntryKind

// DefaultComment is the chart-side head/line comment authored above this
// key in the chart's default values.yaml. Survives RebuildUnifiedEntries
// (which rewrites Comment to reflect the user's overlay file) so the left
// panel can render chart documentation independently from the right
// panel's user annotations. Empty when the chart didn't document the key,
// and always empty for IsCustomOnly entries (no chart counterpart).
DefaultComment string

// FootAfterKey is set only for EntryKindComment rows. It holds the flat key
// of the leaf whose value the comment block sits *after* in the source
// file. The serializer uses this to write the text back as that leaf's
Expand Down Expand Up @@ -117,6 +125,16 @@ func (e FlatValueEntry) IsFocusable() bool {
return !e.IsSection() && !e.IsComment()
}

// IsEmptyContainer reports whether this entry is a container key with no
// children — flattenValues emits "{}" for an empty mapping and "[]" for an
// empty sequence, distinguishable from a populated-container section header
// (which has Value=""). Used by the unified-entries merge to detect a shape
// mismatch when an overlay populates a key that the chart left empty.
func (e FlatValueEntry) IsEmptyContainer() bool {
return (e.Type == typeMap && e.Value == emptyMapValue) ||
(e.Type == typeList && e.Value == emptyListValue)
}

type DiffResult struct {
Lines []DiffLine
Stats DiffStats
Expand Down
10 changes: 5 additions & 5 deletions ui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ func NewApplication(
OnSelectVersion: a.onSelectVersion,
OnSaveChart: a.valuesCtrl.OnSaveChartVersion,
}
a.loadShowComments()
a.loadShowDocs()

a.valuesPage = page.NewValuesPage(th, &a.valuesState, a.valuesCtrl.Callbacks())

Expand Down Expand Up @@ -760,15 +760,15 @@ func (a *Application) preloadCharts(repos []domain.HelmRepository) {
}()
}

func (a *Application) loadShowComments() {
show, err := a.recentService.LoadShowComments(context.Background())
func (a *Application) loadShowDocs() {
show, err := a.recentService.LoadShowDocs(context.Background())
if err != nil {
slog.Error("load show comments preference", "error", err)
slog.Error("load show docs preference", "error", err)

return
}

a.valuesState.ShowComments.Value = show
a.valuesState.ShowDocs.Value = show
}

func (a *Application) loadRecentCharts() {
Expand Down
2 changes: 1 addition & 1 deletion ui/page/values_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ func (vc *ValuesController) Callbacks() ValuesPageCallbacks {
OnRenderDefaults: vc.onRenderDefaults,
OnRenderOverrides: vc.onRenderOverrides,
OnKeyCopied: vc.onKeyCopied,
OnShowCommentsChanged: vc.onShowCommentsChanged,
OnShowDocsChanged: vc.onShowDocsChanged,
OnCellFocusChanged: vc.onCellFocusChanged,
OnCollapseChanged: vc.onCollapseChanged,
OnAnchorCreate: vc.onAnchorCreate,
Expand Down
6 changes: 3 additions & 3 deletions ui/page/values_controller_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -493,10 +493,10 @@ func (vc *ValuesController) OnSaveChartVersion(chartName, version string) {
})
}

func (vc *ValuesController) onShowCommentsChanged(show bool) {
func (vc *ValuesController) onShowDocsChanged(show bool) {
go func() {
if err := vc.RecentService.SaveShowComments(context.Background(), show); err != nil {
slog.Error("save show comments preference", "error", err)
if err := vc.RecentService.SaveShowDocs(context.Background(), show); err != nil {
slog.Error("save show docs preference", "error", err)
}
}()
}
Expand Down
10 changes: 5 additions & 5 deletions ui/page/values_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ const (
stickyParentStripPadV unit.Dp = 4
stickyDiagGap unit.Dp = 14 // gap between override / extras / encoding chips

recentItemPadV unit.Dp = 2
showCommentsSize unit.Dp = 18
showCommentsTextMult float32 = 0.85
recentItemPadV unit.Dp = 2
showDocsSize unit.Dp = 18
showDocsTextMult float32 = 0.85

maxRecentValues = 10 // must match service/recent_service.go

Expand Down Expand Up @@ -132,7 +132,7 @@ type ValuesPageCallbacks struct {
OnRenderDefaults func()
OnRenderOverrides func()
OnKeyCopied func(key string)
OnShowCommentsChanged func(show bool)
OnShowDocsChanged func(show bool)
// OnCellFocusChanged fires when (FocusedRow, FocusedCol) actually change.
// entryKey is the flat key of the focused entry (stable across sessions
// and filter changes); empty when no entry is resolvable at row.
Expand Down Expand Up @@ -310,7 +310,7 @@ func (p *ValuesPage) Layout(gtx layout.Context) layout.Dimensions {
// Wire the table editors from column state.
p.Table.DefaultValueEditors = p.State.DefaultValueEditors
p.Table.ColumnCount = p.State.ColumnCount
p.Table.ShowComments = p.State.ShowComments.Value
p.Table.ShowDocs = p.State.ShowDocs.Value

if p.State.DefaultValues != nil {
p.Table.DefaultAnchors = p.State.DefaultValues.Anchors
Expand Down
12 changes: 6 additions & 6 deletions ui/page/values_page_render.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ func (p *ValuesPage) layoutRenderButtons(gtx layout.Context) layout.Dimensions {
p.OnRenderOverrides()
}

if p.State.ShowComments.Update(gtx) && p.OnShowCommentsChanged != nil {
p.OnShowCommentsChanged(p.State.ShowComments.Value)
if p.State.ShowDocs.Update(gtx) && p.OnShowDocsChanged != nil {
p.OnShowDocsChanged(p.State.ShowDocs.Value)
}

if p.State.SaveChartButton.Clicked(gtx) && p.OnSaveChart != nil {
Expand Down Expand Up @@ -93,13 +93,13 @@ func (p *ValuesPage) layoutRenderButtons(gtx layout.Context) layout.Dimensions {

children[n] = layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Inset{Left: valuesSpacing}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
cb := material.CheckBox(p.Theme, &p.State.ShowComments, "Show comments")
cb.Size = showCommentsSize
cb.TextSize = unit.Sp(float32(p.Theme.TextSize) * showCommentsTextMult)
cb := material.CheckBox(p.Theme, &p.State.ShowDocs, "Show docs")
cb.Size = showDocsSize
cb.TextSize = unit.Sp(float32(p.Theme.TextSize) * showDocsTextMult)

dims := cb.Layout(gtx)

pushPointerCursor(gtx, dims, &p.State.ShowComments)
pushPointerCursor(gtx, dims, &p.State.ShowDocs)

return dims
})
Expand Down
Loading
Loading