| 🛟 Updated. 28d5985 k33g 12h ago | 1 | package app |
| 2 | |
| 📦 Turbo Core f3ade8d k33g 4h ago | 3 | import "rickub.com/turbo-editors/turbo-core/ui" |
| 🛟 Updated. 28d5985 k33g 12h ago | 4 | |
| 5 | // buildMenus returns the editor's menu bar: File, Edit, Search, Run, Options, |
| 6 | // Window and Help, laid out the way Turbo C laid its own out, with Snippets, |
| 7 | // the editor's own toolchain menu, and whatever menus the project's tools file |
| 8 | // asks for. |
| 9 | // |
| 10 | // Every item is wired straight to a method of the app, and the ones that only |
| 11 | // make sense with a file open say so through Enabled, so the menu greys them |
| 12 | // out instead of doing nothing when chosen. |
| 13 | // |
| 14 | // allMenus is where the order lives, because the bar is rebuilt from it every |
| 15 | // time the tools file changes. |
| 16 | func (a *App) buildMenus() *ui.MenuBar { |
| 17 | return ui.NewMenuBar(a.allMenus()...) |
| 18 | } |
| 19 | |
| 20 | // hasWindow reports whether there is a file open to act on. |
| 21 | func (a *App) hasWindow() bool { return a.desktop.Active() != nil } |
| 22 | |
| 23 | func (a *App) fileMenu() *ui.Menu { |
| 24 | return &ui.Menu{Label: "~F~ile", Items: []*ui.MenuItem{ |
| 25 | {Label: "~N~ew", Shortcut: "F4", Action: a.NewFile}, |
| 26 | {Label: "~O~pen…", Shortcut: "F3", Action: a.OpenFile}, |
| 27 | {Label: "~S~ave", Shortcut: "F2", Action: a.SaveFile, Enabled: a.hasWindow}, |
| 28 | {Label: "Save ~a~s…", Action: a.SaveFileAs, Enabled: a.hasWindow}, |
| 29 | {Separator: true}, |
| 30 | {Label: "~C~lose", Shortcut: "Ctrl-W", Action: a.CloseFile, Enabled: a.hasWindow}, |
| 31 | {Separator: true}, |
| 32 | {Label: "E~x~it", Shortcut: "Alt-X", Action: a.Quit}, |
| 33 | }} |
| 34 | } |
| 35 | |
| 36 | func (a *App) editMenu() *ui.Menu { |
| 37 | return &ui.Menu{Label: "~E~dit", Items: []*ui.MenuItem{ |
| 38 | {Label: "~U~ndo", Shortcut: "Ctrl-Z", Action: a.Undo, Enabled: a.hasWindow}, |
| 39 | // Ctrl-R, not the Ctrl-Y it used to be: Ctrl-Y is Turbo C's |
| 40 | // delete-line, and between an editor that looks like Turbo C and a |
| 41 | // habit picked up here, the first has the better claim. Ctrl-Shift-Z |
| 42 | // is not an option — a terminal delivers it as plain Ctrl-Z. |
| 43 | {Label: "~R~edo", Shortcut: "Ctrl-R", Action: a.Redo, Enabled: a.hasWindow}, |
| 44 | {Separator: true}, |
| 45 | {Label: "~I~nsert line", Shortcut: "Ctrl-N", Action: a.InsertLine, Enabled: a.hasWindow}, |
| 46 | {Label: "~D~elete line", Shortcut: "Ctrl-Y", Action: a.DeleteLine, Enabled: a.hasWindow}, |
| 47 | {Separator: true}, |
| 48 | {Label: "Cu~t~", Shortcut: "Shift-Del", Action: a.Cut, Enabled: a.hasWindow}, |
| 49 | {Label: "~C~opy", Shortcut: "Ctrl-Ins", Action: a.Copy, Enabled: a.hasWindow}, |
| 50 | {Label: "~P~aste", Shortcut: "Shift-Ins", Action: a.Paste, Enabled: a.hasWindow}, |
| 51 | {Separator: true}, |
| 52 | {Label: "Select ~a~ll", Shortcut: "Ctrl-A", Action: a.SelectAll, Enabled: a.hasWindow}, |
| 53 | }} |
| 54 | } |
| 55 | |
| 56 | func (a *App) searchMenu() *ui.Menu { |
| 57 | return &ui.Menu{Label: "~S~earch", Items: []*ui.MenuItem{ |
| 58 | {Label: "~F~ind…", Shortcut: "Ctrl-F", Action: a.Find, Enabled: a.hasWindow}, |
| 59 | {Label: "Find ~n~ext", Shortcut: "F7", Action: a.FindNext, Enabled: a.hasWindow}, |
| 60 | {Label: "Find ~p~revious", Shortcut: "Shift-F7", Action: a.FindPrevious, Enabled: a.hasWindow}, |
| 61 | {Separator: true}, |
| 62 | {Label: "~G~o to line…", Shortcut: "Ctrl-G", Action: a.GoToLine, Enabled: a.hasWindow}, |
| 63 | }} |
| 64 | } |
| 65 | |
| 66 | // agentMenu lists the coding agents the project's acp.toml knows about, one |
| 67 | // window each. |
| 68 | // |
| 69 | // It is on the bar whether or not any agent is configured, because that is |
| 70 | // where Create agents file has to be reachable from — the same reason the |
| 71 | // toolchain menu stays fixed even in a project with no tools file. |
| 72 | // |
| 73 | // A file that cannot be read shows the error as a greyed line rather than an |
| 74 | // empty menu: a silent drop looks exactly like having no agents. |
| 75 | func (a *App) agentMenu() *ui.Menu { |
| 76 | menu := &ui.Menu{Label: "~A~gent"} |
| 77 | menu.OnOpen = func() { menu.Items = a.agentItems() } |
| 78 | menu.Items = a.agentItems() |
| 79 | return menu |
| 80 | } |
| 81 | |
| 82 | // agentItems builds the menu's contents, which are read from the file every |
| 83 | // time it drops down so that editing acp.toml never needs a restart. |
| 84 | func (a *App) agentItems() []*ui.MenuItem { |
| 85 | var items []*ui.MenuItem |
| 86 | |
| 87 | list, err := a.loadAgents() |
| 88 | switch { |
| 89 | case err != nil: |
| 90 | items = append(items, &ui.MenuItem{Label: "(the agents file has an error)", Enabled: never}) |
| 91 | case list.Len() == 0: |
| 92 | items = append(items, &ui.MenuItem{Label: "(no agents configured)", Enabled: never}) |
| 93 | default: |
| 94 | for _, agent := range list.Agents() { |
| 95 | name := agent.Name |
| 96 | items = append(items, &ui.MenuItem{Label: name, Action: func() { a.NewAgent(name) }}) |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | return append(items, |
| 101 | &ui.MenuItem{Separator: true}, |
| 102 | &ui.MenuItem{Label: "~C~ancel turn", Shortcut: "Esc", Action: a.CancelAgentTurn, Enabled: a.agentTurnRunning}, |
| 103 | &ui.MenuItem{Label: "Create agents ~f~ile", Action: a.CreateAgentsFile, Enabled: a.hasNoAgentsFile}, |
| 104 | &ui.MenuItem{Label: "Agent ~s~tatus", Action: a.AgentStatus}, |
| 105 | ) |
| 106 | } |
| 107 | |
| 108 | // never is the Enabled of a line that is a message rather than a command. |
| 109 | func never() bool { return false } |
| 110 | |
| 111 | func (a *App) runMenu() *ui.Menu { |
| 112 | return &ui.Menu{Label: "~R~un", Items: []*ui.MenuItem{ |
| 113 | {Label: "~C~ompletion", Shortcut: "Ctrl-Space", Action: a.RequestCompletion, Enabled: a.hasWindow}, |
| 114 | {Separator: true}, |
| 115 | {Label: "~L~anguage server status", Action: a.ShowLanguageStatus}, |
| 116 | }} |
| 117 | } |
| 118 | |
| 119 | // codeMenu holds everything the editor asks the language server about the |
| 120 | // symbol under the cursor. |
| 121 | // |
| 122 | // It exists because that work was split across two menus — Describe symbol in |
| 123 | // Run, Go to definition in Search — and neither is where a reader looks for |
| 124 | // "what else knows about this name". Both keep their keys: only where they are |
| 125 | // listed has changed. |
| 126 | // |
| 127 | // Every item acts on the **cursor**, not on a selection. Almost every request |
| 128 | // in the protocol takes a position rather than a range, so asking a user to |
| 129 | // select something first would be an invented step that answers nothing extra. |
| 130 | func (a *App) codeMenu() *ui.Menu { |
| 131 | return &ui.Menu{Label: "~C~ode", Items: []*ui.MenuItem{ |
| 132 | {Label: "~D~escribe symbol", Shortcut: "F1", Action: a.DescribeSymbol, Enabled: a.hasWindow}, |
| 133 | {Separator: true}, |
| 134 | {Label: "~G~o to definition", Shortcut: "F12", Action: a.GoToDefinition, Enabled: a.hasWindow}, |
| 135 | {Label: "Go to ~t~ype definition", Action: a.GoToTypeDefinition, Enabled: a.hasWindow}, |
| 136 | {Label: "Find ~i~mplementations…", Action: a.FindImplementations, Enabled: a.hasWindow}, |
| 137 | {Label: "Find ~r~eferences…", Shortcut: "Shift-F12", Action: a.FindReferences, Enabled: a.hasWindow}, |
| 138 | {Separator: true}, |
| 139 | // Symbol in file has no key on purpose: the obvious one is Ctrl-Shift-O, |
| 140 | // and a terminal cannot tell that from Ctrl-O — the shift is lost before |
| 141 | // the editor sees it. A menu item that advertises a key which does |
| 142 | // nothing is worse than one with no key at all. |
| 143 | {Label: "Symbol in ~f~ile…", Action: a.SymbolInFile, Enabled: a.hasWindow}, |
| 144 | {Label: "Symbol in ~p~roject…", Shortcut: "Ctrl-T", Action: a.SymbolInProject}, |
| 145 | {Separator: true}, |
| 146 | {Label: "Pro~b~lems…", Action: a.ShowProblems}, |
| 147 | }} |
| 148 | } |
| 149 | |
| 150 | func (a *App) optionsMenu() *ui.Menu { |
| 151 | return &ui.Menu{Label: "~O~ptions", Items: []*ui.MenuItem{ |
| 152 | {Label: "~T~heme…", Action: a.ChooseTheme}, |
| 153 | {Label: "~L~ine numbers", Action: a.ToggleLineNumbers, Enabled: a.hasWindow}, |
| 154 | {Separator: true}, |
| 155 | {Label: "~C~reate project settings", Action: a.CreateProjectSettings, Enabled: not(a.HasProjectSettings)}, |
| 156 | {Label: "~P~roject settings…", Action: a.OpenProjectSettings, Enabled: a.HasProjectSettings}, |
| 157 | }} |
| 158 | } |
| 159 | |
| 160 | func (a *App) windowMenu() *ui.Menu { |
| 161 | return &ui.Menu{Label: "~W~indow", Items: []*ui.MenuItem{ |
| 162 | {Label: "~N~ext", Shortcut: "F6", Action: a.NextWindow, Enabled: a.hasWindow}, |
| 163 | {Separator: true}, |
| 164 | {Label: "New ~t~erminal", Shortcut: "F8", Action: a.NewTerminal}, |
| 165 | {Label: "~P~roject tree", Shortcut: "F9", Action: a.ProjectTree}, |
| 166 | {Separator: true}, |
| 167 | {Label: "~T~ile", Action: a.TileWindows, Enabled: a.hasWindow}, |
| 168 | {Label: "~C~ascade", Action: a.CascadeWindows, Enabled: a.hasWindow}, |
| 169 | {Label: "~M~aximise", Action: a.MaximizeWindow, Enabled: a.hasWindow}, |
| 170 | {Separator: true}, |
| 171 | {Label: "~L~ist…", Shortcut: "Alt-0", Action: a.ListWindows, Enabled: a.hasWindow}, |
| 172 | }} |
| 173 | } |
| 174 | |
| 175 | func (a *App) helpMenu() *ui.Menu { |
| 176 | return &ui.Menu{Label: "~H~elp", Items: []*ui.MenuItem{ |
| 177 | {Label: "~K~eyboard", Action: a.ShowKeyboardHelp}, |
| 178 | {Label: "~A~bout", Action: a.ShowAbout}, |
| 179 | }} |
| 180 | } |
| 181 | |
| 182 | // statusItems returns the hints along the bottom of the screen. They double as |
| 183 | // buttons: clicking one runs it. |
| 184 | func (a *App) statusItems() []ui.StatusItem { |
| 185 | return []ui.StatusItem{ |
| 186 | {Key: "F1", Label: "Describe", Action: a.DescribeSymbol}, |
| 187 | {Key: "F2", Label: "Save", Action: a.SaveFile}, |
| 188 | {Key: "F3", Label: "Open", Action: a.OpenFile}, |
| 189 | {Key: "F6", Label: "Window", Action: a.NextWindow}, |
| 190 | {Key: "F7", Label: "Next", Action: a.FindNext}, |
| 191 | {Key: "F10", Label: "Menu", Action: func() { a.menu.OpenMenu(0) }}, |
| 192 | {Key: "Alt-X", Label: "Exit", Action: a.Quit}, |
| 193 | } |
| 194 | } |