]> git.lizzy.rs Git - micro.git/blob - cmd/micro/command.go
Merge pull request #564 from DanielPower/master
[micro.git] / cmd / micro / command.go
1 package main
2
3 import (
4         "bytes"
5         "fmt"
6         "io"
7         "os"
8         "os/exec"
9         "os/signal"
10         "path/filepath"
11         "regexp"
12         "strconv"
13         "strings"
14
15         "github.com/mitchellh/go-homedir"
16 )
17
18 type Command struct {
19         action      func([]string)
20         completions []Completion
21 }
22
23 type StrCommand struct {
24         action      string
25         completions []Completion
26 }
27
28 var commands map[string]Command
29
30 var commandActions map[string]func([]string)
31
32 func init() {
33         commandActions = map[string]func([]string){
34                 "Set":       Set,
35                 "SetLocal":  SetLocal,
36                 "Show":      Show,
37                 "Run":       Run,
38                 "Bind":      Bind,
39                 "Quit":      Quit,
40                 "Save":      Save,
41                 "Replace":   Replace,
42                 "VSplit":    VSplit,
43                 "HSplit":    HSplit,
44                 "Tab":       NewTab,
45                 "Help":      Help,
46                 "Eval":      Eval,
47                 "ToggleLog": ToggleLog,
48                 "Plugin":    PluginCmd,
49                 "Reload":    Reload,
50                 "Cd":        Cd,
51                 "Pwd":       Pwd,
52                 "Open":      Open,
53                 "TabSwitch": TabSwitch,
54         }
55 }
56
57 // InitCommands initializes the default commands
58 func InitCommands() {
59         commands = make(map[string]Command)
60
61         defaults := DefaultCommands()
62         parseCommands(defaults)
63 }
64
65 func parseCommands(userCommands map[string]StrCommand) {
66         for k, v := range userCommands {
67                 MakeCommand(k, v.action, v.completions...)
68         }
69 }
70
71 // MakeCommand is a function to easily create new commands
72 // This can be called by plugins in Lua so that plugins can define their own commands
73 func MakeCommand(name, function string, completions ...Completion) {
74         action := commandActions[function]
75         if _, ok := commandActions[function]; !ok {
76                 // If the user seems to be binding a function that doesn't exist
77                 // We hope that it's a lua function that exists and bind it to that
78                 action = LuaFunctionCommand(function)
79         }
80
81         commands[name] = Command{action, completions}
82 }
83
84 // DefaultCommands returns a map containing micro's default commands
85 func DefaultCommands() map[string]StrCommand {
86         return map[string]StrCommand{
87                 "set":       {"Set", []Completion{OptionCompletion, NoCompletion}},
88                 "setlocal":  {"SetLocal", []Completion{OptionCompletion, NoCompletion}},
89                 "show":      {"Show", []Completion{OptionCompletion, NoCompletion}},
90                 "bind":      {"Bind", []Completion{NoCompletion}},
91                 "run":       {"Run", []Completion{NoCompletion}},
92                 "quit":      {"Quit", []Completion{NoCompletion}},
93                 "save":      {"Save", []Completion{NoCompletion}},
94                 "replace":   {"Replace", []Completion{NoCompletion}},
95                 "vsplit":    {"VSplit", []Completion{FileCompletion, NoCompletion}},
96                 "hsplit":    {"HSplit", []Completion{FileCompletion, NoCompletion}},
97                 "tab":       {"Tab", []Completion{FileCompletion, NoCompletion}},
98                 "help":      {"Help", []Completion{HelpCompletion, NoCompletion}},
99                 "eval":      {"Eval", []Completion{NoCompletion}},
100                 "log":       {"ToggleLog", []Completion{NoCompletion}},
101                 "plugin":    {"Plugin", []Completion{PluginCmdCompletion, PluginNameCompletion}},
102                 "reload":    {"Reload", []Completion{NoCompletion}},
103                 "cd":        {"Cd", []Completion{FileCompletion}},
104                 "pwd":       {"Pwd", []Completion{NoCompletion}},
105                 "open":      {"Open", []Completion{FileCompletion}},
106                 "tabswitch": {"TabSwitch", []Completion{NoCompletion}},
107         }
108 }
109
110 // PluginCmd installs, removes, updates, lists, or searches for given plugins
111 func PluginCmd(args []string) {
112         if len(args) >= 1 {
113                 switch args[0] {
114                 case "install":
115                         installedVersions := GetInstalledVersions(false)
116                         for _, plugin := range args[1:] {
117                                 pp := GetAllPluginPackages().Get(plugin)
118                                 if pp == nil {
119                                         messenger.Error("Unknown plugin \"" + plugin + "\"")
120                                 } else if err := pp.IsInstallable(); err != nil {
121                                         messenger.Error("Error installing ", plugin, ": ", err)
122                                 } else {
123                                         for _, installed := range installedVersions {
124                                                 if pp.Name == installed.pack.Name {
125                                                         if pp.Versions[0].Version.Compare(installed.Version) == 1 {
126                                                                 messenger.Error(pp.Name, " is already installed but out-of-date: use 'plugin update ", pp.Name, "' to update")
127                                                         } else {
128                                                                 messenger.Error(pp.Name, " is already installed")
129                                                         }
130                                                 }
131                                         }
132                                         pp.Install()
133                                 }
134                         }
135                 case "remove":
136                         removed := ""
137                         for _, plugin := range args[1:] {
138                                 // check if the plugin exists.
139                                 if _, ok := loadedPlugins[plugin]; ok {
140                                         UninstallPlugin(plugin)
141                                         removed += plugin + " "
142                                         continue
143                                 }
144                         }
145                         if !IsSpaces(removed) {
146                                 messenger.Message("Removed ", removed)
147                         } else {
148                                 messenger.Error("The requested plugins do not exist")
149                         }
150                 case "update":
151                         UpdatePlugins(args[1:])
152                 case "list":
153                         plugins := GetInstalledVersions(false)
154                         messenger.AddLog("----------------")
155                         messenger.AddLog("The following plugins are currently installed:\n")
156                         for _, p := range plugins {
157                                 messenger.AddLog(fmt.Sprintf("%s (%s)", p.pack.Name, p.Version))
158                         }
159                         messenger.AddLog("----------------")
160                         if len(plugins) > 0 {
161                                 if CurView().Type != vtLog {
162                                         ToggleLog([]string{})
163                                 }
164                         }
165                 case "search":
166                         plugins := SearchPlugin(args[1:])
167                         messenger.Message(len(plugins), " plugins found")
168                         for _, p := range plugins {
169                                 messenger.AddLog("----------------")
170                                 messenger.AddLog(p.String())
171                         }
172                         messenger.AddLog("----------------")
173                         if len(plugins) > 0 {
174                                 if CurView().Type != vtLog {
175                                         ToggleLog([]string{})
176                                 }
177                         }
178                 case "available":
179                         packages := GetAllPluginPackages()
180                         messenger.AddLog("Available Plugins:")
181                         for _, pkg := range packages {
182                                 messenger.AddLog(pkg.Name)
183                         }
184                         if CurView().Type != vtLog {
185                                 ToggleLog([]string{})
186                         }
187                 }
188         } else {
189                 messenger.Error("Not enough arguments")
190         }
191 }
192
193 func TabSwitch(args []string) {
194         if len(args) > 0 {
195                 num, err := strconv.Atoi(args[0])
196                 if err != nil {
197                         // Check for tab with this name
198
199                         found := false
200                         for _, t := range tabs {
201                                 v := t.views[t.CurView]
202                                 if v.Buf.GetName() == args[0] {
203                                         curTab = v.Num
204                                         found = true
205                                 }
206                         }
207                         if !found {
208                                 messenger.Error("Could not find tab: ", err)
209                         }
210                 } else {
211                         num--
212                         if num >= 0 && num < len(tabs) {
213                                 curTab = num
214                         } else {
215                                 messenger.Error("Invalid tab index")
216                         }
217                 }
218         }
219 }
220
221 func Cd(args []string) {
222         if len(args) > 0 {
223                 home, _ := homedir.Dir()
224                 path := strings.Replace(args[0], "~", home, 1)
225                 os.Chdir(path)
226                 for _, tab := range tabs {
227                         for _, view := range tab.views {
228                                 wd, _ := os.Getwd()
229                                 view.Buf.Path, _ = MakeRelative(view.Buf.AbsPath, wd)
230                                 if p, _ := filepath.Abs(view.Buf.Path); !strings.Contains(p, wd) {
231                                         view.Buf.Path = view.Buf.AbsPath
232                                 }
233                         }
234                 }
235         }
236 }
237
238 func Pwd(args []string) {
239         wd, err := os.Getwd()
240         if err != nil {
241                 messenger.Message(err.Error())
242         } else {
243                 messenger.Message(wd)
244         }
245 }
246
247 func Open(args []string) {
248         if len(args) > 0 {
249                 filename := args[0]
250                 // the filename might or might not be quoted, so unquote first then join the strings.
251                 filename = strings.Join(SplitCommandArgs(filename), " ")
252
253                 CurView().Open(filename)
254         } else {
255                 messenger.Error("No filename")
256         }
257 }
258
259 func ToggleLog(args []string) {
260         buffer := messenger.getBuffer()
261         if CurView().Type != vtLog {
262                 CurView().HSplit(buffer)
263                 CurView().Type = vtLog
264                 RedrawAll()
265                 buffer.Cursor.Loc = buffer.Start()
266                 CurView().Relocate()
267                 buffer.Cursor.Loc = buffer.End()
268                 CurView().Relocate()
269         } else {
270                 CurView().Quit(true)
271         }
272 }
273
274 func Reload(args []string) {
275         LoadAll()
276 }
277
278 // Help tries to open the given help page in a horizontal split
279 func Help(args []string) {
280         if len(args) < 1 {
281                 // Open the default help if the user just typed "> help"
282                 CurView().openHelp("help")
283         } else {
284                 helpPage := args[0]
285                 if FindRuntimeFile(RTHelp, helpPage) != nil {
286                         CurView().openHelp(helpPage)
287                 } else {
288                         messenger.Error("Sorry, no help for ", helpPage)
289                 }
290         }
291 }
292
293 // VSplit opens a vertical split with file given in the first argument
294 // If no file is given, it opens an empty buffer in a new split
295 func VSplit(args []string) {
296         if len(args) == 0 {
297                 CurView().VSplit(NewBuffer(strings.NewReader(""), ""))
298         } else {
299                 filename := args[0]
300                 home, _ := homedir.Dir()
301                 filename = strings.Replace(filename, "~", home, 1)
302                 file, err := os.Open(filename)
303                 defer file.Close()
304
305                 var buf *Buffer
306                 if err != nil {
307                         // File does not exist -- create an empty buffer with that name
308                         buf = NewBuffer(strings.NewReader(""), filename)
309                 } else {
310                         buf = NewBuffer(file, filename)
311                 }
312                 CurView().VSplit(buf)
313         }
314 }
315
316 // HSplit opens a horizontal split with file given in the first argument
317 // If no file is given, it opens an empty buffer in a new split
318 func HSplit(args []string) {
319         if len(args) == 0 {
320                 CurView().HSplit(NewBuffer(strings.NewReader(""), ""))
321         } else {
322                 filename := args[0]
323                 home, _ := homedir.Dir()
324                 filename = strings.Replace(filename, "~", home, 1)
325                 file, err := os.Open(filename)
326                 defer file.Close()
327
328                 var buf *Buffer
329                 if err != nil {
330                         // File does not exist -- create an empty buffer with that name
331                         buf = NewBuffer(strings.NewReader(""), filename)
332                 } else {
333                         buf = NewBuffer(file, filename)
334                 }
335                 CurView().HSplit(buf)
336         }
337 }
338
339 // Eval evaluates a lua expression
340 func Eval(args []string) {
341         if len(args) >= 1 {
342                 err := L.DoString(args[0])
343                 if err != nil {
344                         messenger.Error(err)
345                 }
346         } else {
347                 messenger.Error("Not enough arguments")
348         }
349 }
350
351 // NewTab opens the given file in a new tab
352 func NewTab(args []string) {
353         if len(args) == 0 {
354                 CurView().AddTab(true)
355         } else {
356                 filename := args[0]
357                 home, _ := homedir.Dir()
358                 filename = strings.Replace(filename, "~", home, 1)
359                 file, _ := os.Open(filename)
360                 defer file.Close()
361
362                 tab := NewTabFromView(NewView(NewBuffer(file, filename)))
363                 tab.SetNum(len(tabs))
364                 tabs = append(tabs, tab)
365                 curTab = len(tabs) - 1
366                 if len(tabs) == 2 {
367                         for _, t := range tabs {
368                                 for _, v := range t.views {
369                                         v.ToggleTabbar()
370                                 }
371                         }
372                 }
373         }
374 }
375
376 // Set sets an option
377 func Set(args []string) {
378         if len(args) < 2 {
379                 messenger.Error("Not enough arguments")
380                 return
381         }
382
383         option := args[0]
384         value := args[1]
385
386         SetOptionAndSettings(option, value)
387 }
388
389 // SetLocal sets an option local to the buffer
390 func SetLocal(args []string) {
391         if len(args) < 2 {
392                 messenger.Error("Not enough arguments")
393                 return
394         }
395
396         option := args[0]
397         value := args[1]
398
399         err := SetLocalOption(option, value, CurView())
400         if err != nil {
401                 messenger.Error(err.Error())
402         }
403 }
404
405 // Show shows the value of the given option
406 func Show(args []string) {
407         if len(args) < 1 {
408                 messenger.Error("Please provide an option to show")
409                 return
410         }
411
412         option := GetOption(args[0])
413
414         if option == nil {
415                 messenger.Error(args[0], " is not a valid option")
416                 return
417         }
418
419         messenger.Message(option)
420 }
421
422 // Bind creates a new keybinding
423 func Bind(args []string) {
424         if len(args) < 2 {
425                 messenger.Error("Not enough arguments")
426                 return
427         }
428         BindKey(args[0], args[1])
429 }
430
431 // Run runs a shell command in the background
432 func Run(args []string) {
433         // Run a shell command in the background (openTerm is false)
434         HandleShellCommand(JoinCommandArgs(args...), false, true)
435 }
436
437 // Quit closes the main view
438 func Quit(args []string) {
439         // Close the main view
440         CurView().Quit(true)
441 }
442
443 // Save saves the buffer in the main view
444 func Save(args []string) {
445         if len(args) == 0 {
446                 // Save the main view
447                 CurView().Save(true)
448         } else {
449                 CurView().Buf.SaveAs(args[0])
450         }
451 }
452
453 // Replace runs search and replace
454 func Replace(args []string) {
455         if len(args) < 2 {
456                 // We need to find both a search and replace expression
457                 messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
458                 return
459         }
460
461         var flags string
462         if len(args) == 3 {
463                 // The user included some flags
464                 flags = args[2]
465         }
466
467         search := string(args[0])
468         replace := string(args[1])
469
470         regex, err := regexp.Compile("(?m)" + search)
471         if err != nil {
472                 // There was an error with the user's regex
473                 messenger.Error(err.Error())
474                 return
475         }
476
477         view := CurView()
478
479         found := 0
480         if strings.Contains(flags, "c") {
481                 for {
482                         // The 'check' flag was used
483                         Search(search, view, true)
484                         if !view.Cursor.HasSelection() {
485                                 break
486                         }
487                         view.Relocate()
488                         if view.Buf.Settings["syntax"].(bool) {
489                                 view.matches = Match(view)
490                         }
491                         RedrawAll()
492                         choice, canceled := messenger.YesNoPrompt("Perform replacement? (y,n)")
493                         if canceled {
494                                 if view.Cursor.HasSelection() {
495                                         view.Cursor.Loc = view.Cursor.CurSelection[0]
496                                         view.Cursor.ResetSelection()
497                                 }
498                                 messenger.Reset()
499                                 break
500                         }
501                         if choice {
502                                 view.Cursor.DeleteSelection()
503                                 view.Buf.Insert(view.Cursor.Loc, replace)
504                                 view.Cursor.ResetSelection()
505                                 messenger.Reset()
506                                 found++
507                         } else {
508                                 if view.Cursor.HasSelection() {
509                                         searchStart = ToCharPos(view.Cursor.CurSelection[1], view.Buf)
510                                 } else {
511                                         searchStart = ToCharPos(view.Cursor.Loc, view.Buf)
512                                 }
513                                 continue
514                         }
515                 }
516         } else {
517                 bufStr := view.Buf.String()
518                 matches := regex.FindAllStringIndex(bufStr, -1)
519                 if matches != nil && len(matches) > 0 {
520                         prevMatchCount := runePos(matches[0][0], bufStr)
521                         searchCount := runePos(matches[0][1], bufStr) - prevMatchCount
522                         from := FromCharPos(matches[0][0], view.Buf)
523                         to := from.Move(searchCount, view.Buf)
524                         adjust := Count(replace) - searchCount
525                         view.Buf.Replace(from, to, replace)
526                         found++
527                         if len(matches) > 1 {
528                                 for _, match := range matches[1:] {
529                                         found++
530                                         matchCount := runePos(match[0], bufStr)
531                                         searchCount = runePos(match[1], bufStr) - matchCount
532                                         from = from.Move(matchCount-prevMatchCount+adjust, view.Buf)
533                                         to = from.Move(searchCount, view.Buf)
534                                         view.Buf.Replace(from, to, replace)
535                                         prevMatchCount = matchCount
536                                         adjust = Count(replace) - searchCount
537                                 }
538                         }
539                 }
540         }
541         view.Cursor.Relocate()
542
543         if found > 1 {
544                 messenger.Message("Replaced ", found, " occurrences of ", search)
545         } else if found == 1 {
546                 messenger.Message("Replaced ", found, " occurrence of ", search)
547         } else {
548                 messenger.Message("Nothing matched ", search)
549         }
550 }
551
552 // RunShellCommand executes a shell command and returns the output/error
553 func RunShellCommand(input string) (string, error) {
554         inputCmd := SplitCommandArgs(input)[0]
555         args := SplitCommandArgs(input)[1:]
556
557         cmd := exec.Command(inputCmd, args...)
558         outputBytes := &bytes.Buffer{}
559         cmd.Stdout = outputBytes
560         cmd.Stderr = outputBytes
561         cmd.Start()
562         err := cmd.Wait() // wait for command to finish
563         outstring := outputBytes.String()
564         return outstring, err
565 }
566
567 // HandleShellCommand runs the shell command
568 // The openTerm argument specifies whether a terminal should be opened (for viewing output
569 // or interacting with stdin)
570 func HandleShellCommand(input string, openTerm bool, waitToFinish bool) string {
571         inputCmd := SplitCommandArgs(input)[0]
572         if !openTerm {
573                 // Simply run the command in the background and notify the user when it's done
574                 messenger.Message("Running...")
575                 go func() {
576                         output, err := RunShellCommand(input)
577                         totalLines := strings.Split(output, "\n")
578
579                         if len(totalLines) < 3 {
580                                 if err == nil {
581                                         messenger.Message(inputCmd, " exited without error")
582                                 } else {
583                                         messenger.Message(inputCmd, " exited with error: ", err, ": ", output)
584                                 }
585                         } else {
586                                 messenger.Message(output)
587                         }
588                         // We have to make sure to redraw
589                         RedrawAll()
590                 }()
591         } else {
592                 // Shut down the screen because we're going to interact directly with the shell
593                 screen.Fini()
594                 screen = nil
595
596                 args := SplitCommandArgs(input)[1:]
597
598                 // Set up everything for the command
599                 var outputBuf bytes.Buffer
600                 cmd := exec.Command(inputCmd, args...)
601                 cmd.Stdin = os.Stdin
602                 cmd.Stdout = io.MultiWriter(os.Stdout, &outputBuf)
603                 cmd.Stderr = os.Stderr
604
605                 // This is a trap for Ctrl-C so that it doesn't kill micro
606                 // Instead we trap Ctrl-C to kill the program we're running
607                 c := make(chan os.Signal, 1)
608                 signal.Notify(c, os.Interrupt)
609                 go func() {
610                         for range c {
611                                 cmd.Process.Kill()
612                         }
613                 }()
614
615                 cmd.Start()
616                 err := cmd.Wait()
617
618                 output := outputBuf.String()
619                 if err != nil {
620                         output = err.Error()
621                 }
622
623                 if waitToFinish {
624                         // This is just so we don't return right away and let the user press enter to return
625                         TermMessage("")
626                 }
627
628                 // Start the screen back up
629                 InitScreen()
630
631                 return output
632         }
633         return ""
634 }
635
636 // HandleCommand handles input from the user
637 func HandleCommand(input string) {
638         args := SplitCommandArgs(input)
639         inputCmd := args[0]
640
641         if _, ok := commands[inputCmd]; !ok {
642                 messenger.Error("Unknown command ", inputCmd)
643         } else {
644                 commands[inputCmd].action(args[1:])
645         }
646 }