package app import "rickub.com/turbo-editors/turbo-core/ui" // buildMenus returns the editor's menu bar: File, Edit, Search, Run, Options, // Window and Help, laid out the way Turbo C laid its own out, with Snippets, // the editor's own toolchain menu, and whatever menus the project's tools file // asks for. // // Every item is wired straight to a method of the app, and the ones that only // make sense with a file open say so through Enabled, so the menu greys them // out instead of doing nothing when chosen. // // allMenus is where the order lives, because the bar is rebuilt from it every // time the tools file changes. func (a *App) buildMenus() *ui.MenuBar { return ui.NewMenuBar(a.allMenus()...) } // hasWindow reports whether there is a file open to act on. func (a *App) hasWindow() bool { return a.desktop.Active() != nil } func (a *App) fileMenu() *ui.Menu { return &ui.Menu{Label: "~F~ile", Items: []*ui.MenuItem{ {Label: "~N~ew", Shortcut: "F4", Action: a.NewFile}, {Label: "~O~pen…", Shortcut: "F3", Action: a.OpenFile}, {Label: "~S~ave", Shortcut: "F2", Action: a.SaveFile, Enabled: a.hasWindow}, {Label: "Save ~a~s…", Action: a.SaveFileAs, Enabled: a.hasWindow}, {Separator: true}, {Label: "~C~lose", Shortcut: "Ctrl-W", Action: a.CloseFile, Enabled: a.hasWindow}, {Separator: true}, {Label: "E~x~it", Shortcut: "Alt-X", Action: a.Quit}, }} } func (a *App) editMenu() *ui.Menu { return &ui.Menu{Label: "~E~dit", Items: []*ui.MenuItem{ {Label: "~U~ndo", Shortcut: "Ctrl-Z", Action: a.Undo, Enabled: a.hasWindow}, // Ctrl-R, not the Ctrl-Y it used to be: Ctrl-Y is Turbo C's // delete-line, and between an editor that looks like Turbo C and a // habit picked up here, the first has the better claim. Ctrl-Shift-Z // is not an option — a terminal delivers it as plain Ctrl-Z. {Label: "~R~edo", Shortcut: "Ctrl-R", Action: a.Redo, Enabled: a.hasWindow}, {Separator: true}, {Label: "~I~nsert line", Shortcut: "Ctrl-N", Action: a.InsertLine, Enabled: a.hasWindow}, {Label: "~D~elete line", Shortcut: "Ctrl-Y", Action: a.DeleteLine, Enabled: a.hasWindow}, {Separator: true}, {Label: "Cu~t~", Shortcut: "Shift-Del", Action: a.Cut, Enabled: a.hasWindow}, {Label: "~C~opy", Shortcut: "Ctrl-Ins", Action: a.Copy, Enabled: a.hasWindow}, {Label: "~P~aste", Shortcut: "Shift-Ins", Action: a.Paste, Enabled: a.hasWindow}, {Separator: true}, {Label: "Select ~a~ll", Shortcut: "Ctrl-A", Action: a.SelectAll, Enabled: a.hasWindow}, }} } func (a *App) searchMenu() *ui.Menu { return &ui.Menu{Label: "~S~earch", Items: []*ui.MenuItem{ {Label: "~F~ind…", Shortcut: "Ctrl-F", Action: a.Find, Enabled: a.hasWindow}, {Label: "Find ~n~ext", Shortcut: "F7", Action: a.FindNext, Enabled: a.hasWindow}, {Label: "Find ~p~revious", Shortcut: "Shift-F7", Action: a.FindPrevious, Enabled: a.hasWindow}, {Separator: true}, {Label: "~G~o to line…", Shortcut: "Ctrl-G", Action: a.GoToLine, Enabled: a.hasWindow}, }} } // agentMenu lists the coding agents the project's acp.toml knows about, one // window each. // // It is on the bar whether or not any agent is configured, because that is // where Create agents file has to be reachable from — the same reason the // toolchain menu stays fixed even in a project with no tools file. // // A file that cannot be read shows the error as a greyed line rather than an // empty menu: a silent drop looks exactly like having no agents. func (a *App) agentMenu() *ui.Menu { menu := &ui.Menu{Label: "~A~gent"} menu.OnOpen = func() { menu.Items = a.agentItems() } menu.Items = a.agentItems() return menu } // agentItems builds the menu's contents, which are read from the file every // time it drops down so that editing acp.toml never needs a restart. func (a *App) agentItems() []*ui.MenuItem { var items []*ui.MenuItem list, err := a.loadAgents() switch { case err != nil: items = append(items, &ui.MenuItem{Label: "(the agents file has an error)", Enabled: never}) case list.Len() == 0: items = append(items, &ui.MenuItem{Label: "(no agents configured)", Enabled: never}) default: for _, agent := range list.Agents() { name := agent.Name items = append(items, &ui.MenuItem{Label: name, Action: func() { a.NewAgent(name) }}) } } return append(items, &ui.MenuItem{Separator: true}, &ui.MenuItem{Label: "~C~ancel turn", Shortcut: "Esc", Action: a.CancelAgentTurn, Enabled: a.agentTurnRunning}, &ui.MenuItem{Label: "Create agents ~f~ile", Action: a.CreateAgentsFile, Enabled: a.hasNoAgentsFile}, &ui.MenuItem{Label: "Agent ~s~tatus", Action: a.AgentStatus}, ) } // never is the Enabled of a line that is a message rather than a command. func never() bool { return false } func (a *App) runMenu() *ui.Menu { return &ui.Menu{Label: "~R~un", Items: []*ui.MenuItem{ {Label: "~C~ompletion", Shortcut: "Ctrl-Space", Action: a.RequestCompletion, Enabled: a.hasWindow}, {Separator: true}, {Label: "~L~anguage server status", Action: a.ShowLanguageStatus}, }} } // codeMenu holds everything the editor asks the language server about the // symbol under the cursor. // // It exists because that work was split across two menus — Describe symbol in // Run, Go to definition in Search — and neither is where a reader looks for // "what else knows about this name". Both keep their keys: only where they are // listed has changed. // // Every item acts on the **cursor**, not on a selection. Almost every request // in the protocol takes a position rather than a range, so asking a user to // select something first would be an invented step that answers nothing extra. func (a *App) codeMenu() *ui.Menu { return &ui.Menu{Label: "~C~ode", Items: []*ui.MenuItem{ {Label: "~D~escribe symbol", Shortcut: "F1", Action: a.DescribeSymbol, Enabled: a.hasWindow}, {Separator: true}, {Label: "~G~o to definition", Shortcut: "F12", Action: a.GoToDefinition, Enabled: a.hasWindow}, {Label: "Go to ~t~ype definition", Action: a.GoToTypeDefinition, Enabled: a.hasWindow}, {Label: "Find ~i~mplementations…", Action: a.FindImplementations, Enabled: a.hasWindow}, {Label: "Find ~r~eferences…", Shortcut: "Shift-F12", Action: a.FindReferences, Enabled: a.hasWindow}, {Separator: true}, // Symbol in file has no key on purpose: the obvious one is Ctrl-Shift-O, // and a terminal cannot tell that from Ctrl-O — the shift is lost before // the editor sees it. A menu item that advertises a key which does // nothing is worse than one with no key at all. {Label: "Symbol in ~f~ile…", Action: a.SymbolInFile, Enabled: a.hasWindow}, {Label: "Symbol in ~p~roject…", Shortcut: "Ctrl-T", Action: a.SymbolInProject}, {Separator: true}, {Label: "Pro~b~lems…", Action: a.ShowProblems}, }} } func (a *App) optionsMenu() *ui.Menu { return &ui.Menu{Label: "~O~ptions", Items: []*ui.MenuItem{ {Label: "~T~heme…", Action: a.ChooseTheme}, {Label: "~L~ine numbers", Action: a.ToggleLineNumbers, Enabled: a.hasWindow}, {Separator: true}, {Label: "~C~reate project settings", Action: a.CreateProjectSettings, Enabled: not(a.HasProjectSettings)}, {Label: "~P~roject settings…", Action: a.OpenProjectSettings, Enabled: a.HasProjectSettings}, }} } func (a *App) windowMenu() *ui.Menu { return &ui.Menu{Label: "~W~indow", Items: []*ui.MenuItem{ {Label: "~N~ext", Shortcut: "F6", Action: a.NextWindow, Enabled: a.hasWindow}, {Separator: true}, {Label: "New ~t~erminal", Shortcut: "F8", Action: a.NewTerminal}, {Label: "~P~roject tree", Shortcut: "F9", Action: a.ProjectTree}, {Separator: true}, {Label: "~T~ile", Action: a.TileWindows, Enabled: a.hasWindow}, {Label: "~C~ascade", Action: a.CascadeWindows, Enabled: a.hasWindow}, {Label: "~M~aximise", Action: a.MaximizeWindow, Enabled: a.hasWindow}, {Separator: true}, {Label: "~L~ist…", Shortcut: "Alt-0", Action: a.ListWindows, Enabled: a.hasWindow}, }} } func (a *App) helpMenu() *ui.Menu { return &ui.Menu{Label: "~H~elp", Items: []*ui.MenuItem{ {Label: "~K~eyboard", Action: a.ShowKeyboardHelp}, {Label: "~A~bout", Action: a.ShowAbout}, }} } // statusItems returns the hints along the bottom of the screen. They double as // buttons: clicking one runs it. func (a *App) statusItems() []ui.StatusItem { return []ui.StatusItem{ {Key: "F1", Label: "Describe", Action: a.DescribeSymbol}, {Key: "F2", Label: "Save", Action: a.SaveFile}, {Key: "F3", Label: "Open", Action: a.OpenFile}, {Key: "F6", Label: "Window", Action: a.NextWindow}, {Key: "F7", Label: "Next", Action: a.FindNext}, {Key: "F10", Label: "Menu", Action: func() { a.menu.OpenMenu(0) }}, {Key: "Alt-X", Label: "Exit", Action: a.Quit}, } }