]> git.lizzy.rs Git - micro.git/blob - cmd/micro/command.go
d1ef2c535ea7e8e77ac36abb5cbd8049da153577
[micro.git] / cmd / micro / command.go
1 package main
2
3 import (
4         "bytes"
5         "io"
6         "io/ioutil"
7         "os"
8         "os/exec"
9         "os/signal"
10         "regexp"
11         "strings"
12
13         "github.com/mitchellh/go-homedir"
14 )
15
16 type Command struct {
17         action      func([]string)
18         completions []Completion
19 }
20
21 type StrCommand struct {
22         action      string
23         completions []Completion
24 }
25
26 var commands map[string]Command
27
28 var commandActions = map[string]func([]string){
29         "Set":       Set,
30         "SetLocal":  SetLocal,
31         "Show":      Show,
32         "Run":       Run,
33         "Bind":      Bind,
34         "Quit":      Quit,
35         "Save":      Save,
36         "Replace":   Replace,
37         "VSplit":    VSplit,
38         "HSplit":    HSplit,
39         "Tab":       NewTab,
40         "Help":      Help,
41         "Eval":      Eval,
42         "ToggleLog": ToggleLog,
43 }
44
45 // InitCommands initializes the default commands
46 func InitCommands() {
47         commands = make(map[string]Command)
48
49         defaults := DefaultCommands()
50         parseCommands(defaults)
51 }
52
53 func parseCommands(userCommands map[string]StrCommand) {
54         for k, v := range userCommands {
55                 MakeCommand(k, v.action, v.completions...)
56         }
57 }
58
59 // MakeCommand is a function to easily create new commands
60 // This can be called by plugins in Lua so that plugins can define their own commands
61 func MakeCommand(name, function string, completions ...Completion) {
62         action := commandActions[function]
63         if _, ok := commandActions[function]; !ok {
64                 // If the user seems to be binding a function that doesn't exist
65                 // We hope that it's a lua function that exists and bind it to that
66                 action = LuaFunctionCommand(function)
67         }
68
69         commands[name] = Command{action, completions}
70 }
71
72 // DefaultCommands returns a map containing micro's default commands
73 func DefaultCommands() map[string]StrCommand {
74         return map[string]StrCommand{
75                 "set":      {"Set", []Completion{OptionCompletion, NoCompletion}},
76                 "setlocal": {"SetLocal", []Completion{OptionCompletion, NoCompletion}},
77                 "show":     {"Show", []Completion{OptionCompletion, NoCompletion}},
78                 "bind":     {"Bind", []Completion{NoCompletion}},
79                 "run":      {"Run", []Completion{NoCompletion}},
80                 "quit":     {"Quit", []Completion{NoCompletion}},
81                 "save":     {"Save", []Completion{NoCompletion}},
82                 "replace":  {"Replace", []Completion{NoCompletion}},
83                 "vsplit":   {"VSplit", []Completion{FileCompletion, NoCompletion}},
84                 "hsplit":   {"HSplit", []Completion{FileCompletion, NoCompletion}},
85                 "tab":      {"Tab", []Completion{FileCompletion, NoCompletion}},
86                 "help":     {"Help", []Completion{HelpCompletion, NoCompletion}},
87                 "eval":     {"Eval", []Completion{NoCompletion}},
88                 "log":      {"ToggleLog", []Completion{NoCompletion}},
89         }
90 }
91
92 func ToggleLog(args []string) {
93         buffer := messenger.getBuffer()
94         if CurView().Type != vtLog {
95                 CurView().VSplit(buffer)
96                 CurView().Type = vtLog
97         } else {
98                 CurView().Quit(true)
99         }
100 }
101
102 // Help tries to open the given help page in a horizontal split
103 func Help(args []string) {
104         if len(args) < 1 {
105                 // Open the default help if the user just typed "> help"
106                 CurView().openHelp("help")
107         } else {
108                 helpPage := args[0]
109                 if FindRuntimeFile(RTHelp, helpPage) != nil {
110                         CurView().openHelp(helpPage)
111                 } else {
112                         messenger.Error("Sorry, no help for ", helpPage)
113                 }
114         }
115 }
116
117 // VSplit opens a vertical split with file given in the first argument
118 // If no file is given, it opens an empty buffer in a new split
119 func VSplit(args []string) {
120         if len(args) == 0 {
121                 CurView().VSplit(NewBuffer([]byte{}, ""))
122         } else {
123                 filename := args[0]
124                 home, _ := homedir.Dir()
125                 filename = strings.Replace(filename, "~", home, 1)
126                 file, err := ioutil.ReadFile(filename)
127
128                 var buf *Buffer
129                 if err != nil {
130                         // File does not exist -- create an empty buffer with that name
131                         buf = NewBuffer([]byte{}, filename)
132                 } else {
133                         buf = NewBuffer(file, filename)
134                 }
135                 CurView().VSplit(buf)
136         }
137 }
138
139 // HSplit opens a horizontal split with file given in the first argument
140 // If no file is given, it opens an empty buffer in a new split
141 func HSplit(args []string) {
142         if len(args) == 0 {
143                 CurView().HSplit(NewBuffer([]byte{}, ""))
144         } else {
145                 filename := args[0]
146                 home, _ := homedir.Dir()
147                 filename = strings.Replace(filename, "~", home, 1)
148                 file, err := ioutil.ReadFile(filename)
149
150                 var buf *Buffer
151                 if err != nil {
152                         // File does not exist -- create an empty buffer with that name
153                         buf = NewBuffer([]byte{}, filename)
154                 } else {
155                         buf = NewBuffer(file, filename)
156                 }
157                 CurView().HSplit(buf)
158         }
159 }
160
161 // Eval evaluates a lua expression
162 func Eval(args []string) {
163         if len(args) >= 1 {
164                 err := L.DoString(args[0])
165                 if err != nil {
166                         messenger.Error(err)
167                 }
168         } else {
169                 messenger.Error("Not enough arguments")
170         }
171 }
172
173 // NewTab opens the given file in a new tab
174 func NewTab(args []string) {
175         if len(args) == 0 {
176                 CurView().AddTab(true)
177         } else {
178                 filename := args[0]
179                 home, _ := homedir.Dir()
180                 filename = strings.Replace(filename, "~", home, 1)
181                 file, _ := ioutil.ReadFile(filename)
182
183                 tab := NewTabFromView(NewView(NewBuffer(file, filename)))
184                 tab.SetNum(len(tabs))
185                 tabs = append(tabs, tab)
186                 curTab++
187                 if len(tabs) == 2 {
188                         for _, t := range tabs {
189                                 for _, v := range t.views {
190                                         v.ToggleTabbar()
191                                 }
192                         }
193                 }
194         }
195 }
196
197 // Set sets an option
198 func Set(args []string) {
199         if len(args) < 2 {
200                 messenger.Error("Not enough arguments")
201                 return
202         }
203
204         option := strings.TrimSpace(args[0])
205         value := strings.TrimSpace(args[1])
206
207         SetOptionAndSettings(option, value)
208 }
209
210 // SetLocal sets an option local to the buffer
211 func SetLocal(args []string) {
212         if len(args) < 2 {
213                 messenger.Error("Not enough arguments")
214                 return
215         }
216
217         option := strings.TrimSpace(args[0])
218         value := strings.TrimSpace(args[1])
219
220         err := SetLocalOption(option, value, CurView())
221         if err != nil {
222                 messenger.Error(err.Error())
223         }
224 }
225
226 // Show shows the value of the given option
227 func Show(args []string) {
228         if len(args) < 1 {
229                 messenger.Error("Please provide an option to show")
230                 return
231         }
232
233         option := GetOption(args[0])
234
235         if option == nil {
236                 messenger.Error(args[0], " is not a valid option")
237                 return
238         }
239
240         messenger.Message(option)
241 }
242
243 // Bind creates a new keybinding
244 func Bind(args []string) {
245         if len(args) < 2 {
246                 messenger.Error("Not enough arguments")
247                 return
248         }
249         BindKey(args[0], args[1])
250 }
251
252 // Run runs a shell command in the background
253 func Run(args []string) {
254         // Run a shell command in the background (openTerm is false)
255         HandleShellCommand(JoinCommandArgs(args...), false, true)
256 }
257
258 // Quit closes the main view
259 func Quit(args []string) {
260         // Close the main view
261         CurView().Quit(true)
262 }
263
264 // Save saves the buffer in the main view
265 func Save(args []string) {
266         if len(args) == 0 {
267                 // Save the main view
268                 CurView().Save(true)
269         } else {
270                 CurView().Buf.SaveAs(args[0])
271         }
272 }
273
274 // Replace runs search and replace
275 func Replace(args []string) {
276         if len(args) < 2 {
277                 // We need to find both a search and replace expression
278                 messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
279                 return
280         }
281
282         var flags string
283         if len(args) == 3 {
284                 // The user included some flags
285                 flags = args[2]
286         }
287
288         search := string(args[0])
289         replace := string(args[1])
290
291         regex, err := regexp.Compile(search)
292         if err != nil {
293                 // There was an error with the user's regex
294                 messenger.Error(err.Error())
295                 return
296         }
297
298         view := CurView()
299
300         found := 0
301         if strings.Contains(flags, "c") {
302                 for {
303                         // The 'check' flag was used
304                         Search(search, view, true)
305                         if !view.Cursor.HasSelection() {
306                                 break
307                         }
308                         view.Relocate()
309                         if view.Buf.Settings["syntax"].(bool) {
310                                 view.matches = Match(view)
311                         }
312                         RedrawAll()
313                         choice, canceled := messenger.YesNoPrompt("Perform replacement? (y,n)")
314                         if canceled {
315                                 if view.Cursor.HasSelection() {
316                                         view.Cursor.Loc = view.Cursor.CurSelection[0]
317                                         view.Cursor.ResetSelection()
318                                 }
319                                 messenger.Reset()
320                                 break
321                         }
322                         if choice {
323                                 view.Cursor.DeleteSelection()
324                                 view.Buf.Insert(view.Cursor.Loc, replace)
325                                 view.Cursor.ResetSelection()
326                                 messenger.Reset()
327                                 found++
328                         } else {
329                                 if view.Cursor.HasSelection() {
330                                         searchStart = ToCharPos(view.Cursor.CurSelection[1], view.Buf)
331                                 } else {
332                                         searchStart = ToCharPos(view.Cursor.Loc, view.Buf)
333                                 }
334                                 continue
335                         }
336                 }
337         } else {
338                 matches := regex.FindAllStringIndex(view.Buf.String(), -1)
339                 if matches != nil && len(matches) > 0 {
340                         adjust := 0
341                         prevMatch := matches[0]
342                         from := FromCharPos(prevMatch[0], view.Buf)
343                         to := from.Move(Count(search), view.Buf)
344                         adjust += Count(replace) - Count(search)
345                         view.Buf.Replace(from, to, replace)
346                         if len(matches) > 1 {
347                                 for _, match := range matches[1:] {
348                                         found++
349                                         from = from.Move(match[0]-prevMatch[0]+adjust, view.Buf)
350                                         to := from.Move(Count(search), view.Buf)
351                                         // TermMessage(match[0], " ", prevMatch[0], " ", adjust, "\n", from, " ", to)
352                                         view.Buf.Replace(from, to, replace)
353                                         prevMatch = match
354                                         // adjust += Count(replace) - Count(search)
355                                 }
356                         }
357                 }
358         }
359         view.Cursor.Relocate()
360
361         if found > 1 {
362                 messenger.Message("Replaced ", found, " occurrences of ", search)
363         } else if found == 1 {
364                 messenger.Message("Replaced ", found, " occurrence of ", search)
365         } else {
366                 messenger.Message("Nothing matched ", search)
367         }
368 }
369
370 // RunShellCommand executes a shell command and returns the output/error
371 func RunShellCommand(input string) (string, error) {
372         inputCmd := SplitCommandArgs(input)[0]
373         args := SplitCommandArgs(input)[1:]
374
375         cmd := exec.Command(inputCmd, args...)
376         outputBytes := &bytes.Buffer{}
377         cmd.Stdout = outputBytes
378         cmd.Stderr = outputBytes
379         cmd.Start()
380         err := cmd.Wait() // wait for command to finish
381         outstring := outputBytes.String()
382         return outstring, err
383 }
384
385 // HandleShellCommand runs the shell command
386 // The openTerm argument specifies whether a terminal should be opened (for viewing output
387 // or interacting with stdin)
388 func HandleShellCommand(input string, openTerm bool, waitToFinish bool) string {
389         inputCmd := SplitCommandArgs(input)[0]
390         if !openTerm {
391                 // Simply run the command in the background and notify the user when it's done
392                 messenger.Message("Running...")
393                 go func() {
394                         output, err := RunShellCommand(input)
395                         totalLines := strings.Split(output, "\n")
396
397                         if len(totalLines) < 3 {
398                                 if err == nil {
399                                         messenger.Message(inputCmd, " exited without error")
400                                 } else {
401                                         messenger.Message(inputCmd, " exited with error: ", err, ": ", output)
402                                 }
403                         } else {
404                                 messenger.Message(output)
405                         }
406                         // We have to make sure to redraw
407                         RedrawAll()
408                 }()
409         } else {
410                 // Shut down the screen because we're going to interact directly with the shell
411                 screen.Fini()
412                 screen = nil
413
414                 args := SplitCommandArgs(input)[1:]
415
416                 // Set up everything for the command
417                 var outputBuf bytes.Buffer
418                 cmd := exec.Command(inputCmd, args...)
419                 cmd.Stdin = os.Stdin
420                 cmd.Stdout = io.MultiWriter(os.Stdout, &outputBuf)
421                 cmd.Stderr = os.Stderr
422
423                 // This is a trap for Ctrl-C so that it doesn't kill micro
424                 // Instead we trap Ctrl-C to kill the program we're running
425                 c := make(chan os.Signal, 1)
426                 signal.Notify(c, os.Interrupt)
427                 go func() {
428                         for range c {
429                                 cmd.Process.Kill()
430                         }
431                 }()
432
433                 cmd.Start()
434                 err := cmd.Wait()
435
436                 output := outputBuf.String()
437                 if err != nil {
438                         output = err.Error()
439                 }
440
441                 if waitToFinish {
442                         // This is just so we don't return right away and let the user press enter to return
443                         TermMessage("")
444                 }
445
446                 // Start the screen back up
447                 InitScreen()
448
449                 return output
450         }
451         return ""
452 }
453
454 // HandleCommand handles input from the user
455 func HandleCommand(input string) {
456         args := SplitCommandArgs(input)
457         inputCmd := args[0]
458
459         if _, ok := commands[inputCmd]; !ok {
460                 messenger.Error("Unknown command ", inputCmd)
461         } else {
462                 commands[inputCmd].action(args[1:])
463         }
464 }