// The Snippets menu: reusable pieces of text, read from a TOML file and // inserted at the cursor. package app import ( "os" "rickub.com/turbo-editors/turbo-core/snippets" "rickub.com/turbo-editors/turbo-core/syntax" "rickub.com/turbo-editors/turbo-core/ui" ) // createSnippetsLabel is the item that writes a starter file, kept at the // bottom of the menu so the groups above it read as the content. const createSnippetsLabel = "~C~reate snippets file" // openSnippetsLabel is its partner, and opens the *project's* file — the one // the item above it writes. const openSnippetsLabel = "~O~pen snippets file" // snippetsMenu returns the Snippets menu. // // Its items are filled in by OnOpen rather than here: they come from a file // that can change while the editor runs, and they are filtered by what the // front window holds, so there is nothing to decide at start-up. // // The menu is always on the bar, even when the project has no snippets file at // all — otherwise the item that creates one would be unreachable and the // feature could not be started. func (a *App) snippetsMenu() *ui.Menu { // The hot key is N, not S: Search already owns S, and the bar answers the // first match it finds. TestNoTwoMenusShareAHotKey holds the line. menu := &ui.Menu{Label: "S~n~ippets"} menu.OnOpen = func() { menu.Items = a.snippetItems() } return menu } // snippetItems builds the menu's contents: one submenu per group, then the // item that creates a starter file. func (a *App) snippetItems() []*ui.MenuItem { items := a.snippetGroupItems() if len(items) > 0 { items = append(items, &ui.MenuItem{Separator: true}) } return append(items, &ui.MenuItem{Label: createSnippetsLabel, Action: a.CreateSnippets, Enabled: not(a.HasProjectSnippets)}, &ui.MenuItem{Label: openSnippetsLabel, Action: a.OpenSnippets, Enabled: a.HasProjectSnippets}, ) } // snippetGroupItems turns the snippets that apply to the front window into one // submenu per group. // // A file that cannot be read gives one disabled line saying so, rather than an // empty menu: a typo in the file should be visible where the snippets were // expected, not silent. func (a *App) snippetGroupItems() []*ui.MenuItem { list, err := a.loadSnippets() if err != nil { return []*ui.MenuItem{{Label: "Cannot read snippets", Enabled: func() bool { return false }}} } var items []*ui.MenuItem for _, group := range list.Groups(a.snippetLanguage()) { items = append(items, &ui.MenuItem{ Label: group.Name, Items: snippetSubmenu(a, group.Snippets), }) } return items } // snippetSubmenu turns one group's snippets into the lines of a submenu. func snippetSubmenu(a *App, list []snippets.Snippet) []*ui.MenuItem { items := make([]*ui.MenuItem, 0, len(list)) for _, snippet := range list { body := snippet.Body // captured per iteration, not read back from the loop items = append(items, &ui.MenuItem{ Label: snippet.Name, Action: func() { a.InsertSnippet(body) }, Enabled: a.hasWindow, }) } return items } // loadSnippets reads the project's snippets and the user's own. func (a *App) loadSnippets() (snippets.List, error) { working, err := os.Getwd() if err != nil { return snippets.List{}, err } return snippets.Load(a.profile, working) } // snippetLanguage returns the language the menu should be filtered for: the one // the front window holds. // // A window that is not a file — a terminal, the project tree — and a file whose // language is not recognised both give the empty name, which offers every // snippet that names no language of its own. func (a *App) snippetLanguage() string { view := a.activeView() if view == nil { return "" } language := syntax.LanguageOf(view.Buffer().Path(), view.Buffer().Line(0)) if language == syntax.LanguageNone { return "" } return language.String() } // InsertSnippet puts a snippet's text into the front window at the cursor. // // A window that is not a file is left alone: there is nothing to insert into, // and the menu items are disabled in that case anyway. func (a *App) InsertSnippet(body string) { view := a.activeView() if view == nil { return } view.InsertSnippet(body) a.completion.Hide() a.Message("Snippet inserted") } // CreateSnippets writes a starter snippets file for the project and opens it. // // The file is opened rather than merely written because a snippets file is only // useful once it has been read: the examples in it are how the format is // learnt. A project that already has one is opened unchanged, which is what // someone choosing this twice almost certainly wanted. func (a *App) CreateSnippets() { create := func(project string) (string, error) { return snippets.Create(a.profile, project) } a.createProjectFile("Snippets", create, snippets.ErrExists) }