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