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