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