Add support for user-created commands

Plugins can now create their own commands using the `MakeCommand`
function. Plugins can also now create their own keybindings with the
`BindKey` function. See the go plugin for an example of `MakeCommand`.
This commit is contained in:
Zachary Yedidia
2016-05-30 13:38:50 -04:00
parent 68189fd406
commit 7adcb13c08
8 changed files with 439 additions and 350 deletions

View File

@@ -9,6 +9,183 @@ import (
"strings"
)
var commands map[string]func([]string)
var commandActions = map[string]func([]string){
"Set": Set,
"Run": Run,
"Bind": Bind,
"Quit": Quit,
"Save": Save,
"Replace": Replace,
}
// InitCommands initializes the default commands
func InitCommands() {
commands = make(map[string]func([]string))
defaults := DefaultCommands()
parseCommands(defaults)
}
func parseCommands(userCommands map[string]string) {
for k, v := range userCommands {
MakeCommand(k, v)
}
}
// MakeCommand is a function to easily create new commands
// This can be called by plugins in Lua so that plugins can define their own commands
func MakeCommand(name, function string) {
action := commandActions[function]
if _, ok := commandActions[function]; !ok {
// If the user seems to be binding a function that doesn't exist
// We hope that it's a lua function that exists and bind it to that
action = LuaFunctionCommand(function)
}
commands[name] = action
}
// DefaultCommands returns a map containing micro's default commands
func DefaultCommands() map[string]string {
return map[string]string{
"set": "Set",
"bind": "Bind",
"run": "Run",
"quit": "Quit",
"save": "Save",
"replace": "Replace",
}
}
// Set sets an option
func Set(args []string) {
// Set an option and we have to set it for every view
for _, view := range views {
SetOption(view, args)
}
}
// Bind creates a new keybinding
func Bind(args []string) {
if len(args) != 2 {
messenger.Error("Incorrect number of arguments")
return
}
BindKey(args[0], args[1])
}
// Run runs a shell command in the background
func Run(args []string) {
// Run a shell command in the background (openTerm is false)
HandleShellCommand(strings.Join(args, " "), false)
}
// Quit closes the main view
func Quit(args []string) {
// Close the main view
views[mainView].Quit()
}
// Save saves the buffer in the main view
func Save(args []string) {
// Save the main view
views[mainView].Save()
}
// Replace runs search and replace
func Replace(args []string) {
// This is a regex to parse the replace expression
// We allow no quotes if there are no spaces, but if you want to search
// for or replace an expression with spaces, you can add double quotes
r := regexp.MustCompile(`"[^"\\]*(?:\\.[^"\\]*)*"|[^\s]*`)
replaceCmd := r.FindAllString(strings.Join(args, " "), -1)
if len(replaceCmd) < 2 {
// We need to find both a search and replace expression
messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
return
}
var flags string
if len(replaceCmd) == 3 {
// The user included some flags
flags = replaceCmd[2]
}
search := string(replaceCmd[0])
replace := string(replaceCmd[1])
// If the search and replace expressions have quotes, we need to remove those
if strings.HasPrefix(search, `"`) && strings.HasSuffix(search, `"`) {
search = search[1 : len(search)-1]
}
if strings.HasPrefix(replace, `"`) && strings.HasSuffix(replace, `"`) {
replace = replace[1 : len(replace)-1]
}
// We replace all escaped double quotes to real double quotes
search = strings.Replace(search, `\"`, `"`, -1)
replace = strings.Replace(replace, `\"`, `"`, -1)
// Replace some things so users can actually insert newlines and tabs in replacements
replace = strings.Replace(replace, "\\n", "\n", -1)
replace = strings.Replace(replace, "\\t", "\t", -1)
regex, err := regexp.Compile(search)
if err != nil {
// There was an error with the user's regex
messenger.Error(err.Error())
return
}
view := views[mainView]
found := false
for {
match := regex.FindStringIndex(view.Buf.String())
if match == nil {
break
}
found = true
if strings.Contains(flags, "c") {
// The 'check' flag was used
Search(search, view, true)
view.Relocate()
if settings["syntax"].(bool) {
view.matches = Match(view)
}
RedrawAll()
choice, canceled := messenger.YesNoPrompt("Perform replacement? (y,n)")
if canceled {
if view.Cursor.HasSelection() {
view.Cursor.SetLoc(view.Cursor.CurSelection[0])
view.Cursor.ResetSelection()
}
messenger.Reset()
return
}
if choice {
view.Cursor.DeleteSelection()
view.Buf.Insert(match[0], replace)
view.Cursor.ResetSelection()
messenger.Reset()
} else {
if view.Cursor.HasSelection() {
searchStart = view.Cursor.CurSelection[1]
} else {
searchStart = ToCharPos(view.Cursor.X, view.Cursor.Y, view.Buf)
}
continue
}
} else {
view.Buf.Replace(match[0], match[1], replace)
}
}
if !found {
messenger.Message("Nothing matched " + search)
}
}
// RunShellCommand executes a shell command and returns the output/error
func RunShellCommand(input string) (string, error) {
inputCmd := strings.Split(input, " ")[0]
@@ -88,117 +265,9 @@ func HandleCommand(input string) {
inputCmd := strings.Split(input, " ")[0]
args := strings.Split(input, " ")[1:]
switch inputCmd {
case "set":
// Set an option and we have to set it for every view
for _, view := range views {
SetOption(view, args)
}
case "run":
// Run a shell command in the background (openTerm is false)
HandleShellCommand(strings.Join(args, " "), false)
case "quit":
// This is a bit weird because micro only has one view for now so there is no way to close
// a single view
// Currently if multiple views were open, it would close all of them, and not check the non-mainviews
// for unsaved changes. This, and the behavior of Ctrl-Q need to be changed when splits are implemented
if views[mainView].CanClose("Quit anyway? (yes, no, save) ") {
screen.Fini()
os.Exit(0)
}
case "save":
// Save the main view
views[mainView].Save()
case "replace":
// This is a regex to parse the replace expression
// We allow no quotes if there are no spaces, but if you want to search
// for or replace an expression with spaces, you can add double quotes
r := regexp.MustCompile(`"[^"\\]*(?:\\.[^"\\]*)*"|[^\s]*`)
replaceCmd := r.FindAllString(strings.Join(args, " "), -1)
if len(replaceCmd) < 2 {
// We need to find both a search and replace expression
messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
return
}
var flags string
if len(replaceCmd) == 3 {
// The user included some flags
flags = replaceCmd[2]
}
search := string(replaceCmd[0])
replace := string(replaceCmd[1])
// If the search and replace expressions have quotes, we need to remove those
if strings.HasPrefix(search, `"`) && strings.HasSuffix(search, `"`) {
search = search[1 : len(search)-1]
}
if strings.HasPrefix(replace, `"`) && strings.HasSuffix(replace, `"`) {
replace = replace[1 : len(replace)-1]
}
// We replace all escaped double quotes to real double quotes
search = strings.Replace(search, `\"`, `"`, -1)
replace = strings.Replace(replace, `\"`, `"`, -1)
// Replace some things so users can actually insert newlines and tabs in replacements
replace = strings.Replace(replace, "\\n", "\n", -1)
replace = strings.Replace(replace, "\\t", "\t", -1)
regex, err := regexp.Compile(search)
if err != nil {
// There was an error with the user's regex
messenger.Error(err.Error())
return
}
view := views[mainView]
found := false
for {
match := regex.FindStringIndex(view.Buf.String())
if match == nil {
break
}
found = true
if strings.Contains(flags, "c") {
// The 'check' flag was used
Search(search, view, true)
view.Relocate()
if settings["syntax"].(bool) {
view.matches = Match(view)
}
RedrawAll()
choice, canceled := messenger.YesNoPrompt("Perform replacement? (y,n)")
if canceled {
if view.Cursor.HasSelection() {
view.Cursor.SetLoc(view.Cursor.CurSelection[0])
view.Cursor.ResetSelection()
}
messenger.Reset()
return
}
if choice {
view.Cursor.DeleteSelection()
view.Buf.Insert(match[0], replace)
view.Cursor.ResetSelection()
messenger.Reset()
} else {
if view.Cursor.HasSelection() {
searchStart = view.Cursor.CurSelection[1]
} else {
searchStart = ToCharPos(view.Cursor.X, view.Cursor.Y, view.Buf)
}
continue
}
} else {
view.Buf.Replace(match[0], match[1], replace)
}
}
if !found {
messenger.Message("Nothing matched " + search)
}
default:
messenger.Error("Unknown command: " + inputCmd)
if _, ok := commands[inputCmd]; !ok {
messenger.Error("Unkown command ", inputCmd)
} else {
commands[inputCmd](args)
}
}