]> git.lizzy.rs Git - micro.git/blob - cmd/micro/command.go
0bf1e69428628757e9ed2e990efa43bc5ea83d89
[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.Num
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                 defer file.Close()
330
331                 var buf *Buffer
332                 if err != nil {
333                         // File does not exist -- create an empty buffer with that name
334                         buf = NewBuffer(strings.NewReader(""), filename)
335                 } else {
336                         buf = NewBuffer(file, filename)
337                 }
338                 CurView().VSplit(buf)
339         }
340 }
341
342 // HSplit opens a horizontal split with file given in the first argument
343 // If no file is given, it opens an empty buffer in a new split
344 func HSplit(args []string) {
345         if len(args) == 0 {
346                 CurView().HSplit(NewBuffer(strings.NewReader(""), ""))
347         } else {
348                 filename := args[0]
349                 home, _ := homedir.Dir()
350                 filename = strings.Replace(filename, "~", home, 1)
351                 file, err := os.Open(filename)
352                 defer file.Close()
353
354                 var buf *Buffer
355                 if err != nil {
356                         // File does not exist -- create an empty buffer with that name
357                         buf = NewBuffer(strings.NewReader(""), filename)
358                 } else {
359                         buf = NewBuffer(file, filename)
360                 }
361                 CurView().HSplit(buf)
362         }
363 }
364
365 // Eval evaluates a lua expression
366 func Eval(args []string) {
367         if len(args) >= 1 {
368                 err := L.DoString(args[0])
369                 if err != nil {
370                         messenger.Error(err)
371                 }
372         } else {
373                 messenger.Error("Not enough arguments")
374         }
375 }
376
377 // NewTab opens the given file in a new tab
378 func NewTab(args []string) {
379         if len(args) == 0 {
380                 CurView().AddTab(true)
381         } else {
382                 filename := args[0]
383                 home, _ := homedir.Dir()
384                 filename = strings.Replace(filename, "~", home, 1)
385                 file, _ := os.Open(filename)
386                 defer file.Close()
387
388                 tab := NewTabFromView(NewView(NewBuffer(file, filename)))
389                 tab.SetNum(len(tabs))
390                 tabs = append(tabs, tab)
391                 curTab = len(tabs) - 1
392                 if len(tabs) == 2 {
393                         for _, t := range tabs {
394                                 for _, v := range t.views {
395                                         v.ToggleTabbar()
396                                 }
397                         }
398                 }
399         }
400 }
401
402 // Set sets an option
403 func Set(args []string) {
404         if len(args) < 2 {
405                 messenger.Error("Not enough arguments")
406                 return
407         }
408
409         option := args[0]
410         value := args[1]
411
412         SetOptionAndSettings(option, value)
413 }
414
415 // SetLocal sets an option local to the buffer
416 func SetLocal(args []string) {
417         if len(args) < 2 {
418                 messenger.Error("Not enough arguments")
419                 return
420         }
421
422         option := args[0]
423         value := args[1]
424
425         err := SetLocalOption(option, value, CurView())
426         if err != nil {
427                 messenger.Error(err.Error())
428         }
429 }
430
431 // Show shows the value of the given option
432 func Show(args []string) {
433         if len(args) < 1 {
434                 messenger.Error("Please provide an option to show")
435                 return
436         }
437
438         option := GetOption(args[0])
439
440         if option == nil {
441                 messenger.Error(args[0], " is not a valid option")
442                 return
443         }
444
445         messenger.Message(option)
446 }
447
448 // Bind creates a new keybinding
449 func Bind(args []string) {
450         if len(args) < 2 {
451                 messenger.Error("Not enough arguments")
452                 return
453         }
454         BindKey(args[0], args[1])
455 }
456
457 // Run runs a shell command in the background
458 func Run(args []string) {
459         // Run a shell command in the background (openTerm is false)
460         HandleShellCommand(JoinCommandArgs(args...), false, true)
461 }
462
463 // Quit closes the main view
464 func Quit(args []string) {
465         // Close the main view
466         CurView().Quit(true)
467 }
468
469 // Save saves the buffer in the main view
470 func Save(args []string) {
471         if len(args) == 0 {
472                 // Save the main view
473                 CurView().Save(true)
474         } else {
475                 CurView().Buf.SaveAs(args[0])
476         }
477 }
478
479 // Replace runs search and replace
480 func Replace(args []string) {
481         if len(args) < 2 {
482                 // We need to find both a search and replace expression
483                 messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
484                 return
485         }
486
487         var flags string
488         if len(args) == 3 {
489                 // The user included some flags
490                 flags = args[2]
491         }
492
493         search := string(args[0])
494         replace := string(args[1])
495
496         regex, err := regexp.Compile("(?m)" + search)
497         if err != nil {
498                 // There was an error with the user's regex
499                 messenger.Error(err.Error())
500                 return
501         }
502
503         view := CurView()
504
505         found := 0
506         if strings.Contains(flags, "c") {
507                 for {
508                         // The 'check' flag was used
509                         Search(search, view, true)
510                         if !view.Cursor.HasSelection() {
511                                 break
512                         }
513                         view.Relocate()
514                         RedrawAll()
515                         choice, canceled := messenger.YesNoPrompt("Perform replacement? (y,n)")
516                         if canceled {
517                                 if view.Cursor.HasSelection() {
518                                         view.Cursor.Loc = view.Cursor.CurSelection[0]
519                                         view.Cursor.ResetSelection()
520                                 }
521                                 messenger.Reset()
522                                 break
523                         }
524                         if choice {
525                                 view.Cursor.DeleteSelection()
526                                 view.Buf.Insert(view.Cursor.Loc, replace)
527                                 view.Cursor.ResetSelection()
528                                 messenger.Reset()
529                                 found++
530                         } else {
531                                 if view.Cursor.HasSelection() {
532                                         searchStart = ToCharPos(view.Cursor.CurSelection[1], view.Buf)
533                                 } else {
534                                         searchStart = ToCharPos(view.Cursor.Loc, view.Buf)
535                                 }
536                                 continue
537                         }
538                 }
539         } else {
540                 bufStr := view.Buf.String()
541                 matches := regex.FindAllStringIndex(bufStr, -1)
542                 if matches != nil && len(matches) > 0 {
543                         prevMatchCount := runePos(matches[0][0], bufStr)
544                         searchCount := runePos(matches[0][1], bufStr) - prevMatchCount
545                         from := FromCharPos(matches[0][0], view.Buf)
546                         to := from.Move(searchCount, view.Buf)
547                         adjust := Count(replace) - searchCount
548                         view.Buf.Replace(from, to, replace)
549                         found++
550                         if len(matches) > 1 {
551                                 for _, match := range matches[1:] {
552                                         found++
553                                         matchCount := runePos(match[0], bufStr)
554                                         searchCount = runePos(match[1], bufStr) - matchCount
555                                         from = from.Move(matchCount-prevMatchCount+adjust, view.Buf)
556                                         to = from.Move(searchCount, view.Buf)
557                                         view.Buf.Replace(from, to, replace)
558                                         prevMatchCount = matchCount
559                                         adjust = Count(replace) - searchCount
560                                 }
561                         }
562                 }
563         }
564         view.Cursor.Relocate()
565
566         if found > 1 {
567                 messenger.Message("Replaced ", found, " occurrences of ", search)
568         } else if found == 1 {
569                 messenger.Message("Replaced ", found, " occurrence of ", search)
570         } else {
571                 messenger.Message("Nothing matched ", search)
572         }
573 }
574
575 // RunShellCommand executes a shell command and returns the output/error
576 func RunShellCommand(input string) (string, error) {
577         inputCmd := SplitCommandArgs(input)[0]
578         args := SplitCommandArgs(input)[1:]
579
580         cmd := exec.Command(inputCmd, args...)
581         outputBytes := &bytes.Buffer{}
582         cmd.Stdout = outputBytes
583         cmd.Stderr = outputBytes
584         cmd.Start()
585         err := cmd.Wait() // wait for command to finish
586         outstring := outputBytes.String()
587         return outstring, err
588 }
589
590 // HandleShellCommand runs the shell command
591 // The openTerm argument specifies whether a terminal should be opened (for viewing output
592 // or interacting with stdin)
593 func HandleShellCommand(input string, openTerm bool, waitToFinish bool) string {
594         inputCmd := SplitCommandArgs(input)[0]
595         if !openTerm {
596                 // Simply run the command in the background and notify the user when it's done
597                 messenger.Message("Running...")
598                 go func() {
599                         output, err := RunShellCommand(input)
600                         totalLines := strings.Split(output, "\n")
601
602                         if len(totalLines) < 3 {
603                                 if err == nil {
604                                         messenger.Message(inputCmd, " exited without error")
605                                 } else {
606                                         messenger.Message(inputCmd, " exited with error: ", err, ": ", output)
607                                 }
608                         } else {
609                                 messenger.Message(output)
610                         }
611                         // We have to make sure to redraw
612                         RedrawAll()
613                 }()
614         } else {
615                 // Shut down the screen because we're going to interact directly with the shell
616                 screen.Fini()
617                 screen = nil
618
619                 args := SplitCommandArgs(input)[1:]
620
621                 // Set up everything for the command
622                 var outputBuf bytes.Buffer
623                 cmd := exec.Command(inputCmd, args...)
624                 cmd.Stdin = os.Stdin
625                 cmd.Stdout = io.MultiWriter(os.Stdout, &outputBuf)
626                 cmd.Stderr = os.Stderr
627
628                 // This is a trap for Ctrl-C so that it doesn't kill micro
629                 // Instead we trap Ctrl-C to kill the program we're running
630                 c := make(chan os.Signal, 1)
631                 signal.Notify(c, os.Interrupt)
632                 go func() {
633                         for range c {
634                                 cmd.Process.Kill()
635                         }
636                 }()
637
638                 cmd.Start()
639                 err := cmd.Wait()
640
641                 output := outputBuf.String()
642                 if err != nil {
643                         output = err.Error()
644                 }
645
646                 if waitToFinish {
647                         // This is just so we don't return right away and let the user press enter to return
648                         TermMessage("")
649                 }
650
651                 // Start the screen back up
652                 InitScreen()
653
654                 return output
655         }
656         return ""
657 }
658
659 // HandleCommand handles input from the user
660 func HandleCommand(input string) {
661         args := SplitCommandArgs(input)
662         inputCmd := args[0]
663
664         if _, ok := commands[inputCmd]; !ok {
665                 messenger.Error("Unknown command ", inputCmd)
666         } else {
667                 commands[inputCmd].action(args[1:])
668         }
669 }