]> git.lizzy.rs Git - micro.git/blob - cmd/micro/command.go
2a848884d411923e4a4d2dedcae0afa63b8bde49
[micro.git] / cmd / micro / command.go
1 package main
2
3 import (
4         "bytes"
5         "fmt"
6         "os"
7         "os/exec"
8         "os/signal"
9         "path/filepath"
10         "regexp"
11         "runtime"
12         "strconv"
13         "strings"
14
15         humanize "github.com/dustin/go-humanize"
16 )
17
18 // A Command contains a action (a function to call) as well as information about how to autocomplete the command
19 type Command struct {
20         action      func([]string)
21         completions []Completion
22 }
23
24 // A StrCommand is similar to a command but keeps the name of the action
25 type StrCommand struct {
26         action      string
27         completions []Completion
28 }
29
30 var commands map[string]Command
31
32 var commandActions map[string]func([]string)
33
34 func init() {
35         commandActions = map[string]func([]string){
36                 "Set":        Set,
37                 "SetLocal":   SetLocal,
38                 "Show":       Show,
39                 "Run":        Run,
40                 "Bind":       Bind,
41                 "Quit":       Quit,
42                 "Save":       Save,
43                 "Replace":    Replace,
44                 "ReplaceAll": ReplaceAll,
45                 "VSplit":     VSplit,
46                 "HSplit":     HSplit,
47                 "Tab":        NewTab,
48                 "Help":       Help,
49                 "Eval":       Eval,
50                 "ToggleLog":  ToggleLog,
51                 "Plugin":     PluginCmd,
52                 "Reload":     Reload,
53                 "Cd":         Cd,
54                 "Pwd":        Pwd,
55                 "Open":       Open,
56                 "TabSwitch":  TabSwitch,
57                 "MemUsage":   MemUsage,
58                 "Retab":      Retab,
59         }
60 }
61
62 // InitCommands initializes the default commands
63 func InitCommands() {
64         commands = make(map[string]Command)
65
66         defaults := DefaultCommands()
67         parseCommands(defaults)
68 }
69
70 func parseCommands(userCommands map[string]StrCommand) {
71         for k, v := range userCommands {
72                 MakeCommand(k, v.action, v.completions...)
73         }
74 }
75
76 // MakeCommand is a function to easily create new commands
77 // This can be called by plugins in Lua so that plugins can define their own commands
78 func MakeCommand(name, function string, completions ...Completion) {
79         action := commandActions[function]
80         if _, ok := commandActions[function]; !ok {
81                 // If the user seems to be binding a function that doesn't exist
82                 // We hope that it's a lua function that exists and bind it to that
83                 action = LuaFunctionCommand(function)
84         }
85
86         commands[name] = Command{action, completions}
87 }
88
89 // DefaultCommands returns a map containing micro's default commands
90 func DefaultCommands() map[string]StrCommand {
91         return map[string]StrCommand{
92                 "set":        {"Set", []Completion{OptionCompletion, OptionValueCompletion}},
93                 "setlocal":   {"SetLocal", []Completion{OptionCompletion, OptionValueCompletion}},
94                 "show":       {"Show", []Completion{OptionCompletion, NoCompletion}},
95                 "bind":       {"Bind", []Completion{NoCompletion}},
96                 "run":        {"Run", []Completion{NoCompletion}},
97                 "quit":       {"Quit", []Completion{NoCompletion}},
98                 "save":       {"Save", []Completion{NoCompletion}},
99                 "replace":    {"Replace", []Completion{NoCompletion}},
100                 "replaceall": {"ReplaceAll", []Completion{NoCompletion}},
101                 "vsplit":     {"VSplit", []Completion{FileCompletion, NoCompletion}},
102                 "hsplit":     {"HSplit", []Completion{FileCompletion, NoCompletion}},
103                 "tab":        {"Tab", []Completion{FileCompletion, NoCompletion}},
104                 "help":       {"Help", []Completion{HelpCompletion, NoCompletion}},
105                 "eval":       {"Eval", []Completion{NoCompletion}},
106                 "log":        {"ToggleLog", []Completion{NoCompletion}},
107                 "plugin":     {"Plugin", []Completion{PluginCmdCompletion, PluginNameCompletion}},
108                 "reload":     {"Reload", []Completion{NoCompletion}},
109                 "cd":         {"Cd", []Completion{FileCompletion}},
110                 "pwd":        {"Pwd", []Completion{NoCompletion}},
111                 "open":       {"Open", []Completion{FileCompletion}},
112                 "tabswitch":  {"TabSwitch", []Completion{NoCompletion}},
113                 "memusage":   {"MemUsage", []Completion{NoCompletion}},
114                 "retab":      {"Retab", []Completion{NoCompletion}},
115         }
116 }
117
118 // PluginCmd installs, removes, updates, lists, or searches for given plugins
119 func PluginCmd(args []string) {
120         if len(args) >= 1 {
121                 switch args[0] {
122                 case "install":
123                         installedVersions := GetInstalledVersions(false)
124                         for _, plugin := range args[1:] {
125                                 pp := GetAllPluginPackages().Get(plugin)
126                                 if pp == nil {
127                                         messenger.Error("Unknown plugin \"" + plugin + "\"")
128                                 } else if err := pp.IsInstallable(); err != nil {
129                                         messenger.Error("Error installing ", plugin, ": ", err)
130                                 } else {
131                                         for _, installed := range installedVersions {
132                                                 if pp.Name == installed.pack.Name {
133                                                         if pp.Versions[0].Version.Compare(installed.Version) == 1 {
134                                                                 messenger.Error(pp.Name, " is already installed but out-of-date: use 'plugin update ", pp.Name, "' to update")
135                                                         } else {
136                                                                 messenger.Error(pp.Name, " is already installed")
137                                                         }
138                                                 }
139                                         }
140                                         pp.Install()
141                                 }
142                         }
143                 case "remove":
144                         removed := ""
145                         for _, plugin := range args[1:] {
146                                 // check if the plugin exists.
147                                 if _, ok := loadedPlugins[plugin]; ok {
148                                         UninstallPlugin(plugin)
149                                         removed += plugin + " "
150                                         continue
151                                 }
152                         }
153                         if !IsSpaces(removed) {
154                                 messenger.Message("Removed ", removed)
155                         } else {
156                                 messenger.Error("The requested plugins do not exist")
157                         }
158                 case "update":
159                         UpdatePlugins(args[1:])
160                 case "list":
161                         plugins := GetInstalledVersions(false)
162                         messenger.AddLog("----------------")
163                         messenger.AddLog("The following plugins are currently installed:\n")
164                         for _, p := range plugins {
165                                 messenger.AddLog(fmt.Sprintf("%s (%s)", p.pack.Name, p.Version))
166                         }
167                         messenger.AddLog("----------------")
168                         if len(plugins) > 0 {
169                                 if CurView().Type != vtLog {
170                                         ToggleLog([]string{})
171                                 }
172                         }
173                 case "search":
174                         plugins := SearchPlugin(args[1:])
175                         messenger.Message(len(plugins), " plugins found")
176                         for _, p := range plugins {
177                                 messenger.AddLog("----------------")
178                                 messenger.AddLog(p.String())
179                         }
180                         messenger.AddLog("----------------")
181                         if len(plugins) > 0 {
182                                 if CurView().Type != vtLog {
183                                         ToggleLog([]string{})
184                                 }
185                         }
186                 case "available":
187                         packages := GetAllPluginPackages()
188                         messenger.AddLog("Available Plugins:")
189                         for _, pkg := range packages {
190                                 messenger.AddLog(pkg.Name)
191                         }
192                         if CurView().Type != vtLog {
193                                 ToggleLog([]string{})
194                         }
195                 }
196         } else {
197                 messenger.Error("Not enough arguments")
198         }
199 }
200
201 func Retab(args []string) {
202         CurView().Retab(true)
203 }
204
205 // TabSwitch switches to a given tab either by name or by number
206 func TabSwitch(args []string) {
207         if len(args) > 0 {
208                 num, err := strconv.Atoi(args[0])
209                 if err != nil {
210                         // Check for tab with this name
211
212                         found := false
213                         for _, t := range tabs {
214                                 v := t.views[t.CurView]
215                                 if v.Buf.GetName() == args[0] {
216                                         curTab = v.TabNum
217                                         found = true
218                                 }
219                         }
220                         if !found {
221                                 messenger.Error("Could not find tab: ", err)
222                         }
223                 } else {
224                         num--
225                         if num >= 0 && num < len(tabs) {
226                                 curTab = num
227                         } else {
228                                 messenger.Error("Invalid tab index")
229                         }
230                 }
231         }
232 }
233
234 // Cd changes the current working directory
235 func Cd(args []string) {
236         if len(args) > 0 {
237                 path := ReplaceHome(args[0])
238                 err := os.Chdir(path)
239                 if err != nil {
240                         messenger.Error("Error with cd: ", err)
241                         return
242                 }
243                 wd, _ := os.Getwd()
244                 for _, tab := range tabs {
245                         for _, view := range tab.views {
246                                 if len(view.Buf.name) == 0 {
247                                         continue
248                                 }
249
250                                 view.Buf.Path, _ = MakeRelative(view.Buf.AbsPath, wd)
251                                 if p, _ := filepath.Abs(view.Buf.Path); !strings.Contains(p, wd) {
252                                         view.Buf.Path = view.Buf.AbsPath
253                                 }
254                         }
255                 }
256         }
257 }
258
259 // MemUsage prints micro's memory usage
260 // Alloc shows how many bytes are currently in use
261 // Sys shows how many bytes have been requested from the operating system
262 // NumGC shows how many times the GC has been run
263 // Note that Go commonly reserves more memory from the OS than is currently in-use/required
264 // Additionally, even if Go returns memory to the OS, the OS does not always claim it because
265 // there may be plenty of memory to spare
266 func MemUsage(args []string) {
267         var mem runtime.MemStats
268         runtime.ReadMemStats(&mem)
269
270         messenger.Message(fmt.Sprintf("Alloc: %v, Sys: %v, NumGC: %v", humanize.Bytes(mem.Alloc), humanize.Bytes(mem.Sys), mem.NumGC))
271 }
272
273 // Pwd prints the current working directory
274 func Pwd(args []string) {
275         wd, err := os.Getwd()
276         if err != nil {
277                 messenger.Message(err.Error())
278         } else {
279                 messenger.Message(wd)
280         }
281 }
282
283 // Open opens a new buffer with a given filename
284 func Open(args []string) {
285         if len(args) > 0 {
286                 filename := args[0]
287                 // the filename might or might not be quoted, so unquote first then join the strings.
288                 args, err := SplitCommandArgs(filename)
289                 if err != nil {
290                         messenger.Error("Error parsing args ", err)
291                         return
292                 }
293                 filename = strings.Join(args, " ")
294
295                 CurView().Open(filename)
296         } else {
297                 messenger.Error("No filename")
298         }
299 }
300
301 // ToggleLog toggles the log view
302 func ToggleLog(args []string) {
303         buffer := messenger.getBuffer()
304         if CurView().Type != vtLog {
305                 CurView().HSplit(buffer)
306                 CurView().Type = vtLog
307                 RedrawAll()
308                 buffer.Cursor.Loc = buffer.Start()
309                 CurView().Relocate()
310                 buffer.Cursor.Loc = buffer.End()
311                 CurView().Relocate()
312         } else {
313                 CurView().Quit(true)
314         }
315 }
316
317 // Reload reloads all files (syntax files, colorschemes...)
318 func Reload(args []string) {
319         LoadAll()
320 }
321
322 // Help tries to open the given help page in a horizontal split
323 func Help(args []string) {
324         if len(args) < 1 {
325                 // Open the default help if the user just typed "> help"
326                 CurView().openHelp("help")
327         } else {
328                 helpPage := args[0]
329                 if FindRuntimeFile(RTHelp, helpPage) != nil {
330                         CurView().openHelp(helpPage)
331                 } else {
332                         messenger.Error("Sorry, no help for ", helpPage)
333                 }
334         }
335 }
336
337 // VSplit opens a vertical split with file given in the first argument
338 // If no file is given, it opens an empty buffer in a new split
339 func VSplit(args []string) {
340         if len(args) == 0 {
341                 CurView().VSplit(NewBufferFromString("", ""))
342         } else {
343                 filename := args[0]
344                 filename = ReplaceHome(filename)
345                 file, err := os.Open(filename)
346                 fileInfo, _ := os.Stat(filename)
347
348                 if err == nil && fileInfo.IsDir() {
349                         messenger.Error(filename, " is a directory")
350                         return
351                 }
352
353                 defer file.Close()
354
355                 var buf *Buffer
356                 if err != nil {
357                         // File does not exist -- create an empty buffer with that name
358                         buf = NewBufferFromString("", filename)
359                 } else {
360                         buf = NewBuffer(file, FSize(file), filename)
361                 }
362                 CurView().VSplit(buf)
363         }
364 }
365
366 // HSplit opens a horizontal split with file given in the first argument
367 // If no file is given, it opens an empty buffer in a new split
368 func HSplit(args []string) {
369         if len(args) == 0 {
370                 CurView().HSplit(NewBufferFromString("", ""))
371         } else {
372                 filename := args[0]
373                 filename = ReplaceHome(filename)
374                 file, err := os.Open(filename)
375                 fileInfo, _ := os.Stat(filename)
376
377                 if err == nil && fileInfo.IsDir() {
378                         messenger.Error(filename, " is a directory")
379                         return
380                 }
381
382                 defer file.Close()
383
384                 var buf *Buffer
385                 if err != nil {
386                         // File does not exist -- create an empty buffer with that name
387                         buf = NewBufferFromString("", filename)
388                 } else {
389                         buf = NewBuffer(file, FSize(file), filename)
390                 }
391                 CurView().HSplit(buf)
392         }
393 }
394
395 // Eval evaluates a lua expression
396 func Eval(args []string) {
397         if len(args) >= 1 {
398                 err := L.DoString(args[0])
399                 if err != nil {
400                         messenger.Error(err)
401                 }
402         } else {
403                 messenger.Error("Not enough arguments")
404         }
405 }
406
407 // NewTab opens the given file in a new tab
408 func NewTab(args []string) {
409         if len(args) == 0 {
410                 CurView().AddTab(true)
411         } else {
412                 filename := args[0]
413                 filename = ReplaceHome(filename)
414                 file, err := os.Open(filename)
415                 fileInfo, _ := os.Stat(filename)
416
417                 if err == nil && fileInfo.IsDir() {
418                         messenger.Error(filename, " is a directory")
419                         return
420                 }
421
422                 defer file.Close()
423
424                 var buf *Buffer
425                 if err != nil {
426                         buf = NewBufferFromString("", filename)
427                 } else {
428                         buf = NewBuffer(file, FSize(file), filename)
429                 }
430
431                 tab := NewTabFromView(NewView(buf))
432                 tab.SetNum(len(tabs))
433                 tabs = append(tabs, tab)
434                 curTab = len(tabs) - 1
435                 if len(tabs) == 2 {
436                         for _, t := range tabs {
437                                 for _, v := range t.views {
438                                         v.ToggleTabbar()
439                                 }
440                         }
441                 }
442         }
443 }
444
445 // Set sets an option
446 func Set(args []string) {
447         if len(args) < 2 {
448                 messenger.Error("Not enough arguments")
449                 return
450         }
451
452         option := args[0]
453         value := args[1]
454
455         SetOptionAndSettings(option, value)
456 }
457
458 // SetLocal sets an option local to the buffer
459 func SetLocal(args []string) {
460         if len(args) < 2 {
461                 messenger.Error("Not enough arguments")
462                 return
463         }
464
465         option := args[0]
466         value := args[1]
467
468         err := SetLocalOption(option, value, CurView())
469         if err != nil {
470                 messenger.Error(err.Error())
471         }
472 }
473
474 // Show shows the value of the given option
475 func Show(args []string) {
476         if len(args) < 1 {
477                 messenger.Error("Please provide an option to show")
478                 return
479         }
480
481         option := GetOption(args[0])
482
483         if option == nil {
484                 messenger.Error(args[0], " is not a valid option")
485                 return
486         }
487
488         messenger.Message(option)
489 }
490
491 // Bind creates a new keybinding
492 func Bind(args []string) {
493         if len(args) < 2 {
494                 messenger.Error("Not enough arguments")
495                 return
496         }
497         BindKey(args[0], args[1])
498 }
499
500 // Run runs a shell command in the background
501 func Run(args []string) {
502         // Run a shell command in the background (openTerm is false)
503         HandleShellCommand(JoinCommandArgs(args...), false, true)
504 }
505
506 // Quit closes the main view
507 func Quit(args []string) {
508         // Close the main view
509         CurView().Quit(true)
510 }
511
512 // Save saves the buffer in the main view
513 func Save(args []string) {
514         if len(args) == 0 {
515                 // Save the main view
516                 CurView().Save(true)
517         } else {
518                 CurView().Buf.SaveAs(args[0])
519         }
520 }
521
522 // Replace runs search and replace
523 func Replace(args []string) {
524         if len(args) < 2 || len(args) > 4 {
525                 // We need to find both a search and replace expression
526                 messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
527                 return
528         }
529
530         all := false
531         noRegex := false
532
533         if len(args) > 2 {
534                 for _, arg := range args[2:] {
535                         switch arg {
536                         case "-a":
537                                 all = true
538                         case "-l":
539                                 noRegex = true
540                         default:
541                                 messenger.Error("Invalid flag: " + arg)
542                                 return
543                         }
544                 }
545         }
546
547         search := string(args[0])
548
549         if noRegex {
550                 search = regexp.QuoteMeta(search)
551         }
552
553         replace := string(args[1])
554
555         regex, err := regexp.Compile("(?m)" + search)
556         if err != nil {
557                 // There was an error with the user's regex
558                 messenger.Error(err.Error())
559                 return
560         }
561
562         view := CurView()
563
564         found := 0
565         replaceAll := func() {
566                 var deltas []Delta
567                 deltaXOffset := Count(replace) - Count(search)
568                 for i := 0; i < view.Buf.LinesNum(); i++ {
569                         matches := regex.FindAllIndex(view.Buf.lines[i].data, -1)
570                         str := string(view.Buf.lines[i].data)
571
572                         if matches != nil {
573                                 xOffset := 0
574                                 for _, m := range matches {
575                                         from := Loc{runePos(m[0], str) + xOffset, i}
576                                         to := Loc{runePos(m[1], str) + xOffset, i}
577
578                                         xOffset += deltaXOffset
579
580                                         deltas = append(deltas, Delta{replace, from, to})
581                                         found++
582                                 }
583                         }
584                 }
585                 view.Buf.MultipleReplace(deltas)
586         }
587
588         if all {
589                 replaceAll()
590         } else {
591                 for {
592                         // The 'check' flag was used
593                         Search(search, view, true)
594                         if !view.Cursor.HasSelection() {
595                                 break
596                         }
597                         view.Relocate()
598                         RedrawAll()
599                         choice, canceled := messenger.LetterPrompt("Perform replacement? (y,n,a)", 'y', 'n', 'a')
600                         if canceled {
601                                 if view.Cursor.HasSelection() {
602                                         view.Cursor.Loc = view.Cursor.CurSelection[0]
603                                         view.Cursor.ResetSelection()
604                                 }
605                                 messenger.Reset()
606                                 break
607                         } else if choice == 'a' {
608                                 if view.Cursor.HasSelection() {
609                                         view.Cursor.Loc = view.Cursor.CurSelection[0]
610                                         view.Cursor.ResetSelection()
611                                 }
612                                 messenger.Reset()
613                                 replaceAll()
614                                 break
615                         } else if choice == 'y' {
616                                 view.Cursor.DeleteSelection()
617                                 view.Buf.Insert(view.Cursor.Loc, replace)
618                                 view.Cursor.ResetSelection()
619                                 messenger.Reset()
620                                 found++
621                         }
622                         if view.Cursor.HasSelection() {
623                                 searchStart = view.Cursor.CurSelection[1]
624                         } else {
625                                 searchStart = view.Cursor.Loc
626                         }
627                 }
628         }
629         view.Cursor.Relocate()
630
631         if found > 1 {
632                 messenger.Message("Replaced ", found, " occurrences of ", search)
633         } else if found == 1 {
634                 messenger.Message("Replaced ", found, " occurrence of ", search)
635         } else {
636                 messenger.Message("Nothing matched ", search)
637         }
638 }
639
640 // ReplaceAll replaces search term all at once
641 func ReplaceAll(args []string) {
642         // aliased to Replace command
643         Replace(append(args, "-a"))
644 }
645
646 // RunShellCommand executes a shell command and returns the output/error
647 func RunShellCommand(input string) (string, error) {
648         args, err := SplitCommandArgs(input)
649         if err != nil {
650                 return "", err
651         }
652         inputCmd := args[0]
653
654         cmd := exec.Command(inputCmd, args[1:]...)
655         outputBytes := &bytes.Buffer{}
656         cmd.Stdout = outputBytes
657         cmd.Stderr = outputBytes
658         cmd.Start()
659         err = cmd.Wait() // wait for command to finish
660         outstring := outputBytes.String()
661         return outstring, err
662 }
663
664 // HandleShellCommand runs the shell command
665 // The openTerm argument specifies whether a terminal should be opened (for viewing output
666 // or interacting with stdin)
667 func HandleShellCommand(input string, openTerm bool, waitToFinish bool) string {
668         args, err := SplitCommandArgs(input)
669         if err != nil {
670                 return ""
671         }
672         inputCmd := args[0]
673         if !openTerm {
674                 // Simply run the command in the background and notify the user when it's done
675                 messenger.Message("Running...")
676                 go func() {
677                         output, err := RunShellCommand(input)
678                         totalLines := strings.Split(output, "\n")
679
680                         if len(totalLines) < 3 {
681                                 if err == nil {
682                                         messenger.Message(inputCmd, " exited without error")
683                                 } else {
684                                         messenger.Message(inputCmd, " exited with error: ", err, ": ", output)
685                                 }
686                         } else {
687                                 messenger.Message(output)
688                         }
689                         // We have to make sure to redraw
690                         RedrawAll()
691                 }()
692         } else {
693                 // Shut down the screen because we're going to interact directly with the shell
694                 screen.Fini()
695                 screen = nil
696
697                 args := args[1:]
698
699                 // Set up everything for the command
700                 var output string
701                 cmd := exec.Command(inputCmd, args...)
702                 cmd.Stdin = os.Stdin
703                 cmd.Stdout = os.Stdout
704                 cmd.Stderr = os.Stderr
705
706                 // This is a trap for Ctrl-C so that it doesn't kill micro
707                 // Instead we trap Ctrl-C to kill the program we're running
708                 c := make(chan os.Signal, 1)
709                 signal.Notify(c, os.Interrupt)
710                 go func() {
711                         for range c {
712                                 cmd.Process.Kill()
713                         }
714                 }()
715
716                 cmd.Start()
717                 err := cmd.Wait()
718
719                 if err != nil {
720                         output = err.Error()
721                 }
722
723                 if waitToFinish {
724                         // This is just so we don't return right away and let the user press enter to return
725                         TermMessage("")
726                 }
727
728                 // Start the screen back up
729                 InitScreen()
730
731                 return output
732         }
733         return ""
734 }
735
736 // HandleCommand handles input from the user
737 func HandleCommand(input string) {
738         args, err := SplitCommandArgs(input)
739         if err != nil {
740                 messenger.Error("Error parsing args ", err)
741                 return
742         }
743
744         inputCmd := args[0]
745
746         if _, ok := commands[inputCmd]; !ok {
747                 messenger.Error("Unknown command ", inputCmd)
748         } else {
749                 commands[inputCmd].action(args[1:])
750         }
751 }