]> git.lizzy.rs Git - micro.git/blob - cmd/micro/command.go
60b6455825e345a536407a83e3db55d2f80d22ff
[micro.git] / cmd / micro / command.go
1 package main
2
3 import (
4         "bytes"
5         "io"
6         "io/ioutil"
7         "os"
8         "os/exec"
9         "os/signal"
10         "regexp"
11         "strings"
12
13         "github.com/mitchellh/go-homedir"
14 )
15
16 type Command struct {
17         action      func([]string)
18         completions []Completion
19 }
20
21 type StrCommand struct {
22         action      string
23         completions []Completion
24 }
25
26 var commands map[string]Command
27
28 var commandActions = map[string]func([]string){
29         "Set":      Set,
30         "SetLocal": SetLocal,
31         "Show":     Show,
32         "Run":      Run,
33         "Bind":     Bind,
34         "Quit":     Quit,
35         "Save":     Save,
36         "Replace":  Replace,
37         "VSplit":   VSplit,
38         "HSplit":   HSplit,
39         "Tab":      NewTab,
40         "Help":     Help,
41         "Eval":     Eval,
42         "Plugin":   PluginCmd,
43 }
44
45 // InitCommands initializes the default commands
46 func InitCommands() {
47         commands = make(map[string]Command)
48
49         defaults := DefaultCommands()
50         parseCommands(defaults)
51 }
52
53 func parseCommands(userCommands map[string]StrCommand) {
54         for k, v := range userCommands {
55                 MakeCommand(k, v.action, v.completions...)
56         }
57 }
58
59 // MakeCommand is a function to easily create new commands
60 // This can be called by plugins in Lua so that plugins can define their own commands
61 func MakeCommand(name, function string, completions ...Completion) {
62         action := commandActions[function]
63         if _, ok := commandActions[function]; !ok {
64                 // If the user seems to be binding a function that doesn't exist
65                 // We hope that it's a lua function that exists and bind it to that
66                 action = LuaFunctionCommand(function)
67         }
68
69         commands[name] = Command{action, completions}
70 }
71
72 // DefaultCommands returns a map containing micro's default commands
73 func DefaultCommands() map[string]StrCommand {
74         return map[string]StrCommand{
75                 "set":      {"Set", []Completion{OptionCompletion, NoCompletion}},
76                 "setlocal": {"SetLocal", []Completion{OptionCompletion, NoCompletion}},
77                 "show":     {"Show", []Completion{OptionCompletion, NoCompletion}},
78                 "bind":     {"Bind", []Completion{NoCompletion}},
79                 "run":      {"Run", []Completion{NoCompletion}},
80                 "quit":     {"Quit", []Completion{NoCompletion}},
81                 "save":     {"Save", []Completion{NoCompletion}},
82                 "replace":  {"Replace", []Completion{NoCompletion}},
83                 "vsplit":   {"VSplit", []Completion{FileCompletion, NoCompletion}},
84                 "hsplit":   {"HSplit", []Completion{FileCompletion, NoCompletion}},
85                 "tab":      {"Tab", []Completion{FileCompletion, NoCompletion}},
86                 "help":     {"Help", []Completion{HelpCompletion, NoCompletion}},
87                 "eval":     {"Eval", []Completion{NoCompletion}},
88                 "plugin":   {"Plugin", []Completion{NoCompletion}},
89         }
90 }
91
92 // InstallPlugin installs the given plugin by exact name match
93 func PluginCmd(args []string) {
94         if len(args) >= 1 {
95                 switch args[0] {
96                 case "install":
97                         for _, plugin := range args[1:] {
98                                 pp := GetAllPluginPackages().Get(plugin)
99                                 if pp == nil {
100                                         messenger.Error("Unknown plugin \"" + plugin + "\"")
101                                 } else if !pp.IsInstallable() {
102                                         messenger.Error("Plugin \"" + plugin + "\" can not be installed.")
103                                 } else {
104                                         pp.Install()
105                                 }
106                         }
107                 case "remove":
108                         for _, plugin := range args[1:] {
109                                 // check if the plugin exists.
110                                 for _, lp := range loadedPlugins {
111                                         if lp == plugin {
112                                                 UninstallPlugin(plugin)
113                                                 continue
114                                         }
115                                 }
116                         }
117                 case "update":
118                         UpdatePlugins()
119                 }
120         } else {
121                 messenger.Error("Not enough arguments")
122         }
123 }
124
125 // Help tries to open the given help page in a horizontal split
126 func Help(args []string) {
127         if len(args) < 1 {
128                 // Open the default help if the user just typed "> help"
129                 CurView().openHelp("help")
130         } else {
131                 helpPage := args[0]
132                 if FindRuntimeFile(RTHelp, helpPage) != nil {
133                         CurView().openHelp(helpPage)
134                 } else {
135                         messenger.Error("Sorry, no help for ", helpPage)
136                 }
137         }
138 }
139
140 // VSplit opens a vertical split with file given in the first argument
141 // If no file is given, it opens an empty buffer in a new split
142 func VSplit(args []string) {
143         if len(args) == 0 {
144                 CurView().VSplit(NewBuffer([]byte{}, ""))
145         } else {
146                 filename := args[0]
147                 home, _ := homedir.Dir()
148                 filename = strings.Replace(filename, "~", home, 1)
149                 file, err := ioutil.ReadFile(filename)
150
151                 var buf *Buffer
152                 if err != nil {
153                         // File does not exist -- create an empty buffer with that name
154                         buf = NewBuffer([]byte{}, filename)
155                 } else {
156                         buf = NewBuffer(file, filename)
157                 }
158                 CurView().VSplit(buf)
159         }
160 }
161
162 // HSplit opens a horizontal split with file given in the first argument
163 // If no file is given, it opens an empty buffer in a new split
164 func HSplit(args []string) {
165         if len(args) == 0 {
166                 CurView().HSplit(NewBuffer([]byte{}, ""))
167         } else {
168                 filename := args[0]
169                 home, _ := homedir.Dir()
170                 filename = strings.Replace(filename, "~", home, 1)
171                 file, err := ioutil.ReadFile(filename)
172
173                 var buf *Buffer
174                 if err != nil {
175                         // File does not exist -- create an empty buffer with that name
176                         buf = NewBuffer([]byte{}, filename)
177                 } else {
178                         buf = NewBuffer(file, filename)
179                 }
180                 CurView().HSplit(buf)
181         }
182 }
183
184 // Eval evaluates a lua expression
185 func Eval(args []string) {
186         if len(args) >= 1 {
187                 err := L.DoString(args[0])
188                 if err != nil {
189                         messenger.Error(err)
190                 }
191         } else {
192                 messenger.Error("Not enough arguments")
193         }
194 }
195
196 // NewTab opens the given file in a new tab
197 func NewTab(args []string) {
198         if len(args) == 0 {
199                 CurView().AddTab(true)
200         } else {
201                 filename := args[0]
202                 home, _ := homedir.Dir()
203                 filename = strings.Replace(filename, "~", home, 1)
204                 file, _ := ioutil.ReadFile(filename)
205
206                 tab := NewTabFromView(NewView(NewBuffer(file, filename)))
207                 tab.SetNum(len(tabs))
208                 tabs = append(tabs, tab)
209                 curTab++
210                 if len(tabs) == 2 {
211                         for _, t := range tabs {
212                                 for _, v := range t.views {
213                                         v.ToggleTabbar()
214                                 }
215                         }
216                 }
217         }
218 }
219
220 // Set sets an option
221 func Set(args []string) {
222         if len(args) < 2 {
223                 messenger.Error("Not enough arguments")
224                 return
225         }
226
227         option := strings.TrimSpace(args[0])
228         value := strings.TrimSpace(args[1])
229
230         SetOptionAndSettings(option, value)
231 }
232
233 // SetLocal sets an option local to the buffer
234 func SetLocal(args []string) {
235         if len(args) < 2 {
236                 messenger.Error("Not enough arguments")
237                 return
238         }
239
240         option := strings.TrimSpace(args[0])
241         value := strings.TrimSpace(args[1])
242
243         err := SetLocalOption(option, value, CurView())
244         if err != nil {
245                 messenger.Error(err.Error())
246         }
247 }
248
249 // Show shows the value of the given option
250 func Show(args []string) {
251         if len(args) < 1 {
252                 messenger.Error("Please provide an option to show")
253                 return
254         }
255
256         option := GetOption(args[0])
257
258         if option == nil {
259                 messenger.Error(args[0], " is not a valid option")
260                 return
261         }
262
263         messenger.Message(option)
264 }
265
266 // Bind creates a new keybinding
267 func Bind(args []string) {
268         if len(args) < 2 {
269                 messenger.Error("Not enough arguments")
270                 return
271         }
272         BindKey(args[0], args[1])
273 }
274
275 // Run runs a shell command in the background
276 func Run(args []string) {
277         // Run a shell command in the background (openTerm is false)
278         HandleShellCommand(JoinCommandArgs(args...), false, true)
279 }
280
281 // Quit closes the main view
282 func Quit(args []string) {
283         // Close the main view
284         CurView().Quit(true)
285 }
286
287 // Save saves the buffer in the main view
288 func Save(args []string) {
289         if len(args) == 0 {
290                 // Save the main view
291                 CurView().Save(true)
292         } else {
293                 CurView().Buf.SaveAs(args[0])
294         }
295 }
296
297 // Replace runs search and replace
298 func Replace(args []string) {
299         if len(args) < 2 {
300                 // We need to find both a search and replace expression
301                 messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
302                 return
303         }
304
305         var flags string
306         if len(args) == 3 {
307                 // The user included some flags
308                 flags = args[2]
309         }
310
311         search := string(args[0])
312         replace := string(args[1])
313
314         regex, err := regexp.Compile(search)
315         if err != nil {
316                 // There was an error with the user's regex
317                 messenger.Error(err.Error())
318                 return
319         }
320
321         view := CurView()
322
323         found := 0
324         if strings.Contains(flags, "c") {
325                 for {
326                         // The 'check' flag was used
327                         Search(search, view, true)
328                         if !view.Cursor.HasSelection() {
329                                 break
330                         }
331                         view.Relocate()
332                         if view.Buf.Settings["syntax"].(bool) {
333                                 view.matches = Match(view)
334                         }
335                         RedrawAll()
336                         choice, canceled := messenger.YesNoPrompt("Perform replacement? (y,n)")
337                         if canceled {
338                                 if view.Cursor.HasSelection() {
339                                         view.Cursor.Loc = view.Cursor.CurSelection[0]
340                                         view.Cursor.ResetSelection()
341                                 }
342                                 messenger.Reset()
343                                 break
344                         }
345                         if choice {
346                                 view.Cursor.DeleteSelection()
347                                 view.Buf.Insert(view.Cursor.Loc, replace)
348                                 view.Cursor.ResetSelection()
349                                 messenger.Reset()
350                                 found++
351                         } else {
352                                 if view.Cursor.HasSelection() {
353                                         searchStart = ToCharPos(view.Cursor.CurSelection[1], view.Buf)
354                                 } else {
355                                         searchStart = ToCharPos(view.Cursor.Loc, view.Buf)
356                                 }
357                                 continue
358                         }
359                 }
360         } else {
361                 matches := regex.FindAllStringIndex(view.Buf.String(), -1)
362                 if matches != nil && len(matches) > 0 {
363                         adjust := 0
364                         prevMatch := matches[0]
365                         from := FromCharPos(prevMatch[0], view.Buf)
366                         to := from.Move(Count(search), view.Buf)
367                         adjust += Count(replace) - Count(search)
368                         view.Buf.Replace(from, to, replace)
369                         if len(matches) > 1 {
370                                 for _, match := range matches[1:] {
371                                         found++
372                                         from = from.Move(match[0]-prevMatch[0]+adjust, view.Buf)
373                                         to := from.Move(Count(search), view.Buf)
374                                         // TermMessage(match[0], " ", prevMatch[0], " ", adjust, "\n", from, " ", to)
375                                         view.Buf.Replace(from, to, replace)
376                                         prevMatch = match
377                                         // adjust += Count(replace) - Count(search)
378                                 }
379                         }
380                 }
381         }
382         view.Cursor.Relocate()
383
384         if found > 1 {
385                 messenger.Message("Replaced ", found, " occurrences of ", search)
386         } else if found == 1 {
387                 messenger.Message("Replaced ", found, " occurrence of ", search)
388         } else {
389                 messenger.Message("Nothing matched ", search)
390         }
391 }
392
393 // RunShellCommand executes a shell command and returns the output/error
394 func RunShellCommand(input string) (string, error) {
395         inputCmd := SplitCommandArgs(input)[0]
396         args := SplitCommandArgs(input)[1:]
397
398         cmd := exec.Command(inputCmd, args...)
399         outputBytes := &bytes.Buffer{}
400         cmd.Stdout = outputBytes
401         cmd.Stderr = outputBytes
402         cmd.Start()
403         err := cmd.Wait() // wait for command to finish
404         outstring := outputBytes.String()
405         return outstring, err
406 }
407
408 // HandleShellCommand runs the shell command
409 // The openTerm argument specifies whether a terminal should be opened (for viewing output
410 // or interacting with stdin)
411 func HandleShellCommand(input string, openTerm bool, waitToFinish bool) string {
412         inputCmd := SplitCommandArgs(input)[0]
413         if !openTerm {
414                 // Simply run the command in the background and notify the user when it's done
415                 messenger.Message("Running...")
416                 go func() {
417                         output, err := RunShellCommand(input)
418                         totalLines := strings.Split(output, "\n")
419
420                         if len(totalLines) < 3 {
421                                 if err == nil {
422                                         messenger.Message(inputCmd, " exited without error")
423                                 } else {
424                                         messenger.Message(inputCmd, " exited with error: ", err, ": ", output)
425                                 }
426                         } else {
427                                 messenger.Message(output)
428                         }
429                         // We have to make sure to redraw
430                         RedrawAll()
431                 }()
432         } else {
433                 // Shut down the screen because we're going to interact directly with the shell
434                 screen.Fini()
435                 screen = nil
436
437                 args := SplitCommandArgs(input)[1:]
438
439                 // Set up everything for the command
440                 var outputBuf bytes.Buffer
441                 cmd := exec.Command(inputCmd, args...)
442                 cmd.Stdin = os.Stdin
443                 cmd.Stdout = io.MultiWriter(os.Stdout, &outputBuf)
444                 cmd.Stderr = os.Stderr
445
446                 // This is a trap for Ctrl-C so that it doesn't kill micro
447                 // Instead we trap Ctrl-C to kill the program we're running
448                 c := make(chan os.Signal, 1)
449                 signal.Notify(c, os.Interrupt)
450                 go func() {
451                         for range c {
452                                 cmd.Process.Kill()
453                         }
454                 }()
455
456                 cmd.Start()
457                 err := cmd.Wait()
458
459                 output := outputBuf.String()
460                 if err != nil {
461                         output = err.Error()
462                 }
463
464                 if waitToFinish {
465                         // This is just so we don't return right away and let the user press enter to return
466                         TermMessage("")
467                 }
468
469                 // Start the screen back up
470                 InitScreen()
471
472                 return output
473         }
474         return ""
475 }
476
477 // HandleCommand handles input from the user
478 func HandleCommand(input string) {
479         args := SplitCommandArgs(input)
480         inputCmd := args[0]
481
482         if _, ok := commands[inputCmd]; !ok {
483                 messenger.Error("Unknown command ", inputCmd)
484         } else {
485                 commands[inputCmd].action(args[1:])
486         }
487 }