diff --git a/internal/ui/coverage_helpers_test.go b/internal/ui/coverage_helpers_test.go index e08dfee..2fcac22 100644 --- a/internal/ui/coverage_helpers_test.go +++ b/internal/ui/coverage_helpers_test.go @@ -268,9 +268,9 @@ func TestERDHelpersCoverage(t *testing.T) { _ = renderERDList(g, 80, true) _ = renderERDList(types.ERDGraph{}, 40, true) _ = renderERDList(g, 80, false) - _ = renderERDDiagram(g, 120) - _ = renderERDDiagram(g, 20) - _ = renderERDDiagram(types.ERDGraph{Tables: []types.ERDTable{{Name: "solo", Columns: []string{"id"}}}}, 80) + _ = renderERDDiagram(g, 120, "") + _ = renderERDDiagram(g, 20, "") + _ = renderERDDiagram(types.ERDGraph{Tables: []types.ERDTable{{Name: "solo", Columns: []string{"id"}}}}, 80, "") // Isolates + multi-child edges for wrap/bus/partition paths wide := types.ERDGraph{ Tables: []types.ERDTable{ @@ -291,7 +291,7 @@ func TestERDHelpersCoverage(t *testing.T) { {FromTable: "c5", FromCols: []string{"hub_id"}, ToTable: "hub", ToCols: []string{"id"}}, }, } - _ = renderERDDiagram(wide, 100) + _ = renderERDDiagram(wide, 100, "") linked, isolates := erdPartition(wide) if len(isolates) < 2 || len(linked.Tables) < 2 { t.Fatalf("partition linked=%d isolates=%d", len(linked.Tables), len(isolates)) @@ -307,12 +307,12 @@ func TestERDHelpersCoverage(t *testing.T) { // Only isolates (no FKs) → diagram shows unconnected section _ = renderERDDiagram(types.ERDGraph{ Tables: []types.ERDTable{{Name: "x", Columns: []string{"id"}}, {Name: "y", Columns: []string{"n"}}}, - }, 80) + }, 80, "") // Linked tables but empty edges after self-FK filter _ = renderERDDiagram(types.ERDGraph{ Tables: []types.ERDTable{{Name: "t", Columns: []string{"id", "t_id", "a", "b", "c", "d", "e"}}}, Edges: []types.FKEdge{{FromTable: "t", FromCols: []string{"t_id"}, ToTable: "t", ToCols: []string{"id"}}}, - }, 80) + }, 80, "") // Reverse-layer edge skipped (child.layer <= parent.layer) _ = layoutERDCanvas(types.ERDGraph{ Tables: []types.ERDTable{{Name: "p", Columns: []string{"id"}}, {Name: "c", Columns: []string{"id", "p_id"}}}, @@ -321,7 +321,7 @@ func TestERDHelpersCoverage(t *testing.T) { {FromTable: "p", FromCols: []string{"id"}, ToTable: "c", ToCols: []string{"id"}}, // reverse {FromTable: "missing_from", FromCols: []string{"x"}, ToTable: "p", ToCols: []string{"id"}}, }, - }, [][]string{{"p"}, {"c"}}, map[string]bool{"c.p_id": true, "p.id": true}, 80) + }, [][]string{{"p"}, {"c"}}, map[string]bool{"c.p_id": true, "p.id": true}, 80, "") _ = erdWrapLayers([][]string{{"a", "b", "c", "d", "e", "f"}}, 4) _ = erdWrapLayers([][]string{{"a", "b"}}, 4) _ = erdWrapLayers(nil, 0) @@ -365,8 +365,8 @@ func TestERDHelpersCoverage(t *testing.T) { _ = mergeWireBox('┌', 'x') n1 := erdNode{name: "a", x: 2, y: 2, w: 12, h: 5, table: types.ERDTable{Name: "a", Columns: []string{"id", "email"}}} n2 := erdNode{name: "b", x: 20, y: 2, w: 12, h: 5, table: types.ERDTable{Name: "b", Columns: []string{"id", "a_id"}}} - c.drawBox(n1, map[string]bool{"a.id": true}) - c.drawBox(n2, map[string]bool{"b.a_id": true}) + c.drawBox(n1, map[string]bool{"a.id": true}, false) + c.drawBox(n2, map[string]bool{"b.a_id": true}, false) c.routeEdge(n1, n2, "fk", 0) n3 := erdNode{name: "c", x: 2, y: 12, w: 12, h: 4, table: types.ERDTable{Name: "c", Columns: []string{"id"}}} c.routeEdge(n2, n3, "", 1) @@ -392,22 +392,80 @@ func TestERDHelpersCoverage(t *testing.T) { c2 := newERDCanvas(0, 0) _ = c2 c3 := newERDCanvas(5, 5) - c3.drawBox(erdNode{name: "t", x: 0, y: 0, w: 10, h: 10, table: types.ERDTable{Name: "t", Columns: []string{"a", "b", "c", "d", "e"}}}, nil) + c3.drawBox(erdNode{name: "t", x: 0, y: 0, w: 10, h: 10, table: types.ERDTable{Name: "t", Columns: []string{"a", "b", "c", "d", "e"}}}, nil, false) // many columns → ellipsis; FK accent; id plain c3.drawBox(erdNode{ name: "wide", x: 0, y: 0, w: 14, h: 12, table: types.ERDTable{Name: "wide", Columns: []string{"id", "a_id", "b_id", "c_id", "d_id", "note"}}, - }, map[string]bool{"wide.a_id": true, "wide.b_id": true}) + }, map[string]bool{"wide.a_id": true, "wide.b_id": true}, false) _ = c3.lines() // empty graph layout + missing table name → nCol==0 path - _ = layoutERDCanvas(types.ERDGraph{}, nil, nil, 40) - _ = layoutERDCanvas(types.ERDGraph{}, [][]string{{"ghost"}}, nil, 40) + _ = layoutERDCanvas(types.ERDGraph{}, nil, nil, 40, "") + _ = layoutERDCanvas(types.ERDGraph{}, [][]string{{"ghost"}}, nil, 40, "") // nCol > erdMaxCols in layout (many FK cols) _ = layoutERDCanvas(types.ERDGraph{ Tables: []types.ERDTable{{Name: "fat", Columns: []string{"id", "a_id", "b_id", "c_id", "d_id", "e_id"}}}, }, [][]string{{"fat"}}, map[string]bool{ "fat.a_id": true, "fat.b_id": true, "fat.c_id": true, "fat.d_id": true, "fat.e_id": true, - }, 40) + }, 40, "fat") + // Highlighted focus box + c4 := newERDCanvas(30, 12) + c4.drawBox(erdNode{name: "hub", x: 2, y: 2, w: 12, h: 6, table: types.ERDTable{Name: "hub", Columns: []string{"id"}}}, nil, true) + _ = c4.lines() + // Focus subgraph helpers + fg := types.ERDGraph{ + Schema: "public", + Tables: []types.ERDTable{ + {Name: "users", Columns: []string{"id"}}, + {Name: "orders", Columns: []string{"id", "user_id"}}, + {Name: "items", Columns: []string{"id"}}, + }, + Edges: []types.FKEdge{ + {FromTable: "orders", FromCols: []string{"user_id"}, ToTable: "users", ToCols: []string{"id"}}, + }, + } + sub := erdFocusSubgraph(fg, "users") + if len(sub.Tables) != 2 || len(sub.Edges) != 1 { + t.Fatalf("focus subgraph: %+v", sub) + } + _ = erdFocusSubgraph(fg, "") + _ = erdFocusSubgraph(fg, "missing") + _ = renderERDDiagram(sub, 80, "users") + + m := NewModel() + m.ERD = fg + m.Objects = []types.SchemaObject{ + {Name: "users", Kind: types.ObjectTable}, + {Name: "orders", Kind: types.ObjectTable}, + } + m.SelectedObjIdx = 0 + m.ERDFocusAll = false + g, focus := m.erdViewGraph() + if focus != "users" || len(g.Tables) != 2 { + t.Fatalf("view graph focus=%q tables=%d", focus, len(g.Tables)) + } + m.ERDFocusAll = true + g, focus = m.erdViewGraph() + if focus != "" || len(g.Tables) != 3 { + t.Fatalf("all mode focus=%q tables=%d", focus, len(g.Tables)) + } + m.ERDFocusAll = false + m.Objects = []types.SchemaObject{{Name: "nope", Kind: types.ObjectView}} + if m.erdFocusCandidate() != "" { + t.Fatal("view should not focus") + } + m.Objects = nil + if m.erdFocusCandidate() != "" { + t.Fatal("empty selection") + } + m.ERD = fg + m.Objects = []types.SchemaObject{{Name: "users", Kind: types.ObjectTable}} + m.SelectedObjIdx = 0 + m.ERDFocusAll = false + m.Width, m.Height = 120, 40 + _ = m.viewERDContent(100, 30) + m.ERDFocusAll = true + _ = m.viewERDContent(100, 30) } func TestModelHelpersRemainingBranches(t *testing.T) { diff --git a/internal/ui/coverage_keys_extra_test.go b/internal/ui/coverage_keys_extra_test.go index 6d22546..5411bc1 100644 --- a/internal/ui/coverage_keys_extra_test.go +++ b/internal/ui/coverage_keys_extra_test.go @@ -805,6 +805,28 @@ func TestExtra_KeysERD(t *testing.T) { if cmd == nil { t.Fatal("r openERD") } + // Focus / all toggles + m.Objects = []types.SchemaObject{{Name: "users", Kind: types.ObjectTable}} + m.SelectedObjIdx = 0 + m.ERD = types.ERDGraph{Tables: []types.ERDTable{{Name: "users"}}} + m.Focus = focusContent + m.Screen = types.ScreenERD + nm2, _ := m.keysERD("a") + m = nm2.(Model) + if !m.ERDFocusAll { + t.Fatal("a should show all") + } + nm2, _ = m.keysERD("f") + m = nm2.(Model) + if m.ERDFocusAll { + t.Fatal("f should focus selection") + } + m.Objects = nil + nm2, _ = m.keysERD("f") + m = nm2.(Model) + if m.StatusMsg == "" { + t.Fatal("expected focus hint status") + } _, _ = m.keysERD("z") } diff --git a/internal/ui/coverage_render_test.go b/internal/ui/coverage_render_test.go index bbad635..f741978 100644 --- a/internal/ui/coverage_render_test.go +++ b/internal/ui/coverage_render_test.go @@ -547,7 +547,7 @@ func TestCoverageRenderActivityERDServer(t *testing.T) { }, Edges: []types.FKEdge{{FromTable: "b", FromCols: []string{"a_id"}, ToTable: "a", ToCols: []string{"id"}}}, } - _ = renderERDDiagram(g, 100) + _ = renderERDDiagram(g, 100, "") _ = erdFindTable(g, "missing") _ = erdFindTable(g, "a") _ = orderERDColumns("a", []string{"x", "id", "z"}, map[string]bool{"a.z": true}, false) diff --git a/internal/ui/coverage_view_edge_test.go b/internal/ui/coverage_view_edge_test.go index eb44a2d..80a7254 100644 --- a/internal/ui/coverage_view_edge_test.go +++ b/internal/ui/coverage_view_edge_test.go @@ -512,8 +512,8 @@ func TestCoverageViewEdgeCases(t *testing.T) { // same x center → straight down childStraight := erdNode{x: 4, y: 8, w: 8, h: 3, name: "c", table: types.ERDTable{Name: "c", Columns: []string{"id"}}} c2 := newERDCanvas(20, 14) - c2.drawBox(parent, nil) - c2.drawBox(childStraight, nil) + c2.drawBox(parent, nil, false) + c2.drawBox(childStraight, nil, false) c2.routeEdge(parent, childStraight, "fk", 0) // early exit: child too close c2.routeEdge(parent, erdNode{x: 4, y: 3, w: 4, h: 2}, "early", 0) @@ -553,10 +553,10 @@ func TestCoverageViewEdgeCases(t *testing.T) { "z", 0) // renderERDDiagram: empty, isolated, empty cols, missing edge ends, reverse layer edge - _ = renderERDDiagram(types.ERDGraph{}, 80) + _ = renderERDDiagram(types.ERDGraph{}, 80, "") _ = renderERDDiagram(types.ERDGraph{ Tables: []types.ERDTable{{Name: "solo", Columns: nil}, {Name: "wide", Columns: []string{"id", "a", "b", "c", "d", "e", "f"}}}, - }, 80) + }, 80, "") // max tables → list mode via viewERDContent m.Screen = types.ScreenERD m.Loading = true @@ -602,8 +602,8 @@ func TestCoverageViewEdgeCases(t *testing.T) { {FromTable: "ghost", FromCols: []string{"id"}, ToTable: "users", ToCols: []string{"id"}}, }, } - _ = renderERDDiagram(g, 100) - _ = renderERDDiagram(g, 40) + _ = renderERDDiagram(g, 100, "") + _ = renderERDDiagram(g, 40, "") _ = renderERDList(g, 5, true) _ = renderERDList(types.ERDGraph{Tables: g.Tables}, 40, true) // no edges @@ -673,8 +673,8 @@ func TestCoverageViewEdgeCases(t *testing.T) { {FromTable: "b1", FromCols: []string{"a_id"}, ToTable: "a1", ToCols: []string{"id"}}, }, } - _ = renderERDDiagram(wideG, 30) - _ = renderERDDiagram(wideG, 120) + _ = renderERDDiagram(wideG, 30, "") + _ = renderERDDiagram(wideG, 120, "") m.ERD = g m.CurrentSchema = "" diff --git a/internal/ui/coverage_views_test.go b/internal/ui/coverage_views_test.go index a6fde70..dbeddd6 100644 --- a/internal/ui/coverage_views_test.go +++ b/internal/ui/coverage_views_test.go @@ -470,8 +470,8 @@ func TestViewBranchesRemaining(t *testing.T) { c.put(2, 2, '│', 1) parent := erdNode{x: 1, y: 1, w: 8, h: 4, name: "p", table: types.ERDTable{Name: "p", Columns: []string{"id"}}} child := erdNode{x: 1, y: 7, w: 8, h: 3, name: "c", table: types.ERDTable{Name: "c", Columns: []string{"id"}}} - c.drawBox(parent, nil) - c.drawBox(child, map[string]bool{"c.id": true}) + c.drawBox(parent, nil, false) + c.drawBox(child, map[string]bool{"c.id": true}, false) c.routeEdge(parent, child, "fk", 0) c.routeEdge(parent, erdNode{x: 1, y: 2, w: 4, h: 2}, "early", 0) _ = c.lines() diff --git a/internal/ui/final_gaps_test.go b/internal/ui/final_gaps_test.go index 7247495..a8419f7 100644 --- a/internal/ui/final_gaps_test.go +++ b/internal/ui/final_gaps_test.go @@ -207,7 +207,7 @@ func TestFinalGaps_PaintAndEditor(t *testing.T) { func TestFinalGaps_ERDComplex(t *testing.T) { // empty tables - _ = renderERDDiagram(types.ERDGraph{}, 80) + _ = renderERDDiagram(types.ERDGraph{}, 80, "") // tables with empty columns g := types.ERDGraph{ Tables: []types.ERDTable{ @@ -225,8 +225,8 @@ func TestFinalGaps_ERDComplex(t *testing.T) { {FromTable: "c", FromCols: []string{"id"}, ToTable: "c", ToCols: []string{"id"}}, }, } - _ = renderERDDiagram(g, 100) - _ = renderERDDiagram(g, 30) + _ = renderERDDiagram(g, 100, "") + _ = renderERDDiagram(g, 30, "") // cycle for stack detection in erdLayers g2 := types.ERDGraph{ diff --git a/internal/ui/model.go b/internal/ui/model.go index 0583e6a..6ff4c44 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -150,8 +150,9 @@ type Model struct { Activity []types.ActivityRow SelectedActIdx int - ERD types.ERDGraph - ERDOffset int + ERD types.ERDGraph + ERDOffset int + ERDFocusAll bool // when true, show full schema; otherwise focus selected table Favorites []types.Favorite SelectedFavIdx int diff --git a/internal/ui/residual_100_test.go b/internal/ui/residual_100_test.go index 7bb1b70..46f3040 100644 --- a/internal/ui/residual_100_test.go +++ b/internal/ui/residual_100_test.go @@ -305,13 +305,13 @@ func TestConnectionsFramePadAndERDResidual(t *testing.T) { } _ = erdBarycenterOrder(g2, erdLayers(g2)) // render diagram maxX expand - _ = renderERDDiagram(g2, 20) + _ = renderERDDiagram(g2, 20, "") // edge missing node g3 := types.ERDGraph{ Tables: []types.ERDTable{{Name: "only", Columns: []string{"id"}}}, Edges: []types.FKEdge{{FromTable: "only", ToTable: "nope", FromCols: []string{"id"}, ToCols: []string{"id"}}}, } - _ = renderERDDiagram(g3, 40) + _ = renderERDDiagram(g3, 40, "") } func TestSidebarAndHeaderResidual(t *testing.T) { diff --git a/internal/ui/update.go b/internal/ui/update.go index 2170a9f..700dcff 100644 --- a/internal/ui/update.go +++ b/internal/ui/update.go @@ -1104,6 +1104,8 @@ func (m Model) openERD() (tea.Model, tea.Cmd) { m = m.clearObjectContent() m.ContentMode = contentPreview m.ERDOffset = 0 + // Prefer neighborhood of the selected sidebar table when available. + m.ERDFocusAll = false schema := m.CurrentSchema if schema == "" { schema = "public" @@ -1145,6 +1147,18 @@ func (m Model) keysERD(key string) (tea.Model, tea.Cmd) { m.ERDOffset = 0 case "r": return m.openERD() + case "a": + m.ERDFocusAll = true + m.ERDOffset = 0 + m.StatusMsg = "ERD: all tables" + case "f": + if m.erdFocusCandidate() == "" { + m.StatusMsg = "Select a table to focus" + return m, nil + } + m.ERDFocusAll = false + m.ERDOffset = 0 + m.StatusMsg = "ERD: focus " + m.erdFocusCandidate() } return m, nil } diff --git a/internal/ui/view_erd.go b/internal/ui/view_erd.go index 1eaa4e0..08d1a94 100644 --- a/internal/ui/view_erd.go +++ b/internal/ui/view_erd.go @@ -30,11 +30,20 @@ func (m Model) viewERDContent(width, height int) string { schema = "public" } + graph, focus := m.erdViewGraph() + b.WriteString(headerStyle.Render("ERD")) b.WriteString(" ") b.WriteString(accentStyle.Render(schema)) - b.WriteString(dimStyle.Render(fmt.Sprintf(" · %d tables · %d FKs", - len(m.ERD.Tables), len(m.ERD.Edges)))) + if focus != "" { + b.WriteString(dimStyle.Render(" · focus ")) + b.WriteString(accentStyle.Bold(true).Render(focus)) + b.WriteString(dimStyle.Render(fmt.Sprintf(" · %d tables · %d FKs", + len(graph.Tables), len(graph.Edges)))) + } else { + b.WriteString(dimStyle.Render(fmt.Sprintf(" · %d tables · %d FKs", + len(m.ERD.Tables), len(m.ERD.Edges)))) + } b.WriteByte('\n') b.WriteString(gridSepStyle.Render(strings.Repeat("─", min(width, 100)))) b.WriteByte('\n') @@ -50,12 +59,12 @@ func (m Model) viewERDContent(width, height int) string { return b.String() } - bodyH := max(height-3, 4) + bodyH := max(height-4, 4) var lines []string - if width < erdListMinW || len(m.ERD.Tables) > erdMaxTables { - lines = renderERDList(m.ERD, width, true) + if width < erdListMinW || len(graph.Tables) > erdMaxTables { + lines = renderERDList(graph, width, true) } else { - lines = renderERDDiagram(m.ERD, width) + lines = renderERDDiagram(graph, width, focus) } maxOff := max(len(lines)-bodyH, 0) @@ -66,13 +75,71 @@ func (m Model) viewERDContent(width, height int) string { b.WriteByte('\n') } if len(lines) > bodyH { - b.WriteString(dimStyle.Render(fmt.Sprintf("↕ %d–%d of %d · j/k scroll · r refresh", + b.WriteString(dimStyle.Render(fmt.Sprintf("↕ %d–%d of %d · j/k scroll", offset+1, end, len(lines)))) b.WriteByte('\n') } + hint := "r refresh" + if focus != "" { + hint += " · a all tables" + } else if m.erdFocusCandidate() != "" { + hint += " · f focus selection" + } + b.WriteString(dimStyle.Render(hint)) + b.WriteByte('\n') return b.String() } +// erdViewGraph returns the graph to render and an optional focus table name. +func (m Model) erdViewGraph() (types.ERDGraph, string) { + if m.ERDFocusAll { + return m.ERD, "" + } + focus := m.erdFocusCandidate() + if focus == "" { + return m.ERD, "" + } + return erdFocusSubgraph(m.ERD, focus), focus +} + +// erdFocusCandidate is the table to focus when auto-focus is on. +func (m Model) erdFocusCandidate() string { + o, ok := m.selectedObject() + if !ok || o.Kind != types.ObjectTable { + return "" + } + for _, t := range m.ERD.Tables { + if t.Name == o.Name { + return o.Name + } + } + return "" +} + +// erdFocusSubgraph keeps focus and its 1-hop FK neighbors. +func erdFocusSubgraph(g types.ERDGraph, focus string) types.ERDGraph { + if focus == "" { + return g + } + keep := map[string]bool{focus: true} + var edges []types.FKEdge + for _, e := range g.Edges { + if e.FromTable == focus || e.ToTable == focus { + keep[e.FromTable] = true + keep[e.ToTable] = true + edges = append(edges, e) + } + } + // Include edges between kept neighbors (e.g. sibling FKs not needed; only 1-hop). + var tables []types.ERDTable + for _, t := range g.Tables { + if keep[t.Name] { + tables = append(tables, t) + } + } + return types.ERDGraph{Schema: g.Schema, Tables: tables, Edges: edges} +} + // renderERDList renders the FK edge list. withHeader controls the section title. func renderERDList(g types.ERDGraph, width int, withHeader bool) []string { if len(g.Edges) == 0 { @@ -226,23 +293,27 @@ func (c *erdCanvas) writeStr(x, y int, s string, a byte) { } } -func (c *erdCanvas) drawBox(n erdNode, fkCols map[string]bool) { +func (c *erdCanvas) drawBox(n erdNode, fkCols map[string]bool, highlight bool) { inner := n.w - 2 - c.put(n.x, n.y, '┌', 1) - c.put(n.x+n.w-1, n.y, '┐', 1) - c.put(n.x, n.y+n.h-1, '└', 1) - c.put(n.x+n.w-1, n.y+n.h-1, '┘', 1) - c.hline(n.x+1, n.x+n.w-2, n.y, 1) - c.hline(n.x+1, n.x+n.w-2, n.y+n.h-1, 1) + border := byte(1) + if highlight { + border = 2 + } + c.put(n.x, n.y, '┌', border) + c.put(n.x+n.w-1, n.y, '┐', border) + c.put(n.x, n.y+n.h-1, '└', border) + c.put(n.x+n.w-1, n.y+n.h-1, '┘', border) + c.hline(n.x+1, n.x+n.w-2, n.y, border) + c.hline(n.x+1, n.x+n.w-2, n.y+n.h-1, border) for y := n.y + 1; y < n.y+n.h-1; y++ { - c.put(n.x, y, '│', 1) - c.put(n.x+n.w-1, y, '│', 1) + c.put(n.x, y, '│', border) + c.put(n.x+n.w-1, y, '│', border) } title := truncate(n.name, inner) c.writeStr(n.x+1, n.y+1, padRight(title, inner), 3) - c.put(n.x, n.y+2, '├', 1) - c.put(n.x+n.w-1, n.y+2, '┤', 1) - c.hline(n.x+1, n.x+n.w-2, n.y+2, 1) + c.put(n.x, n.y+2, '├', border) + c.put(n.x+n.w-1, n.y+2, '┤', border) + c.hline(n.x+1, n.x+n.w-2, n.y+2, border) cols := orderERDColumns(n.name, n.table.Columns, fkCols, true) if len(cols) > erdMaxCols { @@ -384,7 +455,7 @@ func (c *erdCanvas) lines() []string { return out } -func renderERDDiagram(g types.ERDGraph, width int) []string { +func renderERDDiagram(g types.ERDGraph, width int, focus string) []string { if len(g.Tables) == 0 { return nil } @@ -395,7 +466,7 @@ func renderERDDiagram(g types.ERDGraph, width int) []string { var lines []string if len(linked.Tables) > 0 { layers := erdWrapLayers(erdBarycenterOrder(linked, erdLayers(linked)), erdMaxPerRow) - lines = append(lines, layoutERDCanvas(linked, layers, fkCols, width)...) + lines = append(lines, layoutERDCanvas(linked, layers, fkCols, width, focus)...) } if len(g.Edges) > 0 { @@ -414,7 +485,7 @@ func renderERDDiagram(g types.ERDGraph, width int) []string { } // layoutERDCanvas places layered boxes and routes FK edges onto a canvas. -func layoutERDCanvas(g types.ERDGraph, layers [][]string, fkCols map[string]bool, width int) []string { +func layoutERDCanvas(g types.ERDGraph, layers [][]string, fkCols map[string]bool, width int, focus string) []string { nAcross := maxLayerWidth(layers) boxW := clamp((width-(nAcross-1)*erdHGap)/max(nAcross, 1), erdBoxMinW, erdBoxMaxW) @@ -500,7 +571,7 @@ func layoutERDCanvas(g types.ERDGraph, layers [][]string, fkCols map[string]bool } for _, n := range nodes { - cv.drawBox(*n, fkCols) + cv.drawBox(*n, fkCols, focus != "" && n.name == focus) } return cv.lines() } diff --git a/internal/ui/view_utils_test.go b/internal/ui/view_utils_test.go index 1d5266a..bffdb8c 100644 --- a/internal/ui/view_utils_test.go +++ b/internal/ui/view_utils_test.go @@ -193,7 +193,7 @@ func TestERDLayersParentsAboveChildren(t *testing.T) { if pos["users"] >= pos["orders"] || pos["orders"] >= pos["order_items"] { t.Fatalf("bad layer order: %v pos=%v", layers, pos) } - out := renderERDDiagram(g, 100) + out := renderERDDiagram(g, 100, "") joined := strings.Join(out, "\n") if !strings.Contains(joined, "users") || !strings.Contains(joined, "orders") { t.Fatalf("diagram missing tables: %q", joined)