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