turbo-editors/turbo-corepublic Fork 0
v0.9.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

🛟 Updated. 28d5985 · on v0.9.0 · k33g · 17h ago
snippets.go · 140 lines · 4.7 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
// The Snippets menu: reusable pieces of text, read from a TOML file and
// inserted at the cursor.

package app

import (
	"os"

	"codeberg.org/turbo-editors/turbo-core/snippets"
	"codeberg.org/turbo-editors/turbo-core/syntax"
	"codeberg.org/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)
}