55 "os"
66 "path/filepath"
77 "strings"
8+
9+ "github.com/yorukot/superfile/src/internal/utils"
810)
911
1012// Cancel typing modal e.g. create file or directory
@@ -144,34 +146,42 @@ func (m *model) helpMenuListUp() {
144146 m .helpMenu .cursor --
145147 if m .helpMenu .cursor < m .helpMenu .renderIndex {
146148 m .helpMenu .renderIndex --
147- if m .helpMenu .data [m .helpMenu .cursor ].subTitle != "" {
149+ if m .helpMenu .filteredData [m .helpMenu .cursor ].subTitle != "" {
148150 m .helpMenu .renderIndex --
149151 }
150152 }
151- if m .helpMenu .data [m .helpMenu .cursor ].subTitle != "" {
153+ if m .helpMenu .filteredData [m .helpMenu .cursor ].subTitle != "" {
152154 m .helpMenu .cursor --
153155 }
154156 } else {
155- m .helpMenu .cursor = len (m .helpMenu .data ) - 1
156- m .helpMenu .renderIndex = len (m .helpMenu .data ) - m .helpMenu .height
157+ // Set the cursor to the last item in the list.
158+ // We use max(..., 0) as a safeguard to prevent a negative cursor index
159+ // in case the filtered list is empty.
160+ m .helpMenu .cursor = max (len (m .helpMenu .filteredData )- 1 , 0 )
161+
162+ // Adjust the render index to show the bottom of the list.
163+ // Similarly, we use max(..., 0) to ensure the renderIndex doesn't become negative,
164+ // which can happen if the number of items is less than the view height.
165+ // This prevents a potential out-of-bounds panic during rendering.
166+ m .helpMenu .renderIndex = max (len (m .helpMenu .filteredData )- m .helpMenu .height , 0 )
157167 }
158168}
159169
160170// Help menu panel list down
161171func (m * model ) helpMenuListDown () {
162- if len (m .helpMenu .data ) == 0 {
172+ if len (m .helpMenu .filteredData ) == 0 {
163173 return
164174 }
165175
166- if m .helpMenu .cursor < len (m .helpMenu .data )- 1 {
176+ if m .helpMenu .cursor < len (m .helpMenu .filteredData )- 1 {
167177 m .helpMenu .cursor ++
168- if m .helpMenu .cursor > m .helpMenu .renderIndex + m .helpMenu .height - 1 {
178+ if m .helpMenu .cursor > m .helpMenu .renderIndex + m .helpMenu .height - 2 {
169179 m .helpMenu .renderIndex ++
170- if m .helpMenu .data [m .helpMenu .cursor ].subTitle != "" {
180+ if m .helpMenu .filteredData [m .helpMenu .cursor ].subTitle != "" {
171181 m .helpMenu .renderIndex ++
172182 }
173183 }
174- if m .helpMenu .data [m .helpMenu .cursor ].subTitle != "" {
184+ if m .helpMenu .filteredData [m .helpMenu .cursor ].subTitle != "" {
175185 m .helpMenu .cursor ++
176186 }
177187 } else {
@@ -180,17 +190,88 @@ func (m *model) helpMenuListDown() {
180190 }
181191}
182192
193+ func removeOrphanSections (items []helpMenuModalData ) []helpMenuModalData {
194+ var result []helpMenuModalData
195+ // Since we can't know beforehand which section are we actually filtering
196+ // we may end up in a scenario where there are two sections (General, Panel navigation)
197+ // with no hotkeys between them, so we need to remove the section which its hotkeys was
198+ // completely filtered out (Orphan sections)
199+ for i := range items {
200+ if items [i ].subTitle != "" {
201+ // Look ahead: is the next item a real hotkey?
202+ if i + 1 < len (items ) && items [i + 1 ].subTitle == "" {
203+ result = append (result , items [i ])
204+ }
205+ // Else: skip this subtitle because no children
206+ } else {
207+ result = append (result , items [i ])
208+ }
209+ }
210+ return result
211+ }
212+
213+ func (m * model ) filterHelpMenu (query string ) {
214+ filtered := fuzzySearch (query , m .helpMenu .data )
215+ filtered = removeOrphanSections (filtered )
216+
217+ m .helpMenu .filteredData = filtered
218+ m .helpMenu .cursor = 1
219+ m .helpMenu .renderIndex = 0
220+ }
221+
222+ // Fuzzy search function for a list of helpMenuModalData.
223+ // inspired from: sidebar/directory_utils.go
224+ func fuzzySearch (query string , data []helpMenuModalData ) []helpMenuModalData {
225+ if len (data ) == 0 {
226+ return []helpMenuModalData {}
227+ }
228+
229+ // Optimization - This haystack can be kept precomputed based on description
230+ // instead of re computing it in each call
231+ haystack := []string {}
232+ idxMap := []int {}
233+ for i , item := range data {
234+ if item .subTitle == "" {
235+ haystack = append (haystack , item .description )
236+ idxMap = append (idxMap , i )
237+ }
238+ }
239+
240+ matchedIdx := map [int ]struct {}{}
241+ for _ , match := range utils .FzfSearch (query , haystack ) {
242+ matchedIdx [idxMap [match.HayIndex ]] = struct {}{}
243+ // if d, ok := idx[match.Key]; ok {
244+ // filteredDirs = append(filteredDirs, d)
245+ // }
246+ }
247+ results := []helpMenuModalData {}
248+ for i , d := range data {
249+ if d .subTitle != "" {
250+ results = append (results , d )
251+ continue
252+ }
253+ if _ , ok := matchedIdx [i ]; ok {
254+ results = append (results , d )
255+ }
256+ }
257+
258+ return results
259+ }
260+
183261// Toggle help menu
184262func (m * model ) openHelpMenu () {
185263 if m .helpMenu .open {
186264 m .helpMenu .open = false
187265 return
188266 }
189267
268+ // Reset filteredData to the full data whenever the helpMenu is opened
269+ m .helpMenu .filteredData = m .helpMenu .data
190270 m .helpMenu .open = true
191271}
192272
193273// Quit help menu
194274func (m * model ) quitHelpMenu () {
275+ m .helpMenu .searchBar .Reset ()
195276 m .helpMenu .open = false
196277}
0 commit comments