]> git.lizzy.rs Git - micro.git/blob - internal/action/command.go
Add support for copy-paste via OSC 52
[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         if action, ok := config.Bindings[args[0]]; ok {
645                 InfoBar.Message(action)
646         } else {
647                 InfoBar.Message(args[0], " has no binding")
648         }
649 }
650
651 // BindCmd creates a new keybinding
652 func (h *BufPane) BindCmd(args []string) {
653         if len(args) < 2 {
654                 InfoBar.Error("Not enough arguments")
655                 return
656         }
657
658         _, err := TryBindKey(args[0], args[1], true)
659         if err != nil {
660                 InfoBar.Error(err)
661         }
662 }
663
664 // UnbindCmd binds a key to its default action
665 func (h *BufPane) UnbindCmd(args []string) {
666         if len(args) < 1 {
667                 InfoBar.Error("Not enough arguments")
668                 return
669         }
670
671         err := UnbindKey(args[0])
672         if err != nil {
673                 InfoBar.Error(err)
674         }
675 }
676
677 // RunCmd runs a shell command in the background
678 func (h *BufPane) RunCmd(args []string) {
679         runf, err := shell.RunBackgroundShell(shellquote.Join(args...))
680         if err != nil {
681                 InfoBar.Error(err)
682         } else {
683                 go func() {
684                         InfoBar.Message(runf())
685                         screen.Redraw()
686                 }()
687         }
688 }
689
690 // QuitCmd closes the main view
691 func (h *BufPane) QuitCmd(args []string) {
692         h.Quit()
693 }
694
695 // GotoCmd is a command that will send the cursor to a certain
696 // position in the buffer
697 // For example: `goto line`, or `goto line:col`
698 func (h *BufPane) GotoCmd(args []string) {
699         if len(args) <= 0 {
700                 InfoBar.Error("Not enough arguments")
701         } else {
702                 h.RemoveAllMultiCursors()
703                 if strings.Contains(args[0], ":") {
704                         parts := strings.SplitN(args[0], ":", 2)
705                         line, err := strconv.Atoi(parts[0])
706                         if err != nil {
707                                 InfoBar.Error(err)
708                                 return
709                         }
710                         col, err := strconv.Atoi(parts[1])
711                         if err != nil {
712                                 InfoBar.Error(err)
713                                 return
714                         }
715                         if line < 0 {
716                                 line = h.Buf.LinesNum() + 1 + line
717                         }
718                         line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
719                         col = util.Clamp(col-1, 0, util.CharacterCount(h.Buf.LineBytes(line)))
720                         h.Cursor.GotoLoc(buffer.Loc{col, line})
721                 } else {
722                         line, err := strconv.Atoi(args[0])
723                         if err != nil {
724                                 InfoBar.Error(err)
725                                 return
726                         }
727                         if line < 0 {
728                                 line = h.Buf.LinesNum() + 1 + line
729                         }
730                         line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
731                         h.Cursor.GotoLoc(buffer.Loc{0, line})
732                 }
733                 h.Relocate()
734         }
735 }
736
737 // SaveCmd saves the buffer optionally with an argument file name
738 func (h *BufPane) SaveCmd(args []string) {
739         if len(args) == 0 {
740                 h.Save()
741         } else {
742                 h.Buf.SaveAs(args[0])
743         }
744 }
745
746 // ReplaceCmd runs search and replace
747 func (h *BufPane) ReplaceCmd(args []string) {
748         if len(args) < 2 || len(args) > 4 {
749                 // We need to find both a search and replace expression
750                 InfoBar.Error("Invalid replace statement: " + strings.Join(args, " "))
751                 return
752         }
753
754         all := false
755         noRegex := false
756
757         foundSearch := false
758         foundReplace := false
759         var search string
760         var replaceStr string
761         for _, arg := range args {
762                 switch arg {
763                 case "-a":
764                         all = true
765                 case "-l":
766                         noRegex = true
767                 default:
768                         if !foundSearch {
769                                 foundSearch = true
770                                 search = arg
771                         } else if !foundReplace {
772                                 foundReplace = true
773                                 replaceStr = arg
774                         } else {
775                                 InfoBar.Error("Invalid flag: " + arg)
776                                 return
777                         }
778                 }
779         }
780
781         if noRegex {
782                 search = regexp.QuoteMeta(search)
783         }
784
785         replace := []byte(replaceStr)
786
787         var regex *regexp.Regexp
788         var err error
789         if h.Buf.Settings["ignorecase"].(bool) {
790                 regex, err = regexp.Compile("(?im)" + search)
791         } else {
792                 regex, err = regexp.Compile("(?m)" + search)
793         }
794         if err != nil {
795                 // There was an error with the user's regex
796                 InfoBar.Error(err)
797                 return
798         }
799
800         nreplaced := 0
801         start := h.Buf.Start()
802         end := h.Buf.End()
803         selection := h.Cursor.HasSelection()
804         if selection {
805                 start = h.Cursor.CurSelection[0]
806                 end = h.Cursor.CurSelection[1]
807         }
808         if all {
809                 nreplaced, _ = h.Buf.ReplaceRegex(start, end, regex, replace)
810         } else {
811                 inRange := func(l buffer.Loc) bool {
812                         return l.GreaterEqual(start) && l.LessEqual(end)
813                 }
814
815                 searchLoc := h.Cursor.Loc
816                 var doReplacement func()
817                 doReplacement = func() {
818                         locs, found, err := h.Buf.FindNext(search, start, end, searchLoc, true, !noRegex)
819                         if err != nil {
820                                 InfoBar.Error(err)
821                                 return
822                         }
823                         if !found || !inRange(locs[0]) || !inRange(locs[1]) {
824                                 h.Cursor.ResetSelection()
825                                 h.Buf.RelocateCursors()
826
827                                 return
828                         }
829
830                         h.Cursor.SetSelectionStart(locs[0])
831                         h.Cursor.SetSelectionEnd(locs[1])
832                         h.Cursor.GotoLoc(locs[0])
833
834                         h.Relocate()
835
836                         InfoBar.YNPrompt("Perform replacement (y,n,esc)", func(yes, canceled bool) {
837                                 if !canceled && yes {
838                                         _, nrunes := h.Buf.ReplaceRegex(locs[0], locs[1], regex, replace)
839
840                                         searchLoc = locs[0]
841                                         searchLoc.X += nrunes + locs[0].Diff(locs[1], h.Buf)
842                                         if end.Y == locs[1].Y {
843                                                 end = end.Move(nrunes, h.Buf)
844                                         }
845                                         h.Cursor.Loc = searchLoc
846                                         nreplaced++
847                                 } else if !canceled && !yes {
848                                         searchLoc = locs[0]
849                                         searchLoc.X += util.CharacterCount(replace)
850                                 } else if canceled {
851                                         h.Cursor.ResetSelection()
852                                         h.Buf.RelocateCursors()
853                                         return
854                                 }
855                                 doReplacement()
856                         })
857                 }
858                 doReplacement()
859         }
860
861         h.Buf.RelocateCursors()
862         h.Relocate()
863
864         var s string
865         if nreplaced > 1 {
866                 s = fmt.Sprintf("Replaced %d occurrences of %s", nreplaced, search)
867         } else if nreplaced == 1 {
868                 s = fmt.Sprintf("Replaced 1 occurrence of %s", search)
869         } else {
870                 s = fmt.Sprintf("Nothing matched %s", search)
871         }
872
873         if selection {
874                 s += " in selection"
875         }
876
877         InfoBar.Message(s)
878 }
879
880 // ReplaceAllCmd replaces search term all at once
881 func (h *BufPane) ReplaceAllCmd(args []string) {
882         // aliased to Replace command
883         h.ReplaceCmd(append(args, "-a"))
884 }
885
886 // TermCmd opens a terminal in the current view
887 func (h *BufPane) TermCmd(args []string) {
888         ps := h.tab.Panes
889
890         if !TermEmuSupported {
891                 InfoBar.Error("Terminal emulator not supported on this system")
892                 return
893         }
894
895         if len(args) == 0 {
896                 sh := os.Getenv("SHELL")
897                 if sh == "" {
898                         InfoBar.Error("Shell environment not found")
899                         return
900                 }
901                 args = []string{sh}
902         }
903
904         term := func(i int, newtab bool) {
905                 t := new(shell.Terminal)
906                 err := t.Start(args, false, true, nil, nil)
907                 if err != nil {
908                         InfoBar.Error(err)
909                         return
910                 }
911
912                 id := h.ID()
913                 if newtab {
914                         h.AddTab()
915                         i = 0
916                         id = MainTab().Panes[0].ID()
917                 } else {
918                         MainTab().Panes[i].Close()
919                 }
920
921                 v := h.GetView()
922                 tp, err := NewTermPane(v.X, v.Y, v.Width, v.Height, t, id, MainTab())
923                 if err != nil {
924                         InfoBar.Error(err)
925                         return
926                 }
927                 MainTab().Panes[i] = tp
928                 MainTab().SetActive(i)
929         }
930
931         // If there is only one open file we make a new tab instead of overwriting it
932         newtab := len(MainTab().Panes) == 1 && len(Tabs.List) == 1
933
934         if newtab {
935                 term(0, true)
936                 return
937         }
938
939         for i, p := range ps {
940                 if p.ID() == h.ID() {
941                         if h.Buf.Modified() {
942                                 InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
943                                         if !canceled && !yes {
944                                                 term(i, false)
945                                         } else if !canceled && yes {
946                                                 h.Save()
947                                                 term(i, false)
948                                         }
949                                 })
950                         } else {
951                                 term(i, false)
952                         }
953                 }
954         }
955 }
956
957 // HandleCommand handles input from the user
958 func (h *BufPane) HandleCommand(input string) {
959         args, err := shellquote.Split(input)
960         if err != nil {
961                 InfoBar.Error("Error parsing args ", err)
962                 return
963         }
964
965         if len(args) == 0 {
966                 return
967         }
968
969         inputCmd := args[0]
970
971         if _, ok := commands[inputCmd]; !ok {
972                 InfoBar.Error("Unknown command ", inputCmd)
973         } else {
974                 WriteLog("> " + input + "\n")
975                 commands[inputCmd].action(h, args[1:])
976                 WriteLog("\n")
977         }
978 }