github.com/aretext/aretext@v1.3.0/state/menu.go (about) 1 package state 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 "os" 8 "path/filepath" 9 "sort" 10 11 "github.com/aretext/aretext/file" 12 "github.com/aretext/aretext/menu" 13 "github.com/aretext/aretext/text" 14 ) 15 16 type MenuStyle int 17 18 const ( 19 MenuStyleCommand = MenuStyle(iota) 20 MenuStyleFilePath 21 MenuStyleFileLocation 22 MenuStyleChildDir 23 MenuStyleParentDir 24 MenuStyleInsertChoice 25 MenuStyleWorkingDir 26 ) 27 28 // EmptyQueryShowAll returns whether an empty query should show all items. 29 func (s MenuStyle) EmptyQueryShowAll() bool { 30 switch s { 31 case MenuStyleFilePath, MenuStyleFileLocation, MenuStyleChildDir, MenuStyleParentDir, MenuStyleInsertChoice, MenuStyleWorkingDir: 32 return true 33 default: 34 return false 35 } 36 } 37 38 // MenuState represents the menu for searching and selecting items. 39 type MenuState struct { 40 // style controls how the menu is displayed. 41 style MenuStyle 42 43 // query is the text input by the user to search for a menu item. 44 query text.RuneStack 45 46 // search controls which items are visible based on the user's current search query. 47 search *menu.Search 48 49 // selectedResultIdx is the index of the currently selected search result. 50 // If there are no results, this is set to zero. 51 // If there are results, this must be less than the number of results. 52 selectedResultIdx int 53 54 // prevInputMode is the input mode to set after exiting menu mode. 55 prevInputMode InputMode 56 } 57 58 func (m *MenuState) Style() MenuStyle { 59 return m.style 60 } 61 62 func (m *MenuState) SearchQuery() string { 63 return m.query.String() 64 } 65 66 func (m *MenuState) SearchResults() (results []menu.Item, selectedResultIdx int) { 67 if m.search == nil { 68 return nil, 0 69 } 70 return m.search.Results(), m.selectedResultIdx 71 } 72 73 // ShowMenu displays the menu with the specified style and items. 74 func ShowMenu(state *EditorState, style MenuStyle, items []menu.Item) { 75 if style == MenuStyleCommand { 76 items = append(items, state.customMenuItems...) 77 } 78 79 switch style { 80 case MenuStyleParentDir: 81 // Sort lexicographic order descending. 82 // This ensures that longer paths appear first when listing parent directory paths. 83 sort.SliceStable(items, func(i, j int) bool { return items[i].Name > items[j].Name }) 84 85 case MenuStyleCommand, MenuStyleFilePath, MenuStyleChildDir: 86 // Sort lexicographic order ascending. 87 sort.SliceStable(items, func(i, j int) bool { return items[i].Name < items[j].Name }) 88 } 89 90 search := menu.NewSearch(items, style.EmptyQueryShowAll()) 91 state.menu = &MenuState{ 92 style: style, 93 search: search, 94 selectedResultIdx: 0, 95 prevInputMode: state.inputMode, 96 } 97 setInputMode(state, InputModeMenu) 98 } 99 100 // ShowFileMenu displays a menu for finding and loading files in the current working directory. 101 // The files are loaded asynchronously as a task that the user can cancel. 102 func ShowFileMenu(s *EditorState, dirPatternsToHide []string) { 103 log.Printf("Scheduling task to load file menu items...\n") 104 StartTask(s, func(ctx context.Context) func(*EditorState) { 105 log.Printf("Starting to load file menu items...\n") 106 items := loadFileMenuItems(ctx, dirPatternsToHide) 107 log.Printf("Successfully loaded %d file menu items\n", len(items)) 108 return func(s *EditorState) { 109 ShowMenu(s, MenuStyleFilePath, items) 110 } 111 }) 112 } 113 114 func loadFileMenuItems(ctx context.Context, dirPatternsToHide []string) []menu.Item { 115 dir, err := os.Getwd() 116 if err != nil { 117 log.Printf("Error loading menu items: %v\n", fmt.Errorf("os.GetCwd: %w", err)) 118 return nil 119 } 120 121 paths := file.ListDir(ctx, dir, file.ListDirOptions{ 122 DirPatternsToHide: dirPatternsToHide, 123 }) 124 log.Printf("Listed %d paths for dir %q\n", len(paths), dir) 125 126 items := make([]menu.Item, 0, len(paths)) 127 for _, p := range paths { 128 menuPath := p // reference path in this iteration of the loop 129 items = append(items, menu.Item{ 130 Name: file.RelativePath(menuPath, dir), 131 Action: func(s *EditorState) { 132 LoadDocument(s, menuPath, true, func(LocatorParams) uint64 { 133 return 0 134 }) 135 }, 136 }) 137 } 138 return items 139 } 140 141 // ShowChildDirsMenu displays a menu for changing the working directory to a child directory. 142 func ShowChildDirsMenu(s *EditorState, dirPatternsToHide []string) { 143 log.Printf("Scheduling task to load child dir menu items...\n") 144 StartTask(s, func(ctx context.Context) func(*EditorState) { 145 log.Printf("Starting to load child dir menu items...\n") 146 items := loadChildDirMenuItems(ctx, dirPatternsToHide) 147 log.Printf("Successfully loaded %d child dir menu items\n", len(items)) 148 return func(s *EditorState) { 149 ShowMenu(s, MenuStyleChildDir, items) 150 } 151 }) 152 } 153 154 func loadChildDirMenuItems(ctx context.Context, dirPatternsToHide []string) []menu.Item { 155 dir, err := os.Getwd() 156 if err != nil { 157 log.Printf("Error loading menu items: %v\n", fmt.Errorf("os.GetCwd: %w", err)) 158 return nil 159 } 160 161 paths := file.ListDir(ctx, dir, file.ListDirOptions{ 162 DirectoriesOnly: true, 163 DirPatternsToHide: dirPatternsToHide, 164 }) 165 log.Printf("Listed %d subdirectory paths for dir %q\n", len(paths), dir) 166 167 items := make([]menu.Item, 0, len(paths)) 168 for _, p := range paths { 169 menuPath := p // reference path in this iteration of the loop 170 items = append(items, menu.Item{ 171 Name: fmt.Sprintf("./%s", file.RelativePath(menuPath, dir)), 172 Action: func(s *EditorState) { 173 SetWorkingDirectory(s, menuPath) 174 }, 175 }) 176 } 177 return items 178 } 179 180 // ShowParentDirsMenu displays a menu for changing the working directory to a parent directory. 181 func ShowParentDirsMenu(s *EditorState) { 182 ShowMenu(s, MenuStyleParentDir, parentDirMenuItems()) 183 } 184 185 func parentDirMenuItems() []menu.Item { 186 dir, err := os.Getwd() 187 if err != nil { 188 log.Printf("Error loading menu items: %v\n", fmt.Errorf("os.GetCwd: %w", err)) 189 return nil 190 } 191 192 dir = filepath.Clean(dir) 193 194 // Create an item for each parent directory. 195 // We can detect when we've reached the root directory by checking the last character 196 // of the path because both filepath.Clean and filepath.Dir 197 // guarantee that only the root directory ends in a separator. 198 var items []menu.Item 199 for len(dir) > 0 && dir[len(dir)-1] != os.PathSeparator { 200 dir = filepath.Dir(dir) 201 menuDir := dir // reference path in this iteration of the loop 202 items = append(items, menu.Item{ 203 Name: dir, 204 Action: func(s *EditorState) { 205 SetWorkingDirectory(s, menuDir) 206 }, 207 }) 208 } 209 return items 210 } 211 212 // HideMenu hides the menu. 213 func HideMenu(state *EditorState) { 214 prevInputMode := state.menu.prevInputMode 215 state.menu = &MenuState{} 216 setInputMode(state, prevInputMode) 217 } 218 219 // ExecuteSelectedMenuItem executes the action of the selected menu item and closes the menu. 220 func ExecuteSelectedMenuItem(state *EditorState) { 221 search := state.menu.search 222 results := search.Results() 223 if len(results) == 0 { 224 // If there are no results, then there is no action to perform. 225 SetStatusMsg(state, StatusMsg{ 226 Style: StatusMsgStyleError, 227 Text: "No menu item selected", 228 }) 229 HideMenu(state) 230 return 231 } 232 233 idx := state.menu.selectedResultIdx 234 selectedItem := results[idx] 235 236 // Some menu commands enter a different input mode (like task mode for shell commands), 237 // then return to whatever the input mode was at the start of the action. 238 // Hide the menu first so that these actions return to normal/visual mode, not menu mode. 239 HideMenu(state) 240 241 executeMenuItemAction(state, selectedItem) 242 ScrollViewToCursor(state) 243 } 244 245 func executeMenuItemAction(state *EditorState, item menu.Item) { 246 log.Printf("Executing menu item %q\n", item.Name) 247 actionFunc, ok := item.Action.(func(*EditorState)) 248 if !ok { 249 log.Printf("Invalid action for menu item %q\n", item.Name) 250 return 251 } 252 actionFunc(state) 253 } 254 255 // MoveMenuSelection moves the menu selection up or down with wraparound. 256 func MoveMenuSelection(state *EditorState, delta int) { 257 numResults := len(state.menu.search.Results()) 258 if numResults == 0 { 259 return 260 } 261 262 newIdx := (state.menu.selectedResultIdx + delta) % numResults 263 if newIdx < 0 { 264 newIdx = numResults + newIdx 265 } 266 267 state.menu.selectedResultIdx = newIdx 268 } 269 270 // AppendMenuSearch appends a rune to the menu search query. 271 func AppendRuneToMenuSearch(state *EditorState, r rune) { 272 menu := state.menu 273 menu.query.Push(r) 274 menu.search.Execute(menu.query.String()) 275 menu.selectedResultIdx = 0 276 } 277 278 // DeleteMenuSearch deletes a rune from the menu search query. 279 func DeleteRuneFromMenuSearch(state *EditorState) { 280 menu := state.menu 281 if menu.query.Len() > 0 { 282 menu.query.Pop() 283 menu.search.Execute(menu.query.String()) 284 menu.selectedResultIdx = 0 285 } 286 }