]> git.lizzy.rs Git - micro.git/blob - cmd/micro/command.go
Fix draw ordering
[micro.git] / cmd / micro / command.go
1 package main
2
3 import (
4         "bytes"
5         "io/ioutil"
6         "os"
7         "os/exec"
8         "os/signal"
9         "regexp"
10         "strings"
11
12         "github.com/mitchellh/go-homedir"
13 )
14
15 var commands map[string]func([]string)
16
17 var commandActions = map[string]func([]string){
18         "Set":     Set,
19         "Run":     Run,
20         "Bind":    Bind,
21         "Quit":    Quit,
22         "Save":    Save,
23         "Replace": Replace,
24         "VSplit":  VSplit,
25         "HSplit":  HSplit,
26 }
27
28 // InitCommands initializes the default commands
29 func InitCommands() {
30         commands = make(map[string]func([]string))
31
32         defaults := DefaultCommands()
33         parseCommands(defaults)
34 }
35
36 func parseCommands(userCommands map[string]string) {
37         for k, v := range userCommands {
38                 MakeCommand(k, v)
39         }
40 }
41
42 // MakeCommand is a function to easily create new commands
43 // This can be called by plugins in Lua so that plugins can define their own commands
44 func MakeCommand(name, function string) {
45         action := commandActions[function]
46         if _, ok := commandActions[function]; !ok {
47                 // If the user seems to be binding a function that doesn't exist
48                 // We hope that it's a lua function that exists and bind it to that
49                 action = LuaFunctionCommand(function)
50         }
51
52         commands[name] = action
53 }
54
55 // DefaultCommands returns a map containing micro's default commands
56 func DefaultCommands() map[string]string {
57         return map[string]string{
58                 "set":     "Set",
59                 "bind":    "Bind",
60                 "run":     "Run",
61                 "quit":    "Quit",
62                 "save":    "Save",
63                 "replace": "Replace",
64                 "vsplit":  "VSplit",
65                 "hsplit":  "HSplit",
66         }
67 }
68
69 // VSplit opens a vertical split with file given in the first argument
70 // If no file is given, it opens an empty buffer in a new split
71 func VSplit(args []string) {
72         if len(args) == 0 {
73                 CurView().VSplit(NewBuffer([]byte{}, ""))
74         } else {
75                 filename := args[0]
76                 home, _ := homedir.Dir()
77                 filename = strings.Replace(filename, "~", home, 1)
78                 file, err := ioutil.ReadFile(filename)
79
80                 var buf *Buffer
81                 if err != nil {
82                         // File does not exist -- create an empty buffer with that name
83                         buf = NewBuffer([]byte{}, filename)
84                 } else {
85                         buf = NewBuffer(file, filename)
86                 }
87                 CurView().VSplit(buf)
88         }
89 }
90
91 // HSplit opens a horizontal split with file given in the first argument
92 // If no file is given, it opens an empty buffer in a new split
93 func HSplit(args []string) {
94         if len(args) == 0 {
95                 CurView().HSplit(NewBuffer([]byte{}, ""))
96         } else {
97                 filename := args[0]
98                 home, _ := homedir.Dir()
99                 filename = strings.Replace(filename, "~", home, 1)
100                 file, err := ioutil.ReadFile(filename)
101
102                 var buf *Buffer
103                 if err != nil {
104                         // File does not exist -- create an empty buffer with that name
105                         buf = NewBuffer([]byte{}, filename)
106                 } else {
107                         buf = NewBuffer(file, filename)
108                 }
109                 CurView().HSplit(buf)
110         }
111 }
112
113 // Set sets an option
114 func Set(args []string) {
115         // Set an option and we have to set it for every view
116         for _, tab := range tabs {
117                 for _, view := range tab.views {
118                         SetOption(view, args)
119                 }
120         }
121 }
122
123 // Bind creates a new keybinding
124 func Bind(args []string) {
125         if len(args) != 2 {
126                 messenger.Error("Incorrect number of arguments")
127                 return
128         }
129         BindKey(args[0], args[1])
130 }
131
132 // Run runs a shell command in the background
133 func Run(args []string) {
134         // Run a shell command in the background (openTerm is false)
135         HandleShellCommand(strings.Join(args, " "), false)
136 }
137
138 // Quit closes the main view
139 func Quit(args []string) {
140         // Close the main view
141         CurView().Quit()
142 }
143
144 // Save saves the buffer in the main view
145 func Save(args []string) {
146         // Save the main view
147         CurView().Save()
148 }
149
150 // Replace runs search and replace
151 func Replace(args []string) {
152         // This is a regex to parse the replace expression
153         // We allow no quotes if there are no spaces, but if you want to search
154         // for or replace an expression with spaces, you can add double quotes
155         r := regexp.MustCompile(`"[^"\\]*(?:\\.[^"\\]*)*"|[^\s]*`)
156         replaceCmd := r.FindAllString(strings.Join(args, " "), -1)
157         if len(replaceCmd) < 2 {
158                 // We need to find both a search and replace expression
159                 messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
160                 return
161         }
162
163         var flags string
164         if len(replaceCmd) == 3 {
165                 // The user included some flags
166                 flags = replaceCmd[2]
167         }
168
169         search := string(replaceCmd[0])
170         replace := string(replaceCmd[1])
171
172         // If the search and replace expressions have quotes, we need to remove those
173         if strings.HasPrefix(search, `"`) && strings.HasSuffix(search, `"`) {
174                 search = search[1 : len(search)-1]
175         }
176         if strings.HasPrefix(replace, `"`) && strings.HasSuffix(replace, `"`) {
177                 replace = replace[1 : len(replace)-1]
178         }
179
180         // We replace all escaped double quotes to real double quotes
181         search = strings.Replace(search, `\"`, `"`, -1)
182         replace = strings.Replace(replace, `\"`, `"`, -1)
183         // Replace some things so users can actually insert newlines and tabs in replacements
184         replace = strings.Replace(replace, "\\n", "\n", -1)
185         replace = strings.Replace(replace, "\\t", "\t", -1)
186
187         regex, err := regexp.Compile(search)
188         if err != nil {
189                 // There was an error with the user's regex
190                 messenger.Error(err.Error())
191                 return
192         }
193
194         view := CurView()
195
196         found := 0
197         for {
198                 match := regex.FindStringIndex(view.Buf.String())
199                 if match == nil {
200                         break
201                 }
202                 found++
203                 if strings.Contains(flags, "c") {
204                         // The 'check' flag was used
205                         Search(search, view, true)
206                         view.Relocate()
207                         if settings["syntax"].(bool) {
208                                 view.matches = Match(view)
209                         }
210                         RedrawAll()
211                         choice, canceled := messenger.YesNoPrompt("Perform replacement? (y,n)")
212                         if canceled {
213                                 if view.Cursor.HasSelection() {
214                                         view.Cursor.Loc = view.Cursor.CurSelection[0]
215                                         view.Cursor.ResetSelection()
216                                 }
217                                 messenger.Reset()
218                                 return
219                         }
220                         if choice {
221                                 view.Cursor.DeleteSelection()
222                                 view.Buf.Insert(FromCharPos(match[0], view.Buf), replace)
223                                 view.Cursor.ResetSelection()
224                                 messenger.Reset()
225                         } else {
226                                 if view.Cursor.HasSelection() {
227                                         searchStart = ToCharPos(view.Cursor.CurSelection[1], view.Buf)
228                                 } else {
229                                         searchStart = ToCharPos(view.Cursor.Loc, view.Buf)
230                                 }
231                                 continue
232                         }
233                 } else {
234                         view.Buf.Replace(FromCharPos(match[0], view.Buf), FromCharPos(match[1], view.Buf), replace)
235                 }
236         }
237         view.Cursor.Relocate()
238
239         if found > 1 {
240                 messenger.Message("Replaced ", found, " occurences of ", search)
241         } else if found == 1 {
242                 messenger.Message("Replaced ", found, " occurence of ", search)
243         } else {
244                 messenger.Message("Nothing matched ", search)
245         }
246 }
247
248 // RunShellCommand executes a shell command and returns the output/error
249 func RunShellCommand(input string) (string, error) {
250         inputCmd := strings.Split(input, " ")[0]
251         args := strings.Split(input, " ")[1:]
252
253         cmd := exec.Command(inputCmd, args...)
254         outputBytes := &bytes.Buffer{}
255         cmd.Stdout = outputBytes
256         cmd.Stderr = outputBytes
257         cmd.Start()
258         err := cmd.Wait() // wait for command to finish
259         outstring := outputBytes.String()
260         return outstring, err
261 }
262
263 // HandleShellCommand runs the shell command
264 // The openTerm argument specifies whether a terminal should be opened (for viewing output
265 // or interacting with stdin)
266 func HandleShellCommand(input string, openTerm bool) {
267         inputCmd := strings.Split(input, " ")[0]
268         if !openTerm {
269                 // Simply run the command in the background and notify the user when it's done
270                 messenger.Message("Running...")
271                 go func() {
272                         output, err := RunShellCommand(input)
273                         totalLines := strings.Split(output, "\n")
274
275                         if len(totalLines) < 3 {
276                                 if err == nil {
277                                         messenger.Message(inputCmd, " exited without error")
278                                 } else {
279                                         messenger.Message(inputCmd, " exited with error: ", err, ": ", output)
280                                 }
281                         } else {
282                                 messenger.Message(output)
283                         }
284                         // We have to make sure to redraw
285                         RedrawAll()
286                 }()
287         } else {
288                 // Shut down the screen because we're going to interact directly with the shell
289                 screen.Fini()
290                 screen = nil
291
292                 args := strings.Split(input, " ")[1:]
293
294                 // Set up everything for the command
295                 cmd := exec.Command(inputCmd, args...)
296                 cmd.Stdin = os.Stdin
297                 cmd.Stdout = os.Stdout
298                 cmd.Stderr = os.Stderr
299
300                 // This is a trap for Ctrl-C so that it doesn't kill micro
301                 // Instead we trap Ctrl-C to kill the program we're running
302                 c := make(chan os.Signal, 1)
303                 signal.Notify(c, os.Interrupt)
304                 go func() {
305                         for range c {
306                                 cmd.Process.Kill()
307                         }
308                 }()
309
310                 // Start the command
311                 cmd.Start()
312                 cmd.Wait()
313
314                 // This is just so we don't return right away and let the user press enter to return
315                 TermMessage("")
316
317                 // Start the screen back up
318                 InitScreen()
319         }
320 }
321
322 // HandleCommand handles input from the user
323 func HandleCommand(input string) {
324         inputCmd := strings.Split(input, " ")[0]
325         args := strings.Split(input, " ")[1:]
326
327         if _, ok := commands[inputCmd]; !ok {
328                 messenger.Error("Unkown command ", inputCmd)
329         } else {
330                 commands[inputCmd](args)
331         }
332 }