]> git.lizzy.rs Git - micro.git/blob - internal/action/command.go
6abc30b33fd2da232b83925c6cb6d33cb25fc466
[micro.git] / internal / action / command.go
1 package action
2
3 import (
4         "bytes"
5         "errors"
6         "fmt"
7         "os"
8         "os/exec"
9         "path/filepath"
10         "regexp"
11         "strconv"
12         "strings"
13
14         shellquote "github.com/kballard/go-shellquote"
15         "github.com/zyedidia/micro/v2/internal/buffer"
16         "github.com/zyedidia/micro/v2/internal/clipboard"
17         "github.com/zyedidia/micro/v2/internal/config"
18         "github.com/zyedidia/micro/v2/internal/screen"
19         "github.com/zyedidia/micro/v2/internal/shell"
20         "github.com/zyedidia/micro/v2/internal/util"
21 )
22
23 // A Command contains information about how to execute a command
24 // It has the action for that command as well as a completer function
25 type Command struct {
26         action    func(*BufPane, []string)
27         completer buffer.Completer
28 }
29
30 var commands map[string]Command
31
32 func InitCommands() {
33         commands = map[string]Command{
34                 "set":        {(*BufPane).SetCmd, OptionValueComplete},
35                 "reset":      {(*BufPane).ResetCmd, OptionValueComplete},
36                 "setlocal":   {(*BufPane).SetLocalCmd, OptionValueComplete},
37                 "show":       {(*BufPane).ShowCmd, OptionComplete},
38                 "showkey":    {(*BufPane).ShowKeyCmd, nil},
39                 "run":        {(*BufPane).RunCmd, nil},
40                 "bind":       {(*BufPane).BindCmd, nil},
41                 "unbind":     {(*BufPane).UnbindCmd, nil},
42                 "quit":       {(*BufPane).QuitCmd, nil},
43                 "goto":       {(*BufPane).GotoCmd, nil},
44                 "save":       {(*BufPane).SaveCmd, nil},
45                 "replace":    {(*BufPane).ReplaceCmd, nil},
46                 "replaceall": {(*BufPane).ReplaceAllCmd, nil},
47                 "vsplit":     {(*BufPane).VSplitCmd, buffer.FileComplete},
48                 "hsplit":     {(*BufPane).HSplitCmd, buffer.FileComplete},
49                 "tab":        {(*BufPane).NewTabCmd, buffer.FileComplete},
50                 "help":       {(*BufPane).HelpCmd, HelpComplete},
51                 "eval":       {(*BufPane).EvalCmd, nil},
52                 "log":        {(*BufPane).ToggleLogCmd, nil},
53                 "plugin":     {(*BufPane).PluginCmd, PluginComplete},
54                 "reload":     {(*BufPane).ReloadCmd, nil},
55                 "reopen":     {(*BufPane).ReopenCmd, nil},
56                 "cd":         {(*BufPane).CdCmd, buffer.FileComplete},
57                 "pwd":        {(*BufPane).PwdCmd, nil},
58                 "open":       {(*BufPane).OpenCmd, buffer.FileComplete},
59                 "tabmove":    {(*BufPane).TabMoveCmd, nil},
60                 "tabswitch":  {(*BufPane).TabSwitchCmd, nil},
61                 "term":       {(*BufPane).TermCmd, nil},
62                 "memusage":   {(*BufPane).MemUsageCmd, nil},
63                 "retab":      {(*BufPane).RetabCmd, nil},
64                 "raw":        {(*BufPane).RawCmd, nil},
65                 "textfilter": {(*BufPane).TextFilterCmd, nil},
66         }
67 }
68
69 // MakeCommand is a function to easily create new commands
70 // This can be called by plugins in Lua so that plugins can define their own commands
71 func MakeCommand(name string, action func(bp *BufPane, args []string), completer buffer.Completer) {
72         if action != nil {
73                 commands[name] = Command{action, completer}
74         }
75 }
76
77 // CommandEditAction returns a bindable function that opens a prompt with
78 // the given string and executes the command when the user presses
79 // enter
80 func CommandEditAction(prompt string) BufKeyAction {
81         return func(h *BufPane) bool {
82                 InfoBar.Prompt("> ", prompt, "Command", nil, func(resp string, canceled bool) {
83                         if !canceled {
84                                 MainTab().CurPane().HandleCommand(resp)
85                         }
86                 })
87                 return false
88         }
89 }
90
91 // CommandAction returns a bindable function which executes the
92 // given command
93 func CommandAction(cmd string) BufKeyAction {
94         return func(h *BufPane) bool {
95                 MainTab().CurPane().HandleCommand(cmd)
96                 return false
97         }
98 }
99
100 var PluginCmds = []string{"install", "remove", "update", "available", "list", "search"}
101
102 // PluginCmd installs, removes, updates, lists, or searches for given plugins
103 func (h *BufPane) PluginCmd(args []string) {
104         if len(args) < 1 {
105                 InfoBar.Error("Not enough arguments")
106                 return
107         }
108
109         if h.Buf.Type != buffer.BTLog {
110                 h.OpenLogBuf()
111         }
112
113         config.PluginCommand(buffer.LogBuf, args[0], args[1:])
114 }
115
116 // RetabCmd changes all spaces to tabs or all tabs to spaces
117 // depending on the user's settings
118 func (h *BufPane) RetabCmd(args []string) {
119         h.Buf.Retab()
120 }
121
122 // RawCmd opens a new raw view which displays the escape sequences micro
123 // is receiving in real-time
124 func (h *BufPane) RawCmd(args []string) {
125         width, height := screen.Screen.Size()
126         iOffset := config.GetInfoBarOffset()
127         tp := NewTabFromPane(0, 0, width, height-iOffset, NewRawPane(nil))
128         Tabs.AddTab(tp)
129         Tabs.SetActive(len(Tabs.List) - 1)
130 }
131
132 // TextFilterCmd filters the selection through the command.
133 // Selection goes to the command input.
134 // On successful run command output replaces the current selection.
135 func (h *BufPane) TextFilterCmd(args []string) {
136         if len(args) == 0 {
137                 InfoBar.Error("usage: textfilter arguments")
138                 return
139         }
140         sel := h.Cursor.GetSelection()
141         if len(sel) == 0 {
142                 h.Cursor.SelectWord()
143                 sel = h.Cursor.GetSelection()
144         }
145         var bout, berr bytes.Buffer
146         cmd := exec.Command(args[0], args[1:]...)
147         cmd.Stdin = strings.NewReader(string(sel))
148         cmd.Stderr = &berr
149         cmd.Stdout = &bout
150         err := cmd.Run()
151         if err != nil {
152                 InfoBar.Error(err.Error() + " " + berr.String())
153                 return
154         }
155         h.Cursor.DeleteSelection()
156         h.Buf.Insert(h.Cursor.Loc, bout.String())
157 }
158
159 // TabMoveCmd moves the current tab to a given index (starts at 1). The
160 // displaced tabs are moved up.
161 func (h *BufPane) TabMoveCmd(args []string) {
162         if len(args) <= 0 {
163                 InfoBar.Error("Not enough arguments: provide an index, starting at 1")
164                 return
165         }
166
167         if len(args[0]) <= 0 {
168                 InfoBar.Error("Invalid argument: empty string")
169                 return
170         }
171
172         num, err := strconv.Atoi(args[0])
173         if err != nil {
174                 InfoBar.Error("Invalid argument: ", err)
175                 return
176         }
177
178         // Preserve sign for relative move, if one exists
179         var shiftDirection byte
180         if strings.Contains("-+", string([]byte{args[0][0]})) {
181                 shiftDirection = args[0][0]
182         }
183
184         // Relative positions -> absolute positions
185         idxFrom := Tabs.Active()
186         idxTo := 0
187         offset := util.Abs(num)
188         if shiftDirection == '-' {
189                 idxTo = idxFrom - offset
190         } else if shiftDirection == '+' {
191                 idxTo = idxFrom + offset
192         } else {
193                 idxTo = offset - 1
194         }
195
196         // Restrain position to within the valid range
197         idxTo = util.Clamp(idxTo, 0, len(Tabs.List)-1)
198
199         activeTab := Tabs.List[idxFrom]
200         Tabs.RemoveTab(activeTab.ID())
201         Tabs.List = append(Tabs.List, nil)
202         copy(Tabs.List[idxTo+1:], Tabs.List[idxTo:])
203         Tabs.List[idxTo] = activeTab
204         Tabs.UpdateNames()
205         Tabs.SetActive(idxTo)
206         // InfoBar.Message(fmt.Sprintf("Moved tab from slot %d to %d", idxFrom+1, idxTo+1))
207 }
208
209 // TabSwitchCmd switches to a given tab either by name or by number
210 func (h *BufPane) TabSwitchCmd(args []string) {
211         if len(args) > 0 {
212                 num, err := strconv.Atoi(args[0])
213                 if err != nil {
214                         // Check for tab with this name
215
216                         found := false
217                         for i, t := range Tabs.List {
218                                 if t.Panes[t.active].Name() == args[0] {
219                                         Tabs.SetActive(i)
220                                         found = true
221                                 }
222                         }
223                         if !found {
224                                 InfoBar.Error("Could not find tab: ", err)
225                         }
226                 } else {
227                         num--
228                         if num >= 0 && num < len(Tabs.List) {
229                                 Tabs.SetActive(num)
230                         } else {
231                                 InfoBar.Error("Invalid tab index")
232                         }
233                 }
234         }
235 }
236
237 // CdCmd changes the current working directory
238 func (h *BufPane) CdCmd(args []string) {
239         if len(args) > 0 {
240                 path, err := util.ReplaceHome(args[0])
241                 if err != nil {
242                         InfoBar.Error(err)
243                         return
244                 }
245                 err = os.Chdir(path)
246                 if err != nil {
247                         InfoBar.Error(err)
248                         return
249                 }
250                 wd, _ := os.Getwd()
251                 for _, b := range buffer.OpenBuffers {
252                         if len(b.Path) > 0 {
253                                 b.Path, _ = util.MakeRelative(b.AbsPath, wd)
254                                 if p, _ := filepath.Abs(b.Path); !strings.Contains(p, wd) {
255                                         b.Path = b.AbsPath
256                                 }
257                         }
258                 }
259         }
260 }
261
262 // MemUsageCmd prints micro's memory usage
263 // Alloc shows how many bytes are currently in use
264 // Sys shows how many bytes have been requested from the operating system
265 // NumGC shows how many times the GC has been run
266 // Note that Go commonly reserves more memory from the OS than is currently in-use/required
267 // Additionally, even if Go returns memory to the OS, the OS does not always claim it because
268 // there may be plenty of memory to spare
269 func (h *BufPane) MemUsageCmd(args []string) {
270         InfoBar.Message(util.GetMemStats())
271 }
272
273 // PwdCmd prints the current working directory
274 func (h *BufPane) PwdCmd(args []string) {
275         wd, err := os.Getwd()
276         if err != nil {
277                 InfoBar.Message(err.Error())
278         } else {
279                 InfoBar.Message(wd)
280         }
281 }
282
283 // OpenCmd opens a new buffer with a given filename
284 func (h *BufPane) OpenCmd(args []string) {
285         if len(args) > 0 {
286                 filename := args[0]
287                 // the filename might or might not be quoted, so unquote first then join the strings.
288                 args, err := shellquote.Split(filename)
289                 if err != nil {
290                         InfoBar.Error("Error parsing args ", err)
291                         return
292                 }
293                 if len(args) == 0 {
294                         return
295                 }
296                 filename = strings.Join(args, " ")
297
298                 open := func() {
299                         b, err := buffer.NewBufferFromFile(filename, buffer.BTDefault)
300                         if err != nil {
301                                 InfoBar.Error(err)
302                                 return
303                         }
304                         h.OpenBuffer(b)
305                 }
306                 if h.Buf.Modified() {
307                         InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
308                                 if !canceled && !yes {
309                                         open()
310                                 } else if !canceled && yes {
311                                         h.Save()
312                                         open()
313                                 }
314                         })
315                 } else {
316                         open()
317                 }
318         } else {
319                 InfoBar.Error("No filename")
320         }
321 }
322
323 // ToggleLogCmd toggles the log view
324 func (h *BufPane) ToggleLogCmd(args []string) {
325         if h.Buf.Type != buffer.BTLog {
326                 h.OpenLogBuf()
327         } else {
328                 h.Quit()
329         }
330 }
331
332 // ReloadCmd reloads all files (syntax files, colorschemes...)
333 func (h *BufPane) ReloadCmd(args []string) {
334         ReloadConfig()
335 }
336
337 func ReloadConfig() {
338         config.InitRuntimeFiles()
339         err := config.ReadSettings()
340         if err != nil {
341                 screen.TermMessage(err)
342         }
343         err = config.InitGlobalSettings()
344         if err != nil {
345                 screen.TermMessage(err)
346         }
347         InitBindings()
348         InitCommands()
349
350         err = config.InitColorscheme()
351         if err != nil {
352                 screen.TermMessage(err)
353         }
354
355         for _, b := range buffer.OpenBuffers {
356                 b.UpdateRules()
357         }
358 }
359
360 // ReopenCmd reopens the buffer (reload from disk)
361 func (h *BufPane) ReopenCmd(args []string) {
362         if h.Buf.Modified() {
363                 InfoBar.YNPrompt("Save file before reopen?", func(yes, canceled bool) {
364                         if !canceled && yes {
365                                 h.Save()
366                                 h.Buf.ReOpen()
367                         } else if !canceled {
368                                 h.Buf.ReOpen()
369                         }
370                 })
371         } else {
372                 h.Buf.ReOpen()
373         }
374 }
375
376 func (h *BufPane) openHelp(page string) error {
377         if data, err := config.FindRuntimeFile(config.RTHelp, page).Data(); err != nil {
378                 return errors.New(fmt.Sprint("Unable to load help text", page, "\n", err))
379         } else {
380                 helpBuffer := buffer.NewBufferFromString(string(data), page+".md", buffer.BTHelp)
381                 helpBuffer.SetName("Help " + page)
382
383                 if h.Buf.Type == buffer.BTHelp {
384                         h.OpenBuffer(helpBuffer)
385                 } else {
386                         h.HSplitBuf(helpBuffer)
387                 }
388         }
389         return nil
390 }
391
392 // HelpCmd tries to open the given help page in a horizontal split
393 func (h *BufPane) HelpCmd(args []string) {
394         if len(args) < 1 {
395                 // Open the default help if the user just typed "> help"
396                 h.openHelp("help")
397         } else {
398                 if config.FindRuntimeFile(config.RTHelp, args[0]) != nil {
399                         err := h.openHelp(args[0])
400                         if err != nil {
401                                 InfoBar.Error(err)
402                         }
403                 } else {
404                         InfoBar.Error("Sorry, no help for ", args[0])
405                 }
406         }
407 }
408
409 // VSplitCmd opens a vertical split with file given in the first argument
410 // If no file is given, it opens an empty buffer in a new split
411 func (h *BufPane) VSplitCmd(args []string) {
412         if len(args) == 0 {
413                 // Open an empty vertical split
414                 h.VSplitAction()
415                 return
416         }
417
418         buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
419         if err != nil {
420                 InfoBar.Error(err)
421                 return
422         }
423
424         h.VSplitBuf(buf)
425 }
426
427 // HSplitCmd opens a horizontal split with file given in the first argument
428 // If no file is given, it opens an empty buffer in a new split
429 func (h *BufPane) HSplitCmd(args []string) {
430         if len(args) == 0 {
431                 // Open an empty horizontal split
432                 h.HSplitAction()
433                 return
434         }
435
436         buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
437         if err != nil {
438                 InfoBar.Error(err)
439                 return
440         }
441
442         h.HSplitBuf(buf)
443 }
444
445 // EvalCmd evaluates a lua expression
446 func (h *BufPane) EvalCmd(args []string) {
447         InfoBar.Error("Eval unsupported")
448 }
449
450 // NewTabCmd opens the given file in a new tab
451 func (h *BufPane) NewTabCmd(args []string) {
452         width, height := screen.Screen.Size()
453         iOffset := config.GetInfoBarOffset()
454         if len(args) > 0 {
455                 for _, a := range args {
456                         b, err := buffer.NewBufferFromFile(a, buffer.BTDefault)
457                         if err != nil {
458                                 InfoBar.Error(err)
459                                 return
460                         }
461                         tp := NewTabFromBuffer(0, 0, width, height-1-iOffset, b)
462                         Tabs.AddTab(tp)
463                         Tabs.SetActive(len(Tabs.List) - 1)
464                 }
465         } else {
466                 b := buffer.NewBufferFromString("", "", buffer.BTDefault)
467                 tp := NewTabFromBuffer(0, 0, width, height-iOffset, b)
468                 Tabs.AddTab(tp)
469                 Tabs.SetActive(len(Tabs.List) - 1)
470         }
471 }
472
473 func SetGlobalOptionNative(option string, nativeValue interface{}) error {
474         local := false
475         for _, s := range config.LocalSettings {
476                 if s == option {
477                         local = true
478                         break
479                 }
480         }
481
482         if !local {
483                 config.GlobalSettings[option] = nativeValue
484                 config.ModifiedSettings[option] = true
485
486                 if option == "colorscheme" {
487                         // LoadSyntaxFiles()
488                         config.InitColorscheme()
489                         for _, b := range buffer.OpenBuffers {
490                                 b.UpdateRules()
491                         }
492                 } else if option == "infobar" || option == "keymenu" {
493                         Tabs.Resize()
494                 } else if option == "mouse" {
495                         if !nativeValue.(bool) {
496                                 screen.Screen.DisableMouse()
497                         } else {
498                                 screen.Screen.EnableMouse()
499                         }
500                 } else if option == "autosave" {
501                         if nativeValue.(float64) > 0 {
502                                 config.SetAutoTime(int(nativeValue.(float64)))
503                                 config.StartAutoSave()
504                         } else {
505                                 config.SetAutoTime(0)
506                         }
507                 } else if option == "paste" {
508                         screen.Screen.SetPaste(nativeValue.(bool))
509                 } else if option == "clipboard" {
510                         m := clipboard.SetMethod(nativeValue.(string))
511                         err := clipboard.Initialize(m)
512                         if err != nil {
513                                 return err
514                         }
515                 } else {
516                         for _, pl := range config.Plugins {
517                                 if option == pl.Name {
518                                         if nativeValue.(bool) && !pl.Loaded {
519                                                 pl.Load()
520                                                 _, err := pl.Call("init")
521                                                 if err != nil && err != config.ErrNoSuchFunction {
522                                                         screen.TermMessage(err)
523                                                 }
524                                         } else if !nativeValue.(bool) && pl.Loaded {
525                                                 _, err := pl.Call("deinit")
526                                                 if err != nil && err != config.ErrNoSuchFunction {
527                                                         screen.TermMessage(err)
528                                                 }
529                                         }
530                                 }
531                         }
532                 }
533         }
534
535         for _, b := range buffer.OpenBuffers {
536                 b.SetOptionNative(option, nativeValue)
537         }
538
539         return config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
540 }
541
542 func SetGlobalOption(option, value string) error {
543         if _, ok := config.GlobalSettings[option]; !ok {
544                 return config.ErrInvalidOption
545         }
546
547         nativeValue, err := config.GetNativeValue(option, config.GlobalSettings[option], value)
548         if err != nil {
549                 return err
550         }
551
552         return SetGlobalOptionNative(option, nativeValue)
553 }
554
555 // ResetCmd resets a setting to its default value
556 func (h *BufPane) ResetCmd(args []string) {
557         if len(args) < 1 {
558                 InfoBar.Error("Not enough arguments")
559                 return
560         }
561
562         option := args[0]
563
564         defaultGlobals := config.DefaultGlobalSettings()
565         defaultLocals := config.DefaultCommonSettings()
566
567         if _, ok := defaultGlobals[option]; ok {
568                 SetGlobalOptionNative(option, defaultGlobals[option])
569                 return
570         }
571         if _, ok := defaultLocals[option]; ok {
572                 h.Buf.SetOptionNative(option, defaultLocals[option])
573                 return
574         }
575         InfoBar.Error(config.ErrInvalidOption)
576 }
577
578 // SetCmd sets an option
579 func (h *BufPane) SetCmd(args []string) {
580         if len(args) < 2 {
581                 InfoBar.Error("Not enough arguments")
582                 return
583         }
584
585         option := args[0]
586         value := args[1]
587
588         err := SetGlobalOption(option, value)
589         if err == config.ErrInvalidOption {
590                 err := h.Buf.SetOption(option, value)
591                 if err != nil {
592                         InfoBar.Error(err)
593                 }
594         } else if err != nil {
595                 InfoBar.Error(err)
596         }
597 }
598
599 // SetLocalCmd sets an option local to the buffer
600 func (h *BufPane) SetLocalCmd(args []string) {
601         if len(args) < 2 {
602                 InfoBar.Error("Not enough arguments")
603                 return
604         }
605
606         option := args[0]
607         value := args[1]
608
609         err := h.Buf.SetOption(option, value)
610         if err != nil {
611                 InfoBar.Error(err)
612         }
613 }
614
615 // ShowCmd shows the value of the given option
616 func (h *BufPane) ShowCmd(args []string) {
617         if len(args) < 1 {
618                 InfoBar.Error("Please provide an option to show")
619                 return
620         }
621
622         var option interface{}
623         if opt, ok := h.Buf.Settings[args[0]]; ok {
624                 option = opt
625         } else if opt, ok := config.GlobalSettings[args[0]]; ok {
626                 option = opt
627         }
628
629         if option == nil {
630                 InfoBar.Error(args[0], " is not a valid option")
631                 return
632         }
633
634         InfoBar.Message(option)
635 }
636
637 // ShowKeyCmd displays the action that a key is bound to
638 func (h *BufPane) ShowKeyCmd(args []string) {
639         if len(args) < 1 {
640                 InfoBar.Error("Please provide a key to show")
641                 return
642         }
643
644         event, err := findEvent(args[0])
645         if err != nil {
646                 InfoBar.Error(err)
647                 return
648         }
649         if action, ok := config.Bindings["buffer"][event.Name()]; ok {
650                 InfoBar.Message(action)
651         } else {
652                 InfoBar.Message(args[0], " has no binding")
653         }
654 }
655
656 // BindCmd creates a new keybinding
657 func (h *BufPane) BindCmd(args []string) {
658         if len(args) < 2 {
659                 InfoBar.Error("Not enough arguments")
660                 return
661         }
662
663         _, err := TryBindKey(args[0], args[1], true)
664         if err != nil {
665                 InfoBar.Error(err)
666         }
667 }
668
669 // UnbindCmd binds a key to its default action
670 func (h *BufPane) UnbindCmd(args []string) {
671         if len(args) < 1 {
672                 InfoBar.Error("Not enough arguments")
673                 return
674         }
675
676         err := UnbindKey(args[0])
677         if err != nil {
678                 InfoBar.Error(err)
679         }
680 }
681
682 // RunCmd runs a shell command in the background
683 func (h *BufPane) RunCmd(args []string) {
684         runf, err := shell.RunBackgroundShell(shellquote.Join(args...))
685         if err != nil {
686                 InfoBar.Error(err)
687         } else {
688                 go func() {
689                         InfoBar.Message(runf())
690                         screen.Redraw()
691                 }()
692         }
693 }
694
695 // QuitCmd closes the main view
696 func (h *BufPane) QuitCmd(args []string) {
697         h.Quit()
698 }
699
700 // GotoCmd is a command that will send the cursor to a certain
701 // position in the buffer
702 // For example: `goto line`, or `goto line:col`
703 func (h *BufPane) GotoCmd(args []string) {
704         if len(args) <= 0 {
705                 InfoBar.Error("Not enough arguments")
706         } else {
707                 h.RemoveAllMultiCursors()
708                 if strings.Contains(args[0], ":") {
709                         parts := strings.SplitN(args[0], ":", 2)
710                         line, err := strconv.Atoi(parts[0])
711                         if err != nil {
712                                 InfoBar.Error(err)
713                                 return
714                         }
715                         col, err := strconv.Atoi(parts[1])
716                         if err != nil {
717                                 InfoBar.Error(err)
718                                 return
719                         }
720                         if line < 0 {
721                                 line = h.Buf.LinesNum() + 1 + line
722                         }
723                         line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
724                         col = util.Clamp(col-1, 0, util.CharacterCount(h.Buf.LineBytes(line)))
725                         h.Cursor.GotoLoc(buffer.Loc{col, line})
726                 } else {
727                         line, err := strconv.Atoi(args[0])
728                         if err != nil {
729                                 InfoBar.Error(err)
730                                 return
731                         }
732                         if line < 0 {
733                                 line = h.Buf.LinesNum() + 1 + line
734                         }
735                         line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
736                         h.Cursor.GotoLoc(buffer.Loc{0, line})
737                 }
738                 h.Relocate()
739         }
740 }
741
742 // SaveCmd saves the buffer optionally with an argument file name
743 func (h *BufPane) SaveCmd(args []string) {
744         if len(args) == 0 {
745                 h.Save()
746         } else {
747                 h.Buf.SaveAs(args[0])
748         }
749 }
750
751 // ReplaceCmd runs search and replace
752 func (h *BufPane) ReplaceCmd(args []string) {
753         if len(args) < 2 || len(args) > 4 {
754                 // We need to find both a search and replace expression
755                 InfoBar.Error("Invalid replace statement: " + strings.Join(args, " "))
756                 return
757         }
758
759         all := false
760         noRegex := false
761
762         foundSearch := false
763         foundReplace := false
764         var search string
765         var replaceStr string
766         for _, arg := range args {
767                 switch arg {
768                 case "-a":
769                         all = true
770                 case "-l":
771                         noRegex = true
772                 default:
773                         if !foundSearch {
774                                 foundSearch = true
775                                 search = arg
776                         } else if !foundReplace {
777                                 foundReplace = true
778                                 replaceStr = arg
779                         } else {
780                                 InfoBar.Error("Invalid flag: " + arg)
781                                 return
782                         }
783                 }
784         }
785
786         if noRegex {
787                 search = regexp.QuoteMeta(search)
788         }
789
790         replace := []byte(replaceStr)
791
792         var regex *regexp.Regexp
793         var err error
794         if h.Buf.Settings["ignorecase"].(bool) {
795                 regex, err = regexp.Compile("(?im)" + search)
796         } else {
797                 regex, err = regexp.Compile("(?m)" + search)
798         }
799         if err != nil {
800                 // There was an error with the user's regex
801                 InfoBar.Error(err)
802                 return
803         }
804
805         nreplaced := 0
806         start := h.Buf.Start()
807         end := h.Buf.End()
808         selection := h.Cursor.HasSelection()
809         if selection {
810                 start = h.Cursor.CurSelection[0]
811                 end = h.Cursor.CurSelection[1]
812         }
813         if all {
814                 nreplaced, _ = h.Buf.ReplaceRegex(start, end, regex, replace)
815         } else {
816                 inRange := func(l buffer.Loc) bool {
817                         return l.GreaterEqual(start) && l.LessEqual(end)
818                 }
819
820                 searchLoc := h.Cursor.Loc
821                 var doReplacement func()
822                 doReplacement = func() {
823                         locs, found, err := h.Buf.FindNext(search, start, end, searchLoc, true, true)
824                         if err != nil {
825                                 InfoBar.Error(err)
826                                 return
827                         }
828                         if !found || !inRange(locs[0]) || !inRange(locs[1]) {
829                                 h.Cursor.ResetSelection()
830                                 h.Buf.RelocateCursors()
831
832                                 return
833                         }
834
835                         h.Cursor.SetSelectionStart(locs[0])
836                         h.Cursor.SetSelectionEnd(locs[1])
837                         h.Cursor.GotoLoc(locs[0])
838
839                         h.Relocate()
840
841                         InfoBar.YNPrompt("Perform replacement (y,n,esc)", func(yes, canceled bool) {
842                                 if !canceled && yes {
843                                         _, nrunes := h.Buf.ReplaceRegex(locs[0], locs[1], regex, replace)
844
845                                         searchLoc = locs[0]
846                                         searchLoc.X += nrunes + locs[0].Diff(locs[1], h.Buf)
847                                         if end.Y == locs[1].Y {
848                                                 end = end.Move(nrunes, h.Buf)
849                                         }
850                                         h.Cursor.Loc = searchLoc
851                                         nreplaced++
852                                 } else if !canceled && !yes {
853                                         searchLoc = locs[0]
854                                         searchLoc.X += util.CharacterCount(replace)
855                                 } else if canceled {
856                                         h.Cursor.ResetSelection()
857                                         h.Buf.RelocateCursors()
858                                         return
859                                 }
860                                 doReplacement()
861                         })
862                 }
863                 doReplacement()
864         }
865
866         h.Buf.RelocateCursors()
867         h.Relocate()
868
869         var s string
870         if nreplaced > 1 {
871                 s = fmt.Sprintf("Replaced %d occurrences of %s", nreplaced, search)
872         } else if nreplaced == 1 {
873                 s = fmt.Sprintf("Replaced 1 occurrence of %s", search)
874         } else {
875                 s = fmt.Sprintf("Nothing matched %s", search)
876         }
877
878         if selection {
879                 s += " in selection"
880         }
881
882         InfoBar.Message(s)
883 }
884
885 // ReplaceAllCmd replaces search term all at once
886 func (h *BufPane) ReplaceAllCmd(args []string) {
887         // aliased to Replace command
888         h.ReplaceCmd(append(args, "-a"))
889 }
890
891 // TermCmd opens a terminal in the current view
892 func (h *BufPane) TermCmd(args []string) {
893         ps := h.tab.Panes
894
895         if !TermEmuSupported {
896                 InfoBar.Error("Terminal emulator not supported on this system")
897                 return
898         }
899
900         if len(args) == 0 {
901                 sh := os.Getenv("SHELL")
902                 if sh == "" {
903                         InfoBar.Error("Shell environment not found")
904                         return
905                 }
906                 args = []string{sh}
907         }
908
909         term := func(i int, newtab bool) {
910                 t := new(shell.Terminal)
911                 err := t.Start(args, false, true, nil, nil)
912                 if err != nil {
913                         InfoBar.Error(err)
914                         return
915                 }
916
917                 id := h.ID()
918                 if newtab {
919                         h.AddTab()
920                         i = 0
921                         id = MainTab().Panes[0].ID()
922                 } else {
923                         MainTab().Panes[i].Close()
924                 }
925
926                 v := h.GetView()
927                 tp, err := NewTermPane(v.X, v.Y, v.Width, v.Height, t, id, MainTab())
928                 if err != nil {
929                         InfoBar.Error(err)
930                         return
931                 }
932                 MainTab().Panes[i] = tp
933                 MainTab().SetActive(i)
934         }
935
936         // If there is only one open file we make a new tab instead of overwriting it
937         newtab := len(MainTab().Panes) == 1 && len(Tabs.List) == 1
938
939         if newtab {
940                 term(0, true)
941                 return
942         }
943
944         for i, p := range ps {
945                 if p.ID() == h.ID() {
946                         if h.Buf.Modified() {
947                                 InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
948                                         if !canceled && !yes {
949                                                 term(i, false)
950                                         } else if !canceled && yes {
951                                                 h.Save()
952                                                 term(i, false)
953                                         }
954                                 })
955                         } else {
956                                 term(i, false)
957                         }
958                 }
959         }
960 }
961
962 // HandleCommand handles input from the user
963 func (h *BufPane) HandleCommand(input string) {
964         args, err := shellquote.Split(input)
965         if err != nil {
966                 InfoBar.Error("Error parsing args ", err)
967                 return
968         }
969
970         if len(args) == 0 {
971                 return
972         }
973
974         inputCmd := args[0]
975
976         if _, ok := commands[inputCmd]; !ok {
977                 InfoBar.Error("Unknown command ", inputCmd)
978         } else {
979                 WriteLog("> " + input + "\n")
980                 commands[inputCmd].action(h, args[1:])
981                 WriteLog("\n")
982         }
983 }