turbo-editors/turbo-corepublic Fork 0
main
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 main · k33g · 4h ago
placeholder.go · 183 lines · 5.9 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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
package tools

import (
	"fmt"
	"strings"
)

// A tool asks for a value by writing a placeholder into its command:
//
//	command = "go mod init {{module path}}"
//
// The text between the braces is what the editor asks for. A trailing "..."
// means the value is substituted verbatim rather than shell-quoted, which is
// how one placeholder can stand for several arguments:
//
//	command = "go test {{extra flags...}} ./..."
//
// **Double braces, not single.** Single ones appear in real commands — `awk
// '{print $1}'` and `find . -exec rm {} +` are both ordinary things to put in a
// tools file — and treating those as placeholders would turn a working command
// into a dialog asking for "print $1".
const (
	placeholderOpen  = "{{"
	placeholderClose = "}}"
	// rawSuffix inside a placeholder asks for the value verbatim.
	rawSuffix = "..."
)

// Placeholder is a value a tool asks for before it runs.
type Placeholder struct {
	// Label is what the editor asks for: the text between the braces, trimmed,
	// with any trailing "..." removed.
	Label string
	// Raw says the value is substituted verbatim rather than shell-quoted.
	//
	// A label written both ways in one command — `{{x}}` here and `{{x...}}`
	// there — is asked for once and substituted according to *each* occurrence,
	// so this reports what the first occurrence asked for.
	Raw bool
}

// Placeholders returns the values a tool's command asks for, in the order they
// first appear, with a label used twice reported once.
//
// A command with none gives nothing, which is what makes an ordinary tool run
// straight away rather than opening an empty box.
//
//	tools.Tool{Command: "go mod init {{module path}}"}.Placeholders()
//	// [{Label: "module path"}]
func (t Tool) Placeholders() []Placeholder {
	found, err := parsePlaceholders(t.Command)
	if err != nil {
		// Load refuses a command this cannot parse, so a Tool that came from a
		// file never reaches here with a broken one. A Tool built in code might;
		// asking for nothing is the safe answer, and running it unchanged shows
		// the braces rather than silently dropping them.
		return nil
	}

	var out []Placeholder
	seen := map[string]bool{}
	for _, p := range found {
		if seen[p.Label] {
			continue
		}
		seen[p.Label] = true
		out = append(out, Placeholder{Label: p.Label, Raw: p.Raw})
	}
	return out
}

// Fill returns the command with every placeholder replaced by the value given
// for its label.
//
// A value is **shell-quoted** unless its placeholder asked to be raw, because
// the result is handed to `sh -c` and a value with a space in it silently
// becoming two arguments is a bug nobody can see. A label with no value in the
// map is replaced by nothing, which lets a command report its own complaint
// rather than the editor guessing what an empty field meant.
//
//	tool := tools.Tool{Command: "go build -o {{output}}"}
//	tool.Fill(map[string]string{"output": "my binary"})
//	// go build -o 'my binary'
func (t Tool) Fill(values map[string]string) string {
	found, err := parsePlaceholders(t.Command)
	if err != nil {
		return t.Command
	}

	var b strings.Builder
	at := 0
	for _, p := range found {
		b.WriteString(t.Command[at:p.start])
		b.WriteString(substitute(values[p.Label], p.Raw))
		at = p.end
	}
	b.WriteString(t.Command[at:])
	return b.String()
}

// substitute renders one value into a command line.
func substitute(value string, raw bool) string {
	if raw {
		return value
	}
	return ShellQuote(value)
}

// ShellQuote wraps a string so that `sh -c` sees it as exactly one argument,
// whatever is in it.
//
// Single quotes are used because nothing inside them is special to the shell —
// no expansion, no escapes — so the only case to handle is a single quote in
// the value itself, which ends the quoting, adds an escaped quote, and starts
// it again.
//
//	tools.ShellQuote("it's here")  // 'it'\''s here'
func ShellQuote(value string) string {
	return "'" + strings.ReplaceAll(value, "'", `'\''`) + "'"
}

// placeholderAt is one occurrence in a command, and where it sits.
type placeholderAt struct {
	Label string
	Raw   bool
	start int // byte offset of the opening brace
	end   int // byte offset just past the closing brace
}

// parsePlaceholders finds every placeholder in a command, in order.
//
// It refuses a command it cannot make sense of rather than guessing: an opener
// with no closer is a half-typed placeholder, and a placeholder with no label
// is a box asking for nothing under a blank heading. Both are reported so the
// person who wrote the file finds out from the editor rather than from a
// command that ran with braces in it.
func parsePlaceholders(command string) ([]placeholderAt, error) {
	var out []placeholderAt
	at := 0

	for {
		open := strings.Index(command[at:], placeholderOpen)
		if open < 0 {
			return out, nil
		}
		open += at

		close := strings.Index(command[open:], placeholderClose)
		if close < 0 {
			return nil, fmt.Errorf("%q is never closed", command[open:])
		}
		close += open

		inside := command[open+len(placeholderOpen) : close]
		label, raw := splitRaw(inside)
		if label == "" {
			return nil, fmt.Errorf("%s%s%s asks for a value but does not say what it is",
				placeholderOpen, inside, placeholderClose)
		}

		out = append(out, placeholderAt{Label: label, Raw: raw, start: open, end: close + len(placeholderClose)})
		at = close + len(placeholderClose)
	}
}

// splitRaw separates a placeholder's label from the "..." that asks for it
// verbatim.
func splitRaw(inside string) (label string, raw bool) {
	trimmed := strings.TrimSpace(inside)
	if after, found := strings.CutSuffix(trimmed, rawSuffix); found {
		return strings.TrimSpace(after), true
	}
	return trimmed, false
}

// checkPlaceholders reports what is wrong with a tool's placeholders, naming
// the tool and the file so the message says where to go.
func checkPlaceholders(tool Tool, path string) error {
	if _, err := parsePlaceholders(tool.Command); err != nil {
		return fmt.Errorf("reading %s: tool %q: %w", path, tool.Name, err)
	}
	return nil
}