From db502622866bc9186b18e4a1f668ba65ddb9d86d Mon Sep 17 00:00:00 2001 From: Zachary Yedidia Date: Tue, 1 Jan 2019 23:29:25 -0500 Subject: [PATCH] Start implementing commands --- cmd/micro/action/actions.go | 5 + cmd/micro/action/command.go | 275 +++++++++++++++++++++++++++++++++ cmd/micro/display/window.go | 30 +++- cmd/micro/info/autocomplete.go | 266 +++++++++++++++++++++++++++++++ cmd/micro/micro.go | 3 +- 5 files changed, 576 insertions(+), 3 deletions(-) create mode 100644 cmd/micro/action/command.go create mode 100644 cmd/micro/info/autocomplete.go diff --git a/cmd/micro/action/actions.go b/cmd/micro/action/actions.go index 2921d4bf..85f87f86 100644 --- a/cmd/micro/action/actions.go +++ b/cmd/micro/action/actions.go @@ -751,6 +751,11 @@ func (h *BufHandler) ShellMode() bool { // CommandMode lets the user enter a command func (h *BufHandler) CommandMode() bool { + InfoBar.Prompt("> ", func(resp string, canceled bool) { + if !canceled { + HandleCommand(resp) + } + }) return false } diff --git a/cmd/micro/action/command.go b/cmd/micro/action/command.go new file mode 100644 index 00000000..a36ec66a --- /dev/null +++ b/cmd/micro/action/command.go @@ -0,0 +1,275 @@ +package action + +import ( + "os" + + "github.com/zyedidia/micro/cmd/micro/shellwords" + "github.com/zyedidia/micro/cmd/micro/util" +) + +// A Command contains an action (a function to call) as well as information about how to autocomplete the command +type Command struct { + action func([]string) + completions []Completion +} + +// A StrCommand is similar to a command but keeps the name of the action +type StrCommand struct { + action string + completions []Completion +} + +var commands map[string]Command + +var commandActions map[string]func([]string) + +func init() { + commandActions = map[string]func([]string){ + "Set": Set, + "SetLocal": SetLocal, + "Show": Show, + "ShowKey": ShowKey, + "Run": Run, + "Bind": Bind, + "Quit": Quit, + "Save": Save, + "Replace": Replace, + "ReplaceAll": ReplaceAll, + "VSplit": VSplit, + "HSplit": HSplit, + "Tab": NewTab, + "Help": Help, + "Eval": Eval, + "ToggleLog": ToggleLog, + "Plugin": PluginCmd, + "Reload": Reload, + "Cd": Cd, + "Pwd": Pwd, + "Open": Open, + "TabSwitch": TabSwitch, + "Term": Term, + "MemUsage": MemUsage, + "Retab": Retab, + "Raw": Raw, + } +} + +// InitCommands initializes the default commands +func InitCommands() { + commands = make(map[string]Command) + + defaults := DefaultCommands() + parseCommands(defaults) +} + +func parseCommands(userCommands map[string]StrCommand) { + for k, v := range userCommands { + MakeCommand(k, v.action, v.completions...) + } +} + +// 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, completions ...Completion) { + 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] = Command{action, completions} +} + +// DefaultCommands returns a map containing micro's default commands +func DefaultCommands() map[string]StrCommand { + return map[string]StrCommand{ + "set": {"Set", []Completion{OptionCompletion, OptionValueCompletion}}, + "setlocal": {"SetLocal", []Completion{OptionCompletion, OptionValueCompletion}}, + "show": {"Show", []Completion{OptionCompletion, NoCompletion}}, + "showkey": {"ShowKey", []Completion{NoCompletion}}, + "bind": {"Bind", []Completion{NoCompletion}}, + "run": {"Run", []Completion{NoCompletion}}, + "quit": {"Quit", []Completion{NoCompletion}}, + "save": {"Save", []Completion{NoCompletion}}, + "replace": {"Replace", []Completion{NoCompletion}}, + "replaceall": {"ReplaceAll", []Completion{NoCompletion}}, + "vsplit": {"VSplit", []Completion{FileCompletion, NoCompletion}}, + "hsplit": {"HSplit", []Completion{FileCompletion, NoCompletion}}, + "tab": {"Tab", []Completion{FileCompletion, NoCompletion}}, + "help": {"Help", []Completion{HelpCompletion, NoCompletion}}, + "eval": {"Eval", []Completion{NoCompletion}}, + "log": {"ToggleLog", []Completion{NoCompletion}}, + "plugin": {"Plugin", []Completion{PluginCmdCompletion, PluginNameCompletion}}, + "reload": {"Reload", []Completion{NoCompletion}}, + "cd": {"Cd", []Completion{FileCompletion}}, + "pwd": {"Pwd", []Completion{NoCompletion}}, + "open": {"Open", []Completion{FileCompletion}}, + "tabswitch": {"TabSwitch", []Completion{NoCompletion}}, + "term": {"Term", []Completion{NoCompletion}}, + "memusage": {"MemUsage", []Completion{NoCompletion}}, + "retab": {"Retab", []Completion{NoCompletion}}, + "raw": {"Raw", []Completion{NoCompletion}}, + } +} + +// CommandEditAction returns a bindable function that opens a prompt with +// the given string and executes the command when the user presses +// enter +func CommandEditAction(prompt string) BufKeyAction { + return func(h *BufHandler) bool { + InfoBar.Prompt("> "+prompt, func(resp string, canceled bool) { + if !canceled { + HandleCommand(resp) + } + }) + return false + } +} + +// CommandAction returns a bindable function which executes the +// given command +func CommandAction(cmd string) BufKeyAction { + return func(h *BufHandler) bool { + HandleCommand(cmd) + return false + } +} + +// PluginCmd installs, removes, updates, lists, or searches for given plugins +func PluginCmd(args []string) { +} + +// Retab changes all spaces to tabs or all tabs to spaces +// depending on the user's settings +func Retab(args []string) { +} + +// Raw opens a new raw view which displays the escape sequences micro +// is receiving in real-time +func Raw(args []string) { +} + +// TabSwitch switches to a given tab either by name or by number +func TabSwitch(args []string) { +} + +// Cd changes the current working directory +func Cd(args []string) { +} + +// MemUsage prints micro's memory usage +// Alloc shows how many bytes are currently in use +// Sys shows how many bytes have been requested from the operating system +// NumGC shows how many times the GC has been run +// Note that Go commonly reserves more memory from the OS than is currently in-use/required +// Additionally, even if Go returns memory to the OS, the OS does not always claim it because +// there may be plenty of memory to spare +func MemUsage(args []string) { + InfoBar.Message(util.GetMemStats()) +} + +// Pwd prints the current working directory +func Pwd(args []string) { + wd, err := os.Getwd() + if err != nil { + InfoBar.Message(err.Error()) + } else { + InfoBar.Message(wd) + } +} + +// Open opens a new buffer with a given filename +func Open(args []string) { +} + +// ToggleLog toggles the log view +func ToggleLog(args []string) { +} + +// Reload reloads all files (syntax files, colorschemes...) +func Reload(args []string) { +} + +// Help tries to open the given help page in a horizontal split +func Help(args []string) { +} + +// VSplit opens a vertical split with file given in the first argument +// If no file is given, it opens an empty buffer in a new split +func VSplit(args []string) { +} + +// HSplit opens a horizontal split with file given in the first argument +// If no file is given, it opens an empty buffer in a new split +func HSplit(args []string) { +} + +// Eval evaluates a lua expression +func Eval(args []string) { +} + +// NewTab opens the given file in a new tab +func NewTab(args []string) { +} + +// Set sets an option +func Set(args []string) { +} + +// SetLocal sets an option local to the buffer +func SetLocal(args []string) { +} + +// Show shows the value of the given option +func Show(args []string) { +} + +// ShowKey displays the action that a key is bound to +func ShowKey(args []string) { +} + +// Bind creates a new keybinding +func Bind(args []string) { +} + +// Run runs a shell command in the background +func Run(args []string) { +} + +// Quit closes the main view +func Quit(args []string) { +} + +// Save saves the buffer in the main view +func Save(args []string) { +} + +// Replace runs search and replace +func Replace(args []string) { +} + +// ReplaceAll replaces search term all at once +func ReplaceAll(args []string) { +} + +// Term opens a terminal in the current view +func Term(args []string) { +} + +// HandleCommand handles input from the user +func HandleCommand(input string) { + args, err := shellwords.Split(input) + if err != nil { + InfoBar.Error("Error parsing args ", err) + return + } + + inputCmd := args[0] + + if _, ok := commands[inputCmd]; !ok { + InfoBar.Error("Unknown command ", inputCmd) + } else { + commands[inputCmd].action(args[1:]) + } +} diff --git a/cmd/micro/display/window.go b/cmd/micro/display/window.go index 0cbd2e7d..0e14c348 100644 --- a/cmd/micro/display/window.go +++ b/cmd/micro/display/window.go @@ -198,6 +198,10 @@ func (w *BufWindow) displayBuffer() { if style, ok := config.Colorscheme["line-number"]; ok { lineNumStyle = style } + curNumStyle := config.DefStyle + if style, ok := config.Colorscheme["current-line-number"]; ok { + curNumStyle = style + } // We need to know the string length of the largest line number // so we can pad appropriately when displaying line numbers @@ -219,7 +223,11 @@ func (w *BufWindow) displayBuffer() { for vloc.Y = 0; vloc.Y < bufHeight; vloc.Y++ { vloc.X = 0 if b.Settings["ruler"].(bool) { - w.drawLineNum(lineNumStyle, false, maxLineNumLength, &vloc, &bloc) + s := lineNumStyle + if bloc.Y == activeC.Y { + s = curNumStyle + } + w.drawLineNum(s, false, maxLineNumLength, &vloc, &bloc) } line := b.LineBytes(bloc.Y) @@ -236,7 +244,14 @@ func (w *BufWindow) displayBuffer() { if s, ok := config.Colorscheme["selection"]; ok { style = s } + } + if b.Settings["cursorline"].(bool) && + !activeC.HasSelection() && activeC.Y == bloc.Y { + if s, ok := config.Colorscheme["cursor-line"]; ok { + fg, _, _ := s.Decompose() + style = style.Background(fg) + } } screen.Screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, r, nil, style) @@ -300,6 +315,19 @@ func (w *BufWindow) displayBuffer() { if activeC.X == bloc.X && activeC.Y == bloc.Y { w.showCursor(vloc.X, vloc.Y, true) } + + if b.Settings["cursorline"].(bool) && + !activeC.HasSelection() && activeC.Y == bloc.Y { + style := config.DefStyle + if s, ok := config.Colorscheme["cursor-line"]; ok { + fg, _, _ := s.Decompose() + style = style.Background(fg) + } + for i := vloc.X; i < w.Width; i++ { + screen.Screen.SetContent(i, vloc.Y, ' ', nil, style) + } + } + bloc.X = w.StartCol bloc.Y++ if bloc.Y >= b.LinesNum() { diff --git a/cmd/micro/info/autocomplete.go b/cmd/micro/info/autocomplete.go new file mode 100644 index 00000000..59d5056e --- /dev/null +++ b/cmd/micro/info/autocomplete.go @@ -0,0 +1,266 @@ +package info + +import ( + "io/ioutil" + "os" + "strings" + + "github.com/zyedidia/micro/cmd/micro/config" + "github.com/zyedidia/micro/cmd/micro/util" +) + +// Completion represents a type of completion +type Completion int + +const ( + NoCompletion Completion = iota + FileCompletion + CommandCompletion + HelpCompletion + OptionCompletion + PluginCmdCompletion + PluginNameCompletion + OptionValueCompletion +) + +var pluginCompletions []func(string) []string + +// This file is meant (for now) for autocompletion in command mode, not +// while coding. This helps micro autocomplete commands and then filenames +// for example with `vsplit filename`. + +// FileComplete autocompletes filenames +func FileComplete(input string) (string, []string) { + var sep string = string(os.PathSeparator) + dirs := strings.Split(input, sep) + + var files []os.FileInfo + var err error + if len(dirs) > 1 { + directories := strings.Join(dirs[:len(dirs)-1], sep) + sep + + directories, _ = util.ReplaceHome(directories) + files, err = ioutil.ReadDir(directories) + } else { + files, err = ioutil.ReadDir(".") + } + + var suggestions []string + if err != nil { + return "", suggestions + } + for _, f := range files { + name := f.Name() + if f.IsDir() { + name += sep + } + if strings.HasPrefix(name, dirs[len(dirs)-1]) { + suggestions = append(suggestions, name) + } + } + + var chosen string + if len(suggestions) == 1 { + if len(dirs) > 1 { + chosen = strings.Join(dirs[:len(dirs)-1], sep) + sep + suggestions[0] + } else { + chosen = suggestions[0] + } + } else { + if len(dirs) > 1 { + chosen = strings.Join(dirs[:len(dirs)-1], sep) + sep + } + } + + return chosen, suggestions +} + +// CommandComplete autocompletes commands +// func CommandComplete(input string) (string, []string) { +// var suggestions []string +// for cmd := range commands { +// if strings.HasPrefix(cmd, input) { +// suggestions = append(suggestions, cmd) +// } +// } +// +// var chosen string +// if len(suggestions) == 1 { +// chosen = suggestions[0] +// } +// return chosen, suggestions +// } + +// HelpComplete autocompletes help topics +func HelpComplete(input string) (string, []string) { + var suggestions []string + + for _, file := range config.ListRuntimeFiles(config.RTHelp) { + topic := file.Name() + if strings.HasPrefix(topic, input) { + suggestions = append(suggestions, topic) + } + } + + var chosen string + if len(suggestions) == 1 { + chosen = suggestions[0] + } + return chosen, suggestions +} + +// ColorschemeComplete tab-completes names of colorschemes. +func ColorschemeComplete(input string) (string, []string) { + var suggestions []string + files := config.ListRuntimeFiles(config.RTColorscheme) + + for _, f := range files { + if strings.HasPrefix(f.Name(), input) { + suggestions = append(suggestions, f.Name()) + } + } + + var chosen string + if len(suggestions) == 1 { + chosen = suggestions[0] + } + + return chosen, suggestions +} + +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} + +// OptionComplete autocompletes options +func OptionComplete(input string) (string, []string) { + var suggestions []string + localSettings := config.DefaultLocalSettings() + for option := range config.GlobalSettings { + if strings.HasPrefix(option, input) { + suggestions = append(suggestions, option) + } + } + for option := range localSettings { + if strings.HasPrefix(option, input) && !contains(suggestions, option) { + suggestions = append(suggestions, option) + } + } + + var chosen string + if len(suggestions) == 1 { + chosen = suggestions[0] + } + return chosen, suggestions +} + +// OptionValueComplete completes values for various options +func OptionValueComplete(inputOpt, input string) (string, []string) { + inputOpt = strings.TrimSpace(inputOpt) + var suggestions []string + localSettings := config.DefaultLocalSettings() + var optionVal interface{} + for k, option := range config.GlobalSettings { + if k == inputOpt { + optionVal = option + } + } + for k, option := range localSettings { + if k == inputOpt { + optionVal = option + } + } + + switch optionVal.(type) { + case bool: + if strings.HasPrefix("on", input) { + suggestions = append(suggestions, "on") + } else if strings.HasPrefix("true", input) { + suggestions = append(suggestions, "true") + } + if strings.HasPrefix("off", input) { + suggestions = append(suggestions, "off") + } else if strings.HasPrefix("false", input) { + suggestions = append(suggestions, "false") + } + case string: + switch inputOpt { + case "colorscheme": + _, suggestions = ColorschemeComplete(input) + case "fileformat": + if strings.HasPrefix("unix", input) { + suggestions = append(suggestions, "unix") + } + if strings.HasPrefix("dos", input) { + suggestions = append(suggestions, "dos") + } + case "sucmd": + if strings.HasPrefix("sudo", input) { + suggestions = append(suggestions, "sudo") + } + if strings.HasPrefix("doas", input) { + suggestions = append(suggestions, "doas") + } + } + } + + var chosen string + if len(suggestions) == 1 { + chosen = suggestions[0] + } + return chosen, suggestions +} + +// // MakeCompletion registers a function from a plugin for autocomplete commands +// func MakeCompletion(function string) Completion { +// pluginCompletions = append(pluginCompletions, LuaFunctionComplete(function)) +// return Completion(-len(pluginCompletions)) +// } +// +// // PluginComplete autocompletes from plugin function +// func PluginComplete(complete Completion, input string) (chosen string, suggestions []string) { +// idx := int(-complete) - 1 +// +// if len(pluginCompletions) <= idx { +// return "", nil +// } +// suggestions = pluginCompletions[idx](input) +// +// if len(suggestions) == 1 { +// chosen = suggestions[0] +// } +// return +// } +// +// // PluginCmdComplete completes with possible choices for the `> plugin` command +// func PluginCmdComplete(input string) (chosen string, suggestions []string) { +// for _, cmd := range []string{"install", "remove", "search", "update", "list"} { +// if strings.HasPrefix(cmd, input) { +// suggestions = append(suggestions, cmd) +// } +// } +// +// if len(suggestions) == 1 { +// chosen = suggestions[0] +// } +// return chosen, suggestions +// } +// +// // PluginnameComplete completes with the names of loaded plugins +// func PluginNameComplete(input string) (chosen string, suggestions []string) { +// for _, pp := range GetAllPluginPackages() { +// if strings.HasPrefix(pp.Name, input) { +// suggestions = append(suggestions, pp.Name) +// } +// } +// +// if len(suggestions) == 1 { +// chosen = suggestions[0] +// } +// return chosen, suggestions +// } diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index 37c8218d..ecee6847 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -168,6 +168,7 @@ func main() { } config.InitGlobalSettings() action.InitBindings() + action.InitCommands() err = config.InitColorscheme() if err != nil { @@ -228,6 +229,4 @@ func main() { } } } - - screen.Screen.Fini() }