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