Repository navigation
Expand file tree
/
Copy pathcommand.go
More file actions
208 lines (188 loc) · 4.8 KB
/
Copy pathcommand.go
File metadata and controls
208 lines (188 loc) · 4.8 KB
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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
package main
import (
"fmt"
"strconv"
"strings"
tea "charm.land/bubbletea/v2"
"github.com/mattn/go-runewidth"
)
// command is a vim-like command line command, run as :name[!] [args].
type command struct {
// name is the vim-style name, the optional part in brackets: "w[rite]".
// Any prefix of the full name that includes the part before the
// brackets runs the command, so :w, :wr and :write all write.
name string
bang bool // whether the command accepts ! after its name
args string // argument placeholder for the help, e.g. "[file]"
desc string // one-line description for the help
// match overrides the name-based matching, e.g. for :<n>.
match func(name string) bool
run func(m *model, c call) tea.Cmd
}
// call is a parsed command line: :name[!] [arg].
type call struct {
name string // the name as typed
bang bool
arg string // the rest of the line, trimmed
}
// commands lists every command line command, in the order of the help.
var commands = []command{
{
name: "<n>",
desc: "goto line n",
match: isLineNumber,
run: func(m *model, c call) tea.Cmd {
line, _ := strconv.Atoi(c.name)
gotoLine(m, line)
return nil
},
},
{
name: "w[rite]",
bang: true,
args: "[file|!cmd]",
desc: "write to file or pipe to cmd, ! skips confirmation",
run: (*model).write,
},
{
name: "wq",
bang: true,
args: "[file|!cmd]",
desc: "write, then quit",
run: (*model).writeQuit,
},
{
name: "q[uit]",
bang: true,
desc: "quit",
run: func(m *model, c call) tea.Cmd {
return tea.Quit
},
},
}
// usage is the command as shown in the help: "w[rite][!] [file]".
func (c command) usage() string {
s := c.name
if c.bang {
s += "[!]"
}
if c.args != "" {
s += " " + c.args
}
return s
}
// matches reports whether name, as typed, runs the command.
func (c command) matches(name string) bool {
if c.match != nil {
return c.match(name)
}
short, full := splitName(c.name)
return len(name) >= len(short) && strings.HasPrefix(full, name)
}
// splitName splits "w[rite]" into the shortest ("w") and the full ("write")
// form of the name.
func splitName(name string) (short, full string) {
open := strings.IndexByte(name, '[')
if open < 0 {
return name, name
}
short = name[:open]
full = short + strings.TrimSuffix(name[open+1:], "]")
return short, full
}
func isLineNumber(name string) bool {
_, err := strconv.Atoi(name)
return err == nil
}
// parseCall splits a command line into its name, bang and argument. The name
// is the leading run of letters, or a number for :<n>.
func parseCall(line string) call {
line = strings.TrimSpace(line)
end := 0
for end < len(line) && isNameByte(line[end], end) {
end++
}
c := call{name: line[:end]}
rest := line[end:]
if strings.HasPrefix(rest, "!") {
c.bang = true
rest = rest[1:]
}
c.arg = strings.TrimSpace(rest)
return c
}
func isNameByte(b byte, pos int) bool {
switch {
case 'a' <= b && b <= 'z', 'A' <= b && b <= 'Z':
return true
case '0' <= b && b <= '9':
return true
case b == '-':
return pos == 0 // a negative line number
}
return false
}
// findCommand returns the command run by name, or nil if there is none.
func findCommand(name string) *command {
for i := range commands {
if commands[i].matches(name) {
return &commands[i]
}
}
return nil
}
// runCommand runs a command line entered after :.
func (m *model) runCommand(line string) (tea.Model, tea.Cmd) {
line = strings.TrimSpace(line)
if line == "" {
return m, nil
}
c := parseCall(line)
cmd := findCommand(c.name)
if cmd == nil {
return m, m.errorf("Not an editor command: %s", line)
}
if c.bang && !cmd.bang {
return m, m.errorf("No ! allowed: %s", line)
}
return m, cmd.run(m, c)
}
// confirmation is a yes/no question shown in the command line. y runs
// yes, any other key cancels.
type confirmation struct {
prompt string
yes func() tea.Cmd
}
// ask shows a yes/no question in place of the command line.
func (m *model) ask(prompt string, yes func() tea.Cmd) tea.Cmd {
m.confirm = &confirmation{prompt: prompt + " (y/n)", yes: yes}
return nil
}
func (m *model) handleConfirmKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
c := m.confirm
m.confirm = nil
if msg.Text == "y" || msg.Text == "Y" {
return m, c.yes()
}
return m, nil
}
// message shows text in the command line until the next key press.
type message struct {
text string
isErr bool
}
func (m *model) errorf(format string, args ...any) tea.Cmd {
m.message = &message{text: fmt.Sprintf(format, args...), isErr: true}
return nil
}
func (m *model) infof(format string, args ...any) tea.Cmd {
m.message = &message{text: fmt.Sprintf(format, args...)}
return nil
}
// clip cuts s to the terminal width, so the command line stays one row.
func (m *model) clip(s string) string {
if m.termWidth <= 0 {
return s
}
return runewidth.Truncate(s, m.termWidth, "…")
}