1- import { useEffect , useMemo , useState } from "react" ;
1+ import { useCallback , useEffect , useMemo , useState } from "react" ;
22
33import type { BranchInfo } from "../types" ;
44import { ChipDropdown } from "./ChipDropdown" ;
5+ import { VirtualChipList , type VirtualChipRow } from "./VirtualChipList" ;
6+
7+ // Virtualised-row heights (px) — mirror the box heights in styles.css.
8+ const ITEM_H = 26 ; // .chip-item — one-line branch entry
9+ const HEADER_H = 25 ; // .chip-section-header — "Local" / "Remote tracking"
10+ const EMPTY_H = 34 ; // .chip-empty — "No branches match."
511
612interface Props {
713 open : boolean ;
@@ -72,46 +78,106 @@ export function BranchChip({
7278 return `${ selected . length } branches` ;
7379 } , [ selected , branches ] ) ;
7480
75- function toggle ( refName : string ) {
76- if ( selected === "all" ) {
77- // GitLens / IDE sidebar pattern: clicking a branch from the "all"
78- // state means "focus on THIS one". The previous "everything except
79- // this one" behaviour was a multi-select-with-checkboxes mental
80- // model that doesn't match what users expect from a branch filter.
81- onChange ( [ refName ] ) ;
82- return ;
83- }
84- const set = new Set ( selected ) ;
85- if ( set . has ( refName ) ) set . delete ( refName ) ;
86- else set . add ( refName ) ;
87- const next = Array . from ( set ) ;
88- onChange ( next . length === branches . length ? "all" : next ) ;
89- }
81+ const toggle = useCallback (
82+ ( refName : string ) => {
83+ if ( selected === "all" ) {
84+ // GitLens / IDE sidebar pattern: clicking a branch from the "all"
85+ // state means "focus on THIS one". The previous "everything except
86+ // this one" behaviour was a multi-select-with-checkboxes mental
87+ // model that doesn't match what users expect from a branch filter.
88+ onChange ( [ refName ] ) ;
89+ return ;
90+ }
91+ const set = new Set ( selected ) ;
92+ if ( set . has ( refName ) ) set . delete ( refName ) ;
93+ else set . add ( refName ) ;
94+ const next = Array . from ( set ) ;
95+ onChange ( next . length === branches . length ? "all" : next ) ;
96+ } ,
97+ [ selected , branches , onChange ] ,
98+ ) ;
99+
100+ // Flatten the two sections into one virtual-row list. The Local / Remote
101+ // headers and the empty-state line are known-height special rows
102+ // interleaved with the branch rows.
103+ const rows = useMemo < VirtualChipRow [ ] > ( ( ) => {
104+ const branchRow = ( b : BranchInfo ) : VirtualChipRow => {
105+ // In the "all" meta-state the All branches row at the top carries the
106+ // highlight — individual items shouldn't ALSO look "checked", or a
107+ // user clicking a row to "uncheck it" is met with the GitLens focus
108+ // behaviour and it feels like a different row got deselected. So: no
109+ // per-item ✓ until the user makes an explicit selection.
110+ const isSelected =
111+ selected !== "all" && ( selected as string [ ] ) . includes ( b . refName ) ;
112+ return {
113+ key : "branch:" + b . refName ,
114+ height : ITEM_H ,
115+ render : ( ) => (
116+ < button
117+ type = "button"
118+ className = { "chip-item" + ( isSelected ? " checked" : "" ) }
119+ onClick = { ( ) => toggle ( b . refName ) }
120+ >
121+ < span className = "chip-check" > { isSelected ? "✓" : "" } </ span >
122+ < span className = "chip-item-name" >
123+ { b . name }
124+ { b . isHead && < span className = "chip-item-head" > · HEAD</ span > }
125+ </ span >
126+ < span className = "chip-item-count" > { b . commitCount } </ span >
127+ </ button >
128+ ) ,
129+ } ;
130+ } ;
90131
91- function renderItem ( b : BranchInfo ) {
92- // In the "all" meta-state the All branches row at the top carries the
93- // highlight — individual items shouldn't ALSO look "checked", or a
94- // user clicking a row to "uncheck it" is met with the GitLens focus
95- // behaviour and it feels like a different row got deselected. So: no
96- // per-item ✓ until the user makes an explicit selection.
97- const isSelected =
98- selected !== "all" && ( selected as string [ ] ) . includes ( b . refName ) ;
99- return (
100- < button
101- key = { b . refName }
102- type = "button"
103- className = { "chip-item" + ( isSelected ? " checked" : "" ) }
104- onClick = { ( ) => toggle ( b . refName ) }
105- >
106- < span className = "chip-check" > { isSelected ? "✓" : "" } </ span >
107- < span className = "chip-item-name" >
108- { b . name }
109- { b . isHead && < span className = "chip-item-head" > · HEAD</ span > }
110- </ span >
111- < span className = "chip-item-count" > { b . commitCount } </ span >
112- </ button >
113- ) ;
114- }
132+ const out : VirtualChipRow [ ] = [ ] ;
133+ out . push ( {
134+ key : "__all" ,
135+ height : ITEM_H ,
136+ render : ( ) => (
137+ < button
138+ type = "button"
139+ className = { "chip-item" + ( selected === "all" ? " active" : "" ) }
140+ onClick = { ( ) => {
141+ onChange ( "all" ) ;
142+ onClose ( ) ;
143+ } }
144+ >
145+ < span className = "chip-item-name" > All branches</ span >
146+ </ button >
147+ ) ,
148+ } ) ;
149+ if ( localBranches . length > 0 ) {
150+ out . push ( {
151+ key : "__local" ,
152+ height : HEADER_H ,
153+ render : ( ) => < div className = "chip-section-header" > Local</ div > ,
154+ } ) ;
155+ for ( const b of localBranches ) out . push ( branchRow ( b ) ) ;
156+ }
157+ if ( remoteBranches . length > 0 ) {
158+ out . push ( {
159+ key : "__remote" ,
160+ height : HEADER_H ,
161+ render : ( ) => (
162+ < div
163+ className = "chip-section-header"
164+ title = "Remote-tracking refs are local — gitwink never calls git fetch. Updated by your IDE / CLI."
165+ >
166+ Remote tracking
167+ </ div >
168+ ) ,
169+ } ) ;
170+ for ( const b of remoteBranches ) out . push ( branchRow ( b ) ) ;
171+ }
172+ if ( localBranches . length === 0 && remoteBranches . length === 0 ) {
173+ out . push ( {
174+ key : "__empty" ,
175+ height : EMPTY_H ,
176+ render : ( ) => < div className = "chip-empty" > No branches match.</ div > ,
177+ } ) ;
178+ }
179+ return out ;
180+ } , [ localBranches , remoteBranches , selected , onChange , onClose , toggle ] ) ;
115181
116182 return (
117183 < ChipDropdown
@@ -129,41 +195,7 @@ export function BranchChip({
129195 placeholder = "Search branches…"
130196 />
131197 </ div >
132- < div className = "chip-list" >
133- < button
134- type = "button"
135- className = { "chip-item" + ( selected === "all" ? " active" : "" ) }
136- onClick = { ( ) => {
137- onChange ( "all" ) ;
138- onClose ( ) ;
139- } }
140- >
141- < span className = "chip-item-name" > All branches</ span >
142- </ button >
143-
144- { localBranches . length > 0 && (
145- < >
146- < div className = "chip-section-header" > Local</ div >
147- { localBranches . map ( renderItem ) }
148- </ >
149- ) }
150-
151- { remoteBranches . length > 0 && (
152- < >
153- < div
154- className = "chip-section-header"
155- title = "Remote-tracking refs are local — gitwink never calls git fetch. Updated by your IDE / CLI."
156- >
157- Remote tracking
158- </ div >
159- { remoteBranches . map ( renderItem ) }
160- </ >
161- ) }
162-
163- { localBranches . length === 0 && remoteBranches . length === 0 && (
164- < div className = "chip-empty" > No branches match.</ div >
165- ) }
166- </ div >
198+ < VirtualChipList rows = { rows } resetKey = { query } />
167199 </ ChipDropdown >
168200 ) ;
169201}
0 commit comments