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