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