]> git.lizzy.rs Git - micro.git/blob - cmd/micro/action/command.go
Add keymenu
[micro.git] / cmd / micro / action / command.go
1 package action
2
3 import (
4         "errors"
5         "fmt"
6         "os"
7         "path/filepath"
8         "regexp"
9         "strconv"
10         "strings"
11         "unicode/utf8"
12
13         "github.com/zyedidia/micro/cmd/micro/buffer"
14         "github.com/zyedidia/micro/cmd/micro/config"
15         "github.com/zyedidia/micro/cmd/micro/screen"
16         "github.com/zyedidia/micro/cmd/micro/shell"
17         "github.com/zyedidia/micro/cmd/micro/shellwords"
18         "github.com/zyedidia/micro/cmd/micro/util"
19 )
20
21 // A Command contains an action (a function to call) as well as information about how to autocomplete the command
22 type Command struct {
23         action      func(*BufHandler, []string)
24         completions []Completion
25 }
26
27 // A StrCommand is similar to a command but keeps the name of the action
28 type StrCommand struct {
29         action      string
30         completions []Completion
31 }
32
33 var commands map[string]Command
34
35 var commandActions = map[string]func(*BufHandler, []string){
36         "Set":        (*BufHandler).SetCmd,
37         "SetLocal":   (*BufHandler).SetLocalCmd,
38         "Show":       (*BufHandler).ShowCmd,
39         "ShowKey":    (*BufHandler).ShowKeyCmd,
40         "Run":        (*BufHandler).RunCmd,
41         "Bind":       (*BufHandler).BindCmd,
42         "Unbind":     (*BufHandler).UnbindCmd,
43         "Quit":       (*BufHandler).QuitCmd,
44         "Save":       (*BufHandler).SaveCmd,
45         "Replace":    (*BufHandler).ReplaceCmd,
46         "ReplaceAll": (*BufHandler).ReplaceAllCmd,
47         "VSplit":     (*BufHandler).VSplitCmd,
48         "HSplit":     (*BufHandler).HSplitCmd,
49         "Tab":        (*BufHandler).NewTabCmd,
50         "Help":       (*BufHandler).HelpCmd,
51         "Eval":       (*BufHandler).EvalCmd,
52         "ToggleLog":  (*BufHandler).ToggleLogCmd,
53         "Plugin":     (*BufHandler).PluginCmd,
54         "Reload":     (*BufHandler).ReloadCmd,
55         "Cd":         (*BufHandler).CdCmd,
56         "Pwd":        (*BufHandler).PwdCmd,
57         "Open":       (*BufHandler).OpenCmd,
58         "TabSwitch":  (*BufHandler).TabSwitchCmd,
59         "Term":       (*BufHandler).TermCmd,
60         "MemUsage":   (*BufHandler).MemUsageCmd,
61         "Retab":      (*BufHandler).RetabCmd,
62         "Raw":        (*BufHandler).RawCmd,
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                 "unbind":     {"Unbind", []Completion{NoCompletion}},
101                 "run":        {"Run", []Completion{NoCompletion}},
102                 "quit":       {"Quit", []Completion{NoCompletion}},
103                 "save":       {"Save", []Completion{NoCompletion}},
104                 "replace":    {"Replace", []Completion{NoCompletion}},
105                 "replaceall": {"ReplaceAll", []Completion{NoCompletion}},
106                 "vsplit":     {"VSplit", []Completion{FileCompletion, NoCompletion}},
107                 "hsplit":     {"HSplit", []Completion{FileCompletion, NoCompletion}},
108                 "tab":        {"Tab", []Completion{FileCompletion, NoCompletion}},
109                 "help":       {"Help", []Completion{HelpCompletion, NoCompletion}},
110                 "eval":       {"Eval", []Completion{NoCompletion}},
111                 "log":        {"ToggleLog", []Completion{NoCompletion}},
112                 "plugin":     {"Plugin", []Completion{PluginCmdCompletion, PluginNameCompletion}},
113                 "reload":     {"Reload", []Completion{NoCompletion}},
114                 "cd":         {"Cd", []Completion{FileCompletion}},
115                 "pwd":        {"Pwd", []Completion{NoCompletion}},
116                 "open":       {"Open", []Completion{FileCompletion}},
117                 "tabswitch":  {"TabSwitch", []Completion{NoCompletion}},
118                 "term":       {"Term", []Completion{NoCompletion}},
119                 "memusage":   {"MemUsage", []Completion{NoCompletion}},
120                 "retab":      {"Retab", []Completion{NoCompletion}},
121                 "raw":        {"Raw", []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) BufKeyAction {
129         return func(h *BufHandler) bool {
130                 InfoBar.Prompt("> ", prompt, "Command", nil, func(resp string, canceled bool) {
131                         if !canceled {
132                                 MainTab().CurPane().HandleCommand(resp)
133                         }
134                 })
135                 return false
136         }
137 }
138
139 // CommandAction returns a bindable function which executes the
140 // given command
141 func CommandAction(cmd string) BufKeyAction {
142         return func(h *BufHandler) bool {
143                 MainTab().CurPane().HandleCommand(cmd)
144                 return false
145         }
146 }
147
148 // PluginCmd installs, removes, updates, lists, or searches for given plugins
149 func (h *BufHandler) PluginCmd(args []string) {
150 }
151
152 // RetabCmd changes all spaces to tabs or all tabs to spaces
153 // depending on the user's settings
154 func (h *BufHandler) RetabCmd(args []string) {
155         h.Buf.Retab()
156 }
157
158 // RawCmd opens a new raw view which displays the escape sequences micro
159 // is receiving in real-time
160 func (h *BufHandler) RawCmd(args []string) {
161 }
162
163 // TabSwitchCmd switches to a given tab either by name or by number
164 func (h *BufHandler) TabSwitchCmd(args []string) {
165         if len(args) > 0 {
166                 num, err := strconv.Atoi(args[0])
167                 if err != nil {
168                         // Check for tab with this name
169
170                         found := false
171                         for i, t := range Tabs.List {
172                                 if t.Panes[t.active].Name() == args[0] {
173                                         Tabs.SetActive(i)
174                                         found = true
175                                 }
176                         }
177                         if !found {
178                                 InfoBar.Error("Could not find tab: ", err)
179                         }
180                 } else {
181                         num--
182                         if num >= 0 && num < len(Tabs.List) {
183                                 Tabs.SetActive(num)
184                         } else {
185                                 InfoBar.Error("Invalid tab index")
186                         }
187                 }
188         }
189 }
190
191 // CdCmd changes the current working directory
192 func (h *BufHandler) CdCmd(args []string) {
193         if len(args) > 0 {
194                 path, err := util.ReplaceHome(args[0])
195                 if err != nil {
196                         InfoBar.Error(err)
197                         return
198                 }
199                 err = os.Chdir(path)
200                 if err != nil {
201                         InfoBar.Error(err)
202                         return
203                 }
204                 wd, _ := os.Getwd()
205                 for _, b := range buffer.OpenBuffers {
206                         if len(b.Path) > 0 {
207                                 b.Path, _ = util.MakeRelative(b.AbsPath, wd)
208                                 if p, _ := filepath.Abs(b.Path); !strings.Contains(p, wd) {
209                                         b.Path = b.AbsPath
210                                 }
211                         }
212                 }
213         }
214 }
215
216 // MemUsageCmd prints micro's memory usage
217 // Alloc shows how many bytes are currently in use
218 // Sys shows how many bytes have been requested from the operating system
219 // NumGC shows how many times the GC has been run
220 // Note that Go commonly reserves more memory from the OS than is currently in-use/required
221 // Additionally, even if Go returns memory to the OS, the OS does not always claim it because
222 // there may be plenty of memory to spare
223 func (h *BufHandler) MemUsageCmd(args []string) {
224         InfoBar.Message(util.GetMemStats())
225 }
226
227 // PwdCmd prints the current working directory
228 func (h *BufHandler) PwdCmd(args []string) {
229         wd, err := os.Getwd()
230         if err != nil {
231                 InfoBar.Message(err.Error())
232         } else {
233                 InfoBar.Message(wd)
234         }
235 }
236
237 // OpenCmd opens a new buffer with a given filename
238 func (h *BufHandler) OpenCmd(args []string) {
239         if len(args) > 0 {
240                 filename := args[0]
241                 // the filename might or might not be quoted, so unquote first then join the strings.
242                 args, err := shellwords.Split(filename)
243                 if err != nil {
244                         InfoBar.Error("Error parsing args ", err)
245                         return
246                 }
247                 filename = strings.Join(args, " ")
248
249                 open := func() {
250                         b, err := buffer.NewBufferFromFile(filename, buffer.BTDefault)
251                         if err != nil {
252                                 InfoBar.Error(err)
253                                 return
254                         }
255                         h.OpenBuffer(b)
256                 }
257                 if h.Buf.Modified() {
258                         InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
259                                 if !canceled && !yes {
260                                         open()
261                                 } else if !canceled && yes {
262                                         h.Save()
263                                         open()
264                                 }
265                         })
266                 } else {
267                         open()
268                 }
269         } else {
270                 InfoBar.Error("No filename")
271         }
272 }
273
274 // ToggleLogCmd toggles the log view
275 func (h *BufHandler) ToggleLogCmd(args []string) {
276 }
277
278 // ReloadCmd reloads all files (syntax files, colorschemes...)
279 func (h *BufHandler) ReloadCmd(args []string) {
280 }
281
282 func (h *BufHandler) openHelp(page string) error {
283         if data, err := config.FindRuntimeFile(config.RTHelp, page).Data(); err != nil {
284                 return errors.New(fmt.Sprint("Unable to load help text", page, "\n", err))
285         } else {
286                 helpBuffer := buffer.NewBufferFromString(string(data), page+".md", buffer.BTHelp)
287                 helpBuffer.SetName("Help " + page)
288
289                 if h.Buf.Type == buffer.BTHelp {
290                         h.OpenBuffer(helpBuffer)
291                 } else {
292                         h.HSplitBuf(helpBuffer)
293                 }
294         }
295         return nil
296 }
297
298 // HelpCmd tries to open the given help page in a horizontal split
299 func (h *BufHandler) HelpCmd(args []string) {
300         if len(args) < 1 {
301                 // Open the default help if the user just typed "> help"
302                 h.openHelp("help")
303         } else {
304                 if config.FindRuntimeFile(config.RTHelp, args[0]) != nil {
305                         err := h.openHelp(args[0])
306                         if err != nil {
307                                 InfoBar.Error(err)
308                         }
309                 } else {
310                         InfoBar.Error("Sorry, no help for ", args[0])
311                 }
312         }
313 }
314
315 // VSplitCmd opens a vertical split with file given in the first argument
316 // If no file is given, it opens an empty buffer in a new split
317 func (h *BufHandler) VSplitCmd(args []string) {
318         if len(args) == 0 {
319                 // Open an empty vertical split
320                 h.VSplitAction()
321                 return
322         }
323
324         buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
325         if err != nil {
326                 InfoBar.Error(err)
327                 return
328         }
329
330         h.VSplitBuf(buf)
331 }
332
333 // HSplitCmd opens a horizontal split with file given in the first argument
334 // If no file is given, it opens an empty buffer in a new split
335 func (h *BufHandler) HSplitCmd(args []string) {
336         if len(args) == 0 {
337                 // Open an empty horizontal split
338                 h.HSplitAction()
339                 return
340         }
341
342         buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
343         if err != nil {
344                 InfoBar.Error(err)
345                 return
346         }
347
348         h.HSplitBuf(buf)
349 }
350
351 // EvalCmd evaluates a lua expression
352 func (h *BufHandler) EvalCmd(args []string) {
353 }
354
355 // NewTabCmd opens the given file in a new tab
356 func (h *BufHandler) NewTabCmd(args []string) {
357         width, height := screen.Screen.Size()
358         iOffset := config.GetInfoBarOffset()
359         if len(args) > 0 {
360                 for _, a := range args {
361                         b, err := buffer.NewBufferFromFile(a, buffer.BTDefault)
362                         if err != nil {
363                                 InfoBar.Error(err)
364                                 return
365                         }
366                         tp := NewTabFromBuffer(0, 0, width, height-1-iOffset, b)
367                         Tabs.AddTab(tp)
368                         Tabs.SetActive(len(Tabs.List) - 1)
369                 }
370         } else {
371                 b := buffer.NewBufferFromString("", "", buffer.BTDefault)
372                 tp := NewTabFromBuffer(0, 0, width, height-iOffset, b)
373                 Tabs.AddTab(tp)
374                 Tabs.SetActive(len(Tabs.List) - 1)
375         }
376 }
377
378 func SetGlobalOption(option, value string) error {
379         if _, ok := config.GlobalSettings[option]; !ok {
380                 return config.ErrInvalidOption
381         }
382
383         nativeValue, err := config.GetNativeValue(option, config.GlobalSettings[option], value)
384         if err != nil {
385                 return err
386         }
387
388         config.GlobalSettings[option] = nativeValue
389
390         if option == "colorscheme" {
391                 // LoadSyntaxFiles()
392                 config.InitColorscheme()
393                 for _, b := range buffer.OpenBuffers {
394                         b.UpdateRules()
395                 }
396         }
397
398         if option == "infobar" || option == "keymenu" {
399                 Tabs.Resize()
400         }
401
402         if option == "mouse" {
403                 if !nativeValue.(bool) {
404                         screen.Screen.DisableMouse()
405                 } else {
406                         screen.Screen.EnableMouse()
407                 }
408         }
409
410         for _, b := range buffer.OpenBuffers {
411                 b.SetOption(option, value)
412         }
413
414         config.WriteSettings(config.ConfigDir + "/settings.json")
415
416         return nil
417 }
418
419 // SetCmd sets an option
420 func (h *BufHandler) SetCmd(args []string) {
421         if len(args) < 2 {
422                 InfoBar.Error("Not enough arguments")
423                 return
424         }
425
426         option := args[0]
427         value := args[1]
428
429         err := SetGlobalOption(option, value)
430         if err == config.ErrInvalidOption {
431                 err := h.Buf.SetOption(option, value)
432                 if err != nil {
433                         InfoBar.Error(err)
434                 }
435         } else if err != nil {
436                 InfoBar.Error(err)
437         }
438 }
439
440 // SetLocalCmd sets an option local to the buffer
441 func (h *BufHandler) SetLocalCmd(args []string) {
442         if len(args) < 2 {
443                 InfoBar.Error("Not enough arguments")
444                 return
445         }
446
447         option := args[0]
448         value := args[1]
449
450         err := h.Buf.SetOption(option, value)
451         if err != nil {
452                 InfoBar.Error(err)
453         }
454
455 }
456
457 // ShowCmd shows the value of the given option
458 func (h *BufHandler) ShowCmd(args []string) {
459         if len(args) < 1 {
460                 InfoBar.Error("Please provide an option to show")
461                 return
462         }
463
464         var option interface{}
465         if opt, ok := h.Buf.Settings[args[0]]; ok {
466                 option = opt
467         } else if opt, ok := config.GlobalSettings[args[0]]; ok {
468                 option = opt
469         }
470
471         if option == nil {
472                 InfoBar.Error(args[0], " is not a valid option")
473                 return
474         }
475
476         InfoBar.Message(option)
477 }
478
479 // ShowKeyCmd displays the action that a key is bound to
480 func (h *BufHandler) ShowKeyCmd(args []string) {
481         if len(args) < 1 {
482                 InfoBar.Error("Please provide a key to show")
483                 return
484         }
485
486         if action, ok := config.Bindings[args[0]]; ok {
487                 InfoBar.Message(action)
488         } else {
489                 InfoBar.Message(args[0], " has no binding")
490         }
491 }
492
493 // BindCmd creates a new keybinding
494 func (h *BufHandler) BindCmd(args []string) {
495         if len(args) < 2 {
496                 InfoBar.Error("Not enough arguments")
497                 return
498         }
499
500         _, err := TryBindKey(args[0], args[1], true)
501         if err != nil {
502                 InfoBar.Error(err)
503         }
504 }
505
506 // UnbindCmd binds a key to its default action
507 func (h *BufHandler) UnbindCmd(args []string) {
508         if len(args) < 1 {
509                 InfoBar.Error("Not enough arguements")
510                 return
511         }
512
513         err := UnbindKey(args[0])
514         if err != nil {
515                 InfoBar.Error(err)
516         }
517 }
518
519 // RunCmd runs a shell command in the background
520 func (h *BufHandler) RunCmd(args []string) {
521         runf, err := shell.RunBackgroundShell(shellwords.Join(args...))
522         if err != nil {
523                 InfoBar.Error(err)
524         } else {
525                 go func() {
526                         InfoBar.Message(runf())
527                         screen.Redraw()
528                 }()
529         }
530 }
531
532 // QuitCmd closes the main view
533 func (h *BufHandler) QuitCmd(args []string) {
534         h.Quit()
535 }
536
537 // SaveCmd saves the buffer in the main view
538 func (h *BufHandler) SaveCmd(args []string) {
539         h.Save()
540 }
541
542 // ReplaceCmd runs search and replace
543 func (h *BufHandler) ReplaceCmd(args []string) {
544         if len(args) < 2 || len(args) > 4 {
545                 // We need to find both a search and replace expression
546                 InfoBar.Error("Invalid replace statement: " + strings.Join(args, " "))
547                 return
548         }
549
550         all := false
551         noRegex := false
552
553         if len(args) > 2 {
554                 for _, arg := range args[2:] {
555                         switch arg {
556                         case "-a":
557                                 all = true
558                         case "-l":
559                                 noRegex = true
560                         default:
561                                 InfoBar.Error("Invalid flag: " + arg)
562                                 return
563                         }
564                 }
565         }
566
567         search := args[0]
568
569         if noRegex {
570                 search = regexp.QuoteMeta(search)
571         }
572
573         replace := []byte(args[1])
574
575         var regex *regexp.Regexp
576         var err error
577         if h.Buf.Settings["ignorecase"].(bool) {
578                 regex, err = regexp.Compile("(?im)" + search)
579         } else {
580                 regex, err = regexp.Compile("(?m)" + search)
581         }
582         if err != nil {
583                 // There was an error with the user's regex
584                 InfoBar.Error(err)
585                 return
586         }
587
588         nreplaced := 0
589         start := h.Buf.Start()
590         end := h.Buf.End()
591         if h.Cursor.HasSelection() {
592                 start = h.Cursor.CurSelection[0]
593                 end = h.Cursor.CurSelection[1]
594         }
595         if all {
596                 nreplaced = h.Buf.ReplaceRegex(start, end, regex, replace)
597         } else {
598                 inRange := func(l buffer.Loc) bool {
599                         return l.GreaterEqual(start) && l.LessThan(end)
600                 }
601
602                 searchLoc := start
603                 searching := true
604                 var doReplacement func()
605                 doReplacement = func() {
606                         locs, found, err := h.Buf.FindNext(search, start, end, searchLoc, true, !noRegex)
607                         if err != nil {
608                                 InfoBar.Error(err)
609                                 return
610                         }
611                         if !found || !inRange(locs[0]) || !inRange(locs[1]) {
612                                 h.Cursor.ResetSelection()
613                                 h.Cursor.Relocate()
614                                 return
615                         }
616
617                         h.Cursor.SetSelectionStart(locs[0])
618                         h.Cursor.SetSelectionEnd(locs[1])
619
620                         InfoBar.YNPrompt("Perform replacement (y,n,esc)", func(yes, canceled bool) {
621                                 if !canceled && yes {
622                                         h.Buf.Replace(locs[0], locs[1], replace)
623                                         searchLoc = locs[0]
624                                         searchLoc.X += utf8.RuneCount(replace)
625                                         h.Cursor.Loc = searchLoc
626                                         nreplaced++
627                                 } else if !canceled && !yes {
628                                         searchLoc = locs[0]
629                                         searchLoc.X += utf8.RuneCount(replace)
630                                 } else if canceled {
631                                         h.Cursor.ResetSelection()
632                                         h.Cursor.Relocate()
633                                         return
634                                 }
635                                 if searching {
636                                         doReplacement()
637                                 }
638                         })
639                 }
640                 doReplacement()
641         }
642
643         // TODO: relocate all cursors?
644         h.Cursor.Relocate()
645
646         if nreplaced > 1 {
647                 InfoBar.Message("Replaced ", nreplaced, " occurrences of ", search)
648         } else if nreplaced == 1 {
649                 InfoBar.Message("Replaced ", nreplaced, " occurrence of ", search)
650         } else {
651                 InfoBar.Message("Nothing matched ", search)
652         }
653 }
654
655 // ReplaceAllCmd replaces search term all at once
656 func (h *BufHandler) ReplaceAllCmd(args []string) {
657 }
658
659 // TermCmd opens a terminal in the current view
660 func (h *BufHandler) TermCmd(args []string) {
661         ps := MainTab().Panes
662
663         if len(args) == 0 {
664                 sh := os.Getenv("SHELL")
665                 if sh == "" {
666                         InfoBar.Error("Shell environment not found")
667                         return
668                 }
669                 args = []string{sh}
670         }
671
672         term := func(i int, newtab bool) {
673
674                 t := new(shell.Terminal)
675                 t.Start(args, false, true)
676
677                 id := h.ID()
678                 if newtab {
679                         h.AddTab()
680                         i = 0
681                         id = MainTab().Panes[0].ID()
682                 } else {
683                         MainTab().Panes[i].Close()
684                 }
685
686                 v := h.GetView()
687                 MainTab().Panes[i] = NewTermHandler(v.X, v.Y, v.Width, v.Height, t, id)
688                 MainTab().SetActive(i)
689         }
690
691         // If there is only one open file we make a new tab instead of overwriting it
692         newtab := len(MainTab().Panes) == 1 && len(Tabs.List) == 1
693
694         if newtab {
695                 term(0, true)
696                 return
697         }
698
699         for i, p := range ps {
700                 if p.ID() == h.ID() {
701                         if h.Buf.Modified() {
702                                 InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
703                                         if !canceled && !yes {
704                                                 term(i, false)
705                                         } else if !canceled && yes {
706                                                 h.Save()
707                                                 term(i, false)
708                                         }
709                                 })
710                         } else {
711                                 term(i, false)
712                         }
713                 }
714         }
715 }
716
717 // HandleCommand handles input from the user
718 func (h *BufHandler) HandleCommand(input string) {
719         args, err := shellwords.Split(input)
720         if err != nil {
721                 InfoBar.Error("Error parsing args ", err)
722                 return
723         }
724
725         inputCmd := args[0]
726
727         if _, ok := commands[inputCmd]; !ok {
728                 InfoBar.Error("Unknown command ", inputCmd)
729         } else {
730                 commands[inputCmd].action(h, args[1:])
731         }
732 }