mirror of
https://github.com/zyedidia/micro.git
synced 2025-06-18 23:05:40 -04:00

It doesn't need to loop over the DefaultCommonSettings() again, since they're already included in the default settings and set via SetGlobalOptionNative().
1082 lines
26 KiB
Go
1082 lines
26 KiB
Go
package action
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
shellquote "github.com/kballard/go-shellquote"
|
|
"github.com/zyedidia/micro/v2/internal/buffer"
|
|
"github.com/zyedidia/micro/v2/internal/clipboard"
|
|
"github.com/zyedidia/micro/v2/internal/config"
|
|
"github.com/zyedidia/micro/v2/internal/screen"
|
|
"github.com/zyedidia/micro/v2/internal/shell"
|
|
"github.com/zyedidia/micro/v2/internal/util"
|
|
)
|
|
|
|
// A Command contains information about how to execute a command
|
|
// It has the action for that command as well as a completer function
|
|
type Command struct {
|
|
action func(*BufPane, []string)
|
|
completer buffer.Completer
|
|
}
|
|
|
|
var commands map[string]Command
|
|
|
|
func InitCommands() {
|
|
commands = map[string]Command{
|
|
"set": {(*BufPane).SetCmd, OptionValueComplete},
|
|
"reset": {(*BufPane).ResetCmd, OptionValueComplete},
|
|
"setlocal": {(*BufPane).SetLocalCmd, OptionValueComplete},
|
|
"show": {(*BufPane).ShowCmd, OptionComplete},
|
|
"showkey": {(*BufPane).ShowKeyCmd, nil},
|
|
"run": {(*BufPane).RunCmd, nil},
|
|
"bind": {(*BufPane).BindCmd, nil},
|
|
"unbind": {(*BufPane).UnbindCmd, nil},
|
|
"quit": {(*BufPane).QuitCmd, nil},
|
|
"goto": {(*BufPane).GotoCmd, nil},
|
|
"jump": {(*BufPane).JumpCmd, nil},
|
|
"save": {(*BufPane).SaveCmd, nil},
|
|
"replace": {(*BufPane).ReplaceCmd, nil},
|
|
"replaceall": {(*BufPane).ReplaceAllCmd, nil},
|
|
"vsplit": {(*BufPane).VSplitCmd, buffer.FileComplete},
|
|
"hsplit": {(*BufPane).HSplitCmd, buffer.FileComplete},
|
|
"tab": {(*BufPane).NewTabCmd, buffer.FileComplete},
|
|
"help": {(*BufPane).HelpCmd, HelpComplete},
|
|
"eval": {(*BufPane).EvalCmd, nil},
|
|
"log": {(*BufPane).ToggleLogCmd, nil},
|
|
"plugin": {(*BufPane).PluginCmd, PluginComplete},
|
|
"reload": {(*BufPane).ReloadCmd, nil},
|
|
"reopen": {(*BufPane).ReopenCmd, nil},
|
|
"cd": {(*BufPane).CdCmd, buffer.FileComplete},
|
|
"pwd": {(*BufPane).PwdCmd, nil},
|
|
"open": {(*BufPane).OpenCmd, buffer.FileComplete},
|
|
"tabmove": {(*BufPane).TabMoveCmd, nil},
|
|
"tabswitch": {(*BufPane).TabSwitchCmd, nil},
|
|
"term": {(*BufPane).TermCmd, nil},
|
|
"memusage": {(*BufPane).MemUsageCmd, nil},
|
|
"retab": {(*BufPane).RetabCmd, nil},
|
|
"raw": {(*BufPane).RawCmd, nil},
|
|
"textfilter": {(*BufPane).TextFilterCmd, nil},
|
|
}
|
|
}
|
|
|
|
// 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 string, action func(bp *BufPane, args []string), completer buffer.Completer) {
|
|
if action != nil {
|
|
commands[name] = Command{action, completer}
|
|
}
|
|
}
|
|
|
|
// 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 *BufPane) bool {
|
|
InfoBar.Prompt("> ", prompt, "Command", nil, func(resp string, canceled bool) {
|
|
if !canceled {
|
|
MainTab().CurPane().HandleCommand(resp)
|
|
}
|
|
})
|
|
return false
|
|
}
|
|
}
|
|
|
|
// CommandAction returns a bindable function which executes the
|
|
// given command
|
|
func CommandAction(cmd string) BufKeyAction {
|
|
return func(h *BufPane) bool {
|
|
MainTab().CurPane().HandleCommand(cmd)
|
|
return false
|
|
}
|
|
}
|
|
|
|
var PluginCmds = []string{"install", "remove", "update", "available", "list", "search"}
|
|
|
|
// PluginCmd installs, removes, updates, lists, or searches for given plugins
|
|
func (h *BufPane) PluginCmd(args []string) {
|
|
if len(args) < 1 {
|
|
InfoBar.Error("Not enough arguments")
|
|
return
|
|
}
|
|
|
|
if h.Buf.Type != buffer.BTLog {
|
|
h.OpenLogBuf()
|
|
}
|
|
|
|
config.PluginCommand(buffer.LogBuf, args[0], args[1:])
|
|
}
|
|
|
|
// RetabCmd changes all spaces to tabs or all tabs to spaces
|
|
// depending on the user's settings
|
|
func (h *BufPane) RetabCmd(args []string) {
|
|
h.Buf.Retab()
|
|
}
|
|
|
|
// RawCmd opens a new raw view which displays the escape sequences micro
|
|
// is receiving in real-time
|
|
func (h *BufPane) RawCmd(args []string) {
|
|
width, height := screen.Screen.Size()
|
|
iOffset := config.GetInfoBarOffset()
|
|
tp := NewTabFromPane(0, 0, width, height-iOffset, NewRawPane(nil))
|
|
Tabs.AddTab(tp)
|
|
Tabs.SetActive(len(Tabs.List) - 1)
|
|
}
|
|
|
|
// TextFilterCmd filters the selection through the command.
|
|
// Selection goes to the command input.
|
|
// On successful run command output replaces the current selection.
|
|
func (h *BufPane) TextFilterCmd(args []string) {
|
|
if len(args) == 0 {
|
|
InfoBar.Error("usage: textfilter arguments")
|
|
return
|
|
}
|
|
sel := h.Cursor.GetSelection()
|
|
if len(sel) == 0 {
|
|
h.Cursor.SelectWord()
|
|
sel = h.Cursor.GetSelection()
|
|
}
|
|
var bout, berr bytes.Buffer
|
|
cmd := exec.Command(args[0], args[1:]...)
|
|
cmd.Stdin = strings.NewReader(string(sel))
|
|
cmd.Stderr = &berr
|
|
cmd.Stdout = &bout
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
InfoBar.Error(err.Error() + " " + berr.String())
|
|
return
|
|
}
|
|
h.Cursor.DeleteSelection()
|
|
h.Buf.Insert(h.Cursor.Loc, bout.String())
|
|
}
|
|
|
|
// TabMoveCmd moves the current tab to a given index (starts at 1). The
|
|
// displaced tabs are moved up.
|
|
func (h *BufPane) TabMoveCmd(args []string) {
|
|
if len(args) <= 0 {
|
|
InfoBar.Error("Not enough arguments: provide an index, starting at 1")
|
|
return
|
|
}
|
|
|
|
if len(args[0]) <= 0 {
|
|
InfoBar.Error("Invalid argument: empty string")
|
|
return
|
|
}
|
|
|
|
num, err := strconv.Atoi(args[0])
|
|
if err != nil {
|
|
InfoBar.Error("Invalid argument: ", err)
|
|
return
|
|
}
|
|
|
|
// Preserve sign for relative move, if one exists
|
|
var shiftDirection byte
|
|
if strings.Contains("-+", string([]byte{args[0][0]})) {
|
|
shiftDirection = args[0][0]
|
|
}
|
|
|
|
// Relative positions -> absolute positions
|
|
idxFrom := Tabs.Active()
|
|
idxTo := 0
|
|
offset := util.Abs(num)
|
|
if shiftDirection == '-' {
|
|
idxTo = idxFrom - offset
|
|
} else if shiftDirection == '+' {
|
|
idxTo = idxFrom + offset
|
|
} else {
|
|
idxTo = offset - 1
|
|
}
|
|
|
|
// Restrain position to within the valid range
|
|
idxTo = util.Clamp(idxTo, 0, len(Tabs.List)-1)
|
|
|
|
activeTab := Tabs.List[idxFrom]
|
|
Tabs.RemoveTab(activeTab.Panes[0].ID())
|
|
Tabs.List = append(Tabs.List, nil)
|
|
copy(Tabs.List[idxTo+1:], Tabs.List[idxTo:])
|
|
Tabs.List[idxTo] = activeTab
|
|
Tabs.UpdateNames()
|
|
Tabs.SetActive(idxTo)
|
|
// InfoBar.Message(fmt.Sprintf("Moved tab from slot %d to %d", idxFrom+1, idxTo+1))
|
|
}
|
|
|
|
// TabSwitchCmd switches to a given tab either by name or by number
|
|
func (h *BufPane) TabSwitchCmd(args []string) {
|
|
if len(args) > 0 {
|
|
num, err := strconv.Atoi(args[0])
|
|
if err != nil {
|
|
// Check for tab with this name
|
|
|
|
found := false
|
|
for i, t := range Tabs.List {
|
|
if t.Panes[t.active].Name() == args[0] {
|
|
Tabs.SetActive(i)
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
InfoBar.Error("Could not find tab: ", err)
|
|
}
|
|
} else {
|
|
num--
|
|
if num >= 0 && num < len(Tabs.List) {
|
|
Tabs.SetActive(num)
|
|
} else {
|
|
InfoBar.Error("Invalid tab index")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// CdCmd changes the current working directory
|
|
func (h *BufPane) CdCmd(args []string) {
|
|
if len(args) > 0 {
|
|
path, err := util.ReplaceHome(args[0])
|
|
if err != nil {
|
|
InfoBar.Error(err)
|
|
return
|
|
}
|
|
err = os.Chdir(path)
|
|
if err != nil {
|
|
InfoBar.Error(err)
|
|
return
|
|
}
|
|
wd, _ := os.Getwd()
|
|
for _, b := range buffer.OpenBuffers {
|
|
if len(b.Path) > 0 {
|
|
b.Path, _ = util.MakeRelative(b.AbsPath, wd)
|
|
if p, _ := filepath.Abs(b.Path); !strings.Contains(p, wd) {
|
|
b.Path = b.AbsPath
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MemUsageCmd 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 (h *BufPane) MemUsageCmd(args []string) {
|
|
InfoBar.Message(util.GetMemStats())
|
|
}
|
|
|
|
// PwdCmd prints the current working directory
|
|
func (h *BufPane) PwdCmd(args []string) {
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
InfoBar.Message(err.Error())
|
|
} else {
|
|
InfoBar.Message(wd)
|
|
}
|
|
}
|
|
|
|
// OpenCmd opens a new buffer with a given filename
|
|
func (h *BufPane) OpenCmd(args []string) {
|
|
if len(args) > 0 {
|
|
filename := args[0]
|
|
// the filename might or might not be quoted, so unquote first then join the strings.
|
|
args, err := shellquote.Split(filename)
|
|
if err != nil {
|
|
InfoBar.Error("Error parsing args ", err)
|
|
return
|
|
}
|
|
if len(args) == 0 {
|
|
return
|
|
}
|
|
filename = strings.Join(args, " ")
|
|
|
|
open := func() {
|
|
b, err := buffer.NewBufferFromFile(filename, buffer.BTDefault)
|
|
if err != nil {
|
|
InfoBar.Error(err)
|
|
return
|
|
}
|
|
h.OpenBuffer(b)
|
|
}
|
|
if h.Buf.Modified() {
|
|
InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
|
|
if !canceled && !yes {
|
|
open()
|
|
} else if !canceled && yes {
|
|
h.Save()
|
|
open()
|
|
}
|
|
})
|
|
} else {
|
|
open()
|
|
}
|
|
} else {
|
|
InfoBar.Error("No filename")
|
|
}
|
|
}
|
|
|
|
// ToggleLogCmd toggles the log view
|
|
func (h *BufPane) ToggleLogCmd(args []string) {
|
|
if h.Buf.Type != buffer.BTLog {
|
|
h.OpenLogBuf()
|
|
} else {
|
|
h.Quit()
|
|
}
|
|
}
|
|
|
|
// ReloadCmd reloads all files (syntax files, colorschemes, plugins...)
|
|
func (h *BufPane) ReloadCmd(args []string) {
|
|
reloadRuntime(true)
|
|
}
|
|
|
|
// ReloadConfig reloads only the configuration
|
|
func ReloadConfig() {
|
|
reloadRuntime(false)
|
|
}
|
|
|
|
func reloadRuntime(reloadPlugins bool) {
|
|
if reloadPlugins {
|
|
err := config.RunPluginFn("deinit")
|
|
if err != nil {
|
|
screen.TermMessage(err)
|
|
}
|
|
}
|
|
|
|
config.InitRuntimeFiles(true)
|
|
|
|
if reloadPlugins {
|
|
config.InitPlugins()
|
|
}
|
|
|
|
err := config.ReadSettings()
|
|
if err != nil {
|
|
screen.TermMessage(err)
|
|
} else {
|
|
parsedSettings := config.ParsedSettings()
|
|
defaultSettings := config.DefaultAllSettings()
|
|
for k := range defaultSettings {
|
|
if _, ok := config.VolatileSettings[k]; ok {
|
|
// reload should not override volatile settings
|
|
continue
|
|
}
|
|
|
|
if _, ok := parsedSettings[k]; ok {
|
|
err = doSetGlobalOptionNative(k, parsedSettings[k])
|
|
} else {
|
|
err = doSetGlobalOptionNative(k, defaultSettings[k])
|
|
}
|
|
if err != nil {
|
|
screen.TermMessage(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if reloadPlugins {
|
|
err = config.LoadAllPlugins()
|
|
if err != nil {
|
|
screen.TermMessage(err)
|
|
}
|
|
}
|
|
|
|
InitBindings()
|
|
InitCommands()
|
|
|
|
if reloadPlugins {
|
|
err = config.RunPluginFn("preinit")
|
|
if err != nil {
|
|
screen.TermMessage(err)
|
|
}
|
|
err = config.RunPluginFn("init")
|
|
if err != nil {
|
|
screen.TermMessage(err)
|
|
}
|
|
err = config.RunPluginFn("postinit")
|
|
if err != nil {
|
|
screen.TermMessage(err)
|
|
}
|
|
}
|
|
|
|
err = config.InitColorscheme()
|
|
if err != nil {
|
|
screen.TermMessage(err)
|
|
}
|
|
for _, b := range buffer.OpenBuffers {
|
|
b.ReloadSettings(true)
|
|
}
|
|
}
|
|
|
|
// ReopenCmd reopens the buffer (reload from disk)
|
|
func (h *BufPane) ReopenCmd(args []string) {
|
|
if h.Buf.Modified() {
|
|
InfoBar.YNPrompt("Save file before reopen?", func(yes, canceled bool) {
|
|
if !canceled && yes {
|
|
h.Save()
|
|
h.ReOpen()
|
|
} else if !canceled {
|
|
h.ReOpen()
|
|
}
|
|
})
|
|
} else {
|
|
h.ReOpen()
|
|
}
|
|
}
|
|
|
|
func (h *BufPane) openHelp(page string) error {
|
|
if data, err := config.FindRuntimeFile(config.RTHelp, page).Data(); err != nil {
|
|
return errors.New(fmt.Sprintf("Unable to load help text for %s: %v", page, err))
|
|
} else {
|
|
helpBuffer := buffer.NewBufferFromString(string(data), page+".md", buffer.BTHelp)
|
|
helpBuffer.SetName("Help " + page)
|
|
helpBuffer.SetOptionNative("hltaberrors", false)
|
|
helpBuffer.SetOptionNative("hltrailingws", false)
|
|
|
|
if h.Buf.Type == buffer.BTHelp {
|
|
h.OpenBuffer(helpBuffer)
|
|
} else {
|
|
h.HSplitBuf(helpBuffer)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HelpCmd tries to open the given help page in a horizontal split
|
|
func (h *BufPane) HelpCmd(args []string) {
|
|
if len(args) < 1 {
|
|
// Open the default help if the user just typed "> help"
|
|
h.openHelp("help")
|
|
} else {
|
|
if config.FindRuntimeFile(config.RTHelp, args[0]) != nil {
|
|
err := h.openHelp(args[0])
|
|
if err != nil {
|
|
InfoBar.Error(err)
|
|
}
|
|
} else {
|
|
InfoBar.Error("Sorry, no help for ", args[0])
|
|
}
|
|
}
|
|
}
|
|
|
|
// VSplitCmd 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 (h *BufPane) VSplitCmd(args []string) {
|
|
if len(args) == 0 {
|
|
// Open an empty vertical split
|
|
h.VSplitAction()
|
|
return
|
|
}
|
|
|
|
buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
|
|
if err != nil {
|
|
InfoBar.Error(err)
|
|
return
|
|
}
|
|
|
|
h.VSplitBuf(buf)
|
|
}
|
|
|
|
// HSplitCmd 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 (h *BufPane) HSplitCmd(args []string) {
|
|
if len(args) == 0 {
|
|
// Open an empty horizontal split
|
|
h.HSplitAction()
|
|
return
|
|
}
|
|
|
|
buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
|
|
if err != nil {
|
|
InfoBar.Error(err)
|
|
return
|
|
}
|
|
|
|
h.HSplitBuf(buf)
|
|
}
|
|
|
|
// EvalCmd evaluates a lua expression
|
|
func (h *BufPane) EvalCmd(args []string) {
|
|
InfoBar.Error("Eval unsupported")
|
|
}
|
|
|
|
// NewTabCmd opens the given file in a new tab
|
|
func (h *BufPane) NewTabCmd(args []string) {
|
|
width, height := screen.Screen.Size()
|
|
iOffset := config.GetInfoBarOffset()
|
|
if len(args) > 0 {
|
|
for _, a := range args {
|
|
b, err := buffer.NewBufferFromFile(a, buffer.BTDefault)
|
|
if err != nil {
|
|
InfoBar.Error(err)
|
|
return
|
|
}
|
|
tp := NewTabFromBuffer(0, 0, width, height-1-iOffset, b)
|
|
Tabs.AddTab(tp)
|
|
Tabs.SetActive(len(Tabs.List) - 1)
|
|
}
|
|
} else {
|
|
b := buffer.NewBufferFromString("", "", buffer.BTDefault)
|
|
tp := NewTabFromBuffer(0, 0, width, height-iOffset, b)
|
|
Tabs.AddTab(tp)
|
|
Tabs.SetActive(len(Tabs.List) - 1)
|
|
}
|
|
}
|
|
|
|
func doSetGlobalOptionNative(option string, nativeValue interface{}) error {
|
|
if reflect.DeepEqual(config.GlobalSettings[option], nativeValue) {
|
|
return nil
|
|
}
|
|
|
|
config.GlobalSettings[option] = nativeValue
|
|
config.ModifiedSettings[option] = true
|
|
delete(config.VolatileSettings, option)
|
|
|
|
if option == "colorscheme" {
|
|
// LoadSyntaxFiles()
|
|
config.InitColorscheme()
|
|
for _, b := range buffer.OpenBuffers {
|
|
b.UpdateRules()
|
|
}
|
|
} else if option == "infobar" || option == "keymenu" {
|
|
Tabs.Resize()
|
|
} else if option == "mouse" {
|
|
if !nativeValue.(bool) {
|
|
screen.Screen.DisableMouse()
|
|
} else {
|
|
screen.Screen.EnableMouse()
|
|
}
|
|
} else if option == "autosave" {
|
|
if nativeValue.(float64) > 0 {
|
|
config.SetAutoTime(int(nativeValue.(float64)))
|
|
config.StartAutoSave()
|
|
} else {
|
|
config.SetAutoTime(0)
|
|
}
|
|
} else if option == "paste" {
|
|
screen.Screen.SetPaste(nativeValue.(bool))
|
|
} else if option == "clipboard" {
|
|
m := clipboard.SetMethod(nativeValue.(string))
|
|
err := clipboard.Initialize(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
for _, pl := range config.Plugins {
|
|
if option == pl.Name {
|
|
if nativeValue.(bool) && !pl.Loaded {
|
|
pl.Load()
|
|
_, err := pl.Call("init")
|
|
if err != nil && err != config.ErrNoSuchFunction {
|
|
screen.TermMessage(err)
|
|
}
|
|
} else if !nativeValue.(bool) && pl.Loaded {
|
|
_, err := pl.Call("deinit")
|
|
if err != nil && err != config.ErrNoSuchFunction {
|
|
screen.TermMessage(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func SetGlobalOptionNative(option string, nativeValue interface{}) error {
|
|
if err := config.OptionIsValid(option, nativeValue); err != nil {
|
|
return err
|
|
}
|
|
|
|
// check for local option first...
|
|
for _, s := range config.LocalSettings {
|
|
if s == option {
|
|
return MainTab().CurPane().Buf.SetOptionNative(option, nativeValue)
|
|
}
|
|
}
|
|
|
|
// ...if it's not local continue with the globals...
|
|
if err := doSetGlobalOptionNative(option, nativeValue); err != nil {
|
|
return err
|
|
}
|
|
|
|
// ...at last check the buffer locals
|
|
for _, b := range buffer.OpenBuffers {
|
|
b.DoSetOptionNative(option, nativeValue)
|
|
delete(b.LocalSettings, option)
|
|
}
|
|
|
|
return config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
|
|
}
|
|
|
|
func SetGlobalOption(option, value string) error {
|
|
if _, ok := config.GlobalSettings[option]; !ok {
|
|
return config.ErrInvalidOption
|
|
}
|
|
|
|
nativeValue, err := config.GetNativeValue(option, config.GlobalSettings[option], value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return SetGlobalOptionNative(option, nativeValue)
|
|
}
|
|
|
|
// ResetCmd resets a setting to its default value
|
|
func (h *BufPane) ResetCmd(args []string) {
|
|
if len(args) < 1 {
|
|
InfoBar.Error("Not enough arguments")
|
|
return
|
|
}
|
|
|
|
option := args[0]
|
|
defaults := config.DefaultAllSettings()
|
|
|
|
if _, ok := defaults[option]; ok {
|
|
SetGlobalOptionNative(option, defaults[option])
|
|
return
|
|
}
|
|
InfoBar.Error(config.ErrInvalidOption)
|
|
}
|
|
|
|
// SetCmd sets an option
|
|
func (h *BufPane) SetCmd(args []string) {
|
|
if len(args) < 2 {
|
|
InfoBar.Error("Not enough arguments")
|
|
return
|
|
}
|
|
|
|
option := args[0]
|
|
value := args[1]
|
|
|
|
err := SetGlobalOption(option, value)
|
|
if err == config.ErrInvalidOption {
|
|
err := h.Buf.SetOption(option, value)
|
|
if err != nil {
|
|
InfoBar.Error(err)
|
|
}
|
|
} else if err != nil {
|
|
InfoBar.Error(err)
|
|
}
|
|
}
|
|
|
|
// SetLocalCmd sets an option local to the buffer
|
|
func (h *BufPane) SetLocalCmd(args []string) {
|
|
if len(args) < 2 {
|
|
InfoBar.Error("Not enough arguments")
|
|
return
|
|
}
|
|
|
|
option := args[0]
|
|
value := args[1]
|
|
|
|
err := h.Buf.SetOption(option, value)
|
|
if err != nil {
|
|
InfoBar.Error(err)
|
|
}
|
|
}
|
|
|
|
// ShowCmd shows the value of the given option
|
|
func (h *BufPane) ShowCmd(args []string) {
|
|
if len(args) < 1 {
|
|
InfoBar.Error("Please provide an option to show")
|
|
return
|
|
}
|
|
|
|
var option interface{}
|
|
if opt, ok := h.Buf.Settings[args[0]]; ok {
|
|
option = opt
|
|
} else if opt, ok := config.GlobalSettings[args[0]]; ok {
|
|
option = opt
|
|
}
|
|
|
|
if option == nil {
|
|
InfoBar.Error(args[0], " is not a valid option")
|
|
return
|
|
}
|
|
|
|
InfoBar.Message(option)
|
|
}
|
|
|
|
func parseKeyArg(arg string) string {
|
|
// If this is a raw escape sequence, convert it to its raw byte form
|
|
return strings.ReplaceAll(arg, "\\x1b", "\x1b")
|
|
}
|
|
|
|
// ShowKeyCmd displays the action that a key is bound to
|
|
func (h *BufPane) ShowKeyCmd(args []string) {
|
|
if len(args) < 1 {
|
|
InfoBar.Error("Please provide a key to show")
|
|
return
|
|
}
|
|
|
|
event, err := findEvent(parseKeyArg(args[0]))
|
|
if err != nil {
|
|
InfoBar.Error(err)
|
|
return
|
|
}
|
|
if action, ok := config.Bindings["buffer"][event.Name()]; ok {
|
|
InfoBar.Message(action)
|
|
} else {
|
|
InfoBar.Message(args[0], " has no binding")
|
|
}
|
|
}
|
|
|
|
// BindCmd creates a new keybinding
|
|
func (h *BufPane) BindCmd(args []string) {
|
|
if len(args) < 2 {
|
|
InfoBar.Error("Not enough arguments")
|
|
return
|
|
}
|
|
|
|
_, err := TryBindKey(parseKeyArg(args[0]), args[1], true)
|
|
if err != nil {
|
|
InfoBar.Error(err)
|
|
}
|
|
}
|
|
|
|
// UnbindCmd binds a key to its default action
|
|
func (h *BufPane) UnbindCmd(args []string) {
|
|
if len(args) < 1 {
|
|
InfoBar.Error("Not enough arguments")
|
|
return
|
|
}
|
|
|
|
err := UnbindKey(parseKeyArg(args[0]))
|
|
if err != nil {
|
|
InfoBar.Error(err)
|
|
}
|
|
}
|
|
|
|
// RunCmd runs a shell command in the background
|
|
func (h *BufPane) RunCmd(args []string) {
|
|
runf, err := shell.RunBackgroundShell(shellquote.Join(args...))
|
|
if err != nil {
|
|
InfoBar.Error(err)
|
|
} else {
|
|
go func() {
|
|
InfoBar.Message(runf())
|
|
screen.Redraw()
|
|
}()
|
|
}
|
|
}
|
|
|
|
// QuitCmd closes the main view
|
|
func (h *BufPane) QuitCmd(args []string) {
|
|
h.Quit()
|
|
}
|
|
|
|
// GotoCmd is a command that will send the cursor to a certain
|
|
// position in the buffer
|
|
// For example: `goto line`, or `goto line:col`
|
|
func (h *BufPane) GotoCmd(args []string) {
|
|
line, col, err := h.parseLineCol(args)
|
|
if err != nil {
|
|
InfoBar.Error(err)
|
|
return
|
|
}
|
|
|
|
if line < 0 {
|
|
line = h.Buf.LinesNum() + 1 + line
|
|
}
|
|
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
|
|
col = util.Clamp(col-1, 0, util.CharacterCount(h.Buf.LineBytes(line)))
|
|
|
|
h.RemoveAllMultiCursors()
|
|
h.Cursor.Deselect(true)
|
|
h.GotoLoc(buffer.Loc{col, line})
|
|
}
|
|
|
|
// JumpCmd is a command that will send the cursor to a certain relative
|
|
// position in the buffer
|
|
// For example: `jump line`, `jump -line`, or `jump -line:col`
|
|
func (h *BufPane) JumpCmd(args []string) {
|
|
line, col, err := h.parseLineCol(args)
|
|
if err != nil {
|
|
InfoBar.Error(err)
|
|
return
|
|
}
|
|
|
|
line = h.Buf.GetActiveCursor().Y + 1 + line
|
|
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
|
|
col = util.Clamp(col-1, 0, util.CharacterCount(h.Buf.LineBytes(line)))
|
|
|
|
h.RemoveAllMultiCursors()
|
|
h.Cursor.Deselect(true)
|
|
h.GotoLoc(buffer.Loc{col, line})
|
|
}
|
|
|
|
// parseLineCol is a helper to parse the input of GotoCmd and JumpCmd
|
|
func (h *BufPane) parseLineCol(args []string) (line int, col int, err error) {
|
|
if len(args) <= 0 {
|
|
return 0, 0, errors.New("Not enough arguments")
|
|
}
|
|
|
|
line, col = 0, 0
|
|
if strings.Contains(args[0], ":") {
|
|
parts := strings.SplitN(args[0], ":", 2)
|
|
line, err = strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
col, err = strconv.Atoi(parts[1])
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
} else {
|
|
line, err = strconv.Atoi(args[0])
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
}
|
|
|
|
return line, col, nil
|
|
}
|
|
|
|
// SaveCmd saves the buffer optionally with an argument file name
|
|
func (h *BufPane) SaveCmd(args []string) {
|
|
if len(args) == 0 {
|
|
h.Save()
|
|
} else {
|
|
h.Buf.SaveAs(args[0])
|
|
}
|
|
}
|
|
|
|
// ReplaceCmd runs search and replace
|
|
func (h *BufPane) ReplaceCmd(args []string) {
|
|
if len(args) < 2 || len(args) > 4 {
|
|
// We need to find both a search and replace expression
|
|
InfoBar.Error("Invalid replace statement: " + strings.Join(args, " "))
|
|
return
|
|
}
|
|
|
|
all := false
|
|
noRegex := false
|
|
|
|
foundSearch := false
|
|
foundReplace := false
|
|
var search string
|
|
var replaceStr string
|
|
for _, arg := range args {
|
|
switch arg {
|
|
case "-a":
|
|
all = true
|
|
case "-l":
|
|
noRegex = true
|
|
default:
|
|
if !foundSearch {
|
|
foundSearch = true
|
|
search = arg
|
|
} else if !foundReplace {
|
|
foundReplace = true
|
|
replaceStr = arg
|
|
} else {
|
|
InfoBar.Error("Invalid flag: " + arg)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if noRegex {
|
|
search = regexp.QuoteMeta(search)
|
|
}
|
|
|
|
replace := []byte(replaceStr)
|
|
|
|
var regex *regexp.Regexp
|
|
var err error
|
|
if h.Buf.Settings["ignorecase"].(bool) {
|
|
regex, err = regexp.Compile("(?im)" + search)
|
|
} else {
|
|
regex, err = regexp.Compile("(?m)" + search)
|
|
}
|
|
if err != nil {
|
|
// There was an error with the user's regex
|
|
InfoBar.Error(err)
|
|
return
|
|
}
|
|
|
|
nreplaced := 0
|
|
start := h.Buf.Start()
|
|
end := h.Buf.End()
|
|
selection := h.Cursor.HasSelection()
|
|
if selection {
|
|
start = h.Cursor.CurSelection[0]
|
|
end = h.Cursor.CurSelection[1]
|
|
}
|
|
if all {
|
|
nreplaced, _ = h.Buf.ReplaceRegex(start, end, regex, replace, !noRegex)
|
|
} else {
|
|
inRange := func(l buffer.Loc) bool {
|
|
return l.GreaterEqual(start) && l.LessEqual(end)
|
|
}
|
|
|
|
searchLoc := h.Cursor.Loc
|
|
var doReplacement func()
|
|
doReplacement = func() {
|
|
locs, found, err := h.Buf.FindNext(search, start, end, searchLoc, true, true)
|
|
if err != nil {
|
|
InfoBar.Error(err)
|
|
return
|
|
}
|
|
if !found || !inRange(locs[0]) || !inRange(locs[1]) {
|
|
h.Cursor.ResetSelection()
|
|
h.Buf.RelocateCursors()
|
|
|
|
return
|
|
}
|
|
|
|
h.Cursor.SetSelectionStart(locs[0])
|
|
h.Cursor.SetSelectionEnd(locs[1])
|
|
h.GotoLoc(locs[0])
|
|
h.Buf.LastSearch = search
|
|
h.Buf.LastSearchRegex = true
|
|
h.Buf.HighlightSearch = h.Buf.Settings["hlsearch"].(bool)
|
|
|
|
InfoBar.YNPrompt("Perform replacement (y,n,esc)", func(yes, canceled bool) {
|
|
if !canceled && yes {
|
|
_, nrunes := h.Buf.ReplaceRegex(locs[0], locs[1], regex, replace, !noRegex)
|
|
|
|
searchLoc = locs[0]
|
|
searchLoc.X += nrunes + locs[0].Diff(locs[1], h.Buf)
|
|
if end.Y == locs[1].Y {
|
|
end = end.Move(nrunes, h.Buf)
|
|
}
|
|
h.Cursor.Loc = searchLoc
|
|
nreplaced++
|
|
} else if !canceled && !yes {
|
|
searchLoc = locs[1]
|
|
} else if canceled {
|
|
h.Cursor.ResetSelection()
|
|
h.Buf.RelocateCursors()
|
|
return
|
|
}
|
|
doReplacement()
|
|
})
|
|
}
|
|
doReplacement()
|
|
}
|
|
|
|
h.Buf.RelocateCursors()
|
|
h.Relocate()
|
|
|
|
var s string
|
|
if nreplaced > 1 {
|
|
s = fmt.Sprintf("Replaced %d occurrences of %s", nreplaced, search)
|
|
} else if nreplaced == 1 {
|
|
s = fmt.Sprintf("Replaced 1 occurrence of %s", search)
|
|
} else {
|
|
s = fmt.Sprintf("Nothing matched %s", search)
|
|
}
|
|
|
|
if selection {
|
|
s += " in selection"
|
|
}
|
|
|
|
InfoBar.Message(s)
|
|
}
|
|
|
|
// ReplaceAllCmd replaces search term all at once
|
|
func (h *BufPane) ReplaceAllCmd(args []string) {
|
|
// aliased to Replace command
|
|
h.ReplaceCmd(append(args, "-a"))
|
|
}
|
|
|
|
// TermCmd opens a terminal in the current view
|
|
func (h *BufPane) TermCmd(args []string) {
|
|
ps := h.tab.Panes
|
|
|
|
if !TermEmuSupported {
|
|
InfoBar.Error("Terminal emulator not supported on this system")
|
|
return
|
|
}
|
|
|
|
if len(args) == 0 {
|
|
sh := os.Getenv("SHELL")
|
|
if sh == "" {
|
|
InfoBar.Error("Shell environment not found")
|
|
return
|
|
}
|
|
args = []string{sh}
|
|
}
|
|
|
|
term := func(i int, newtab bool) {
|
|
t := new(shell.Terminal)
|
|
err := t.Start(args, false, true, nil, nil)
|
|
if err != nil {
|
|
InfoBar.Error(err)
|
|
return
|
|
}
|
|
|
|
id := h.ID()
|
|
if newtab {
|
|
h.AddTab()
|
|
i = 0
|
|
id = MainTab().Panes[0].ID()
|
|
} else {
|
|
MainTab().Panes[i].Close()
|
|
}
|
|
|
|
v := h.GetView()
|
|
tp, err := NewTermPane(v.X, v.Y, v.Width, v.Height, t, id, MainTab())
|
|
if err != nil {
|
|
InfoBar.Error(err)
|
|
return
|
|
}
|
|
MainTab().Panes[i] = tp
|
|
MainTab().SetActive(i)
|
|
}
|
|
|
|
// If there is only one open file we make a new tab instead of overwriting it
|
|
newtab := len(MainTab().Panes) == 1 && len(Tabs.List) == 1
|
|
|
|
if newtab {
|
|
term(0, true)
|
|
return
|
|
}
|
|
|
|
for i, p := range ps {
|
|
if p.ID() == h.ID() {
|
|
if h.Buf.Modified() {
|
|
InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
|
|
if !canceled && !yes {
|
|
term(i, false)
|
|
} else if !canceled && yes {
|
|
h.Save()
|
|
term(i, false)
|
|
}
|
|
})
|
|
} else {
|
|
term(i, false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// HandleCommand handles input from the user
|
|
func (h *BufPane) HandleCommand(input string) {
|
|
args, err := shellquote.Split(input)
|
|
if err != nil {
|
|
InfoBar.Error("Error parsing args ", err)
|
|
return
|
|
}
|
|
|
|
if len(args) == 0 {
|
|
return
|
|
}
|
|
|
|
inputCmd := args[0]
|
|
|
|
if _, ok := commands[inputCmd]; !ok {
|
|
InfoBar.Error("Unknown command ", inputCmd)
|
|
} else {
|
|
WriteLog("> " + input + "\n")
|
|
commands[inputCmd].action(h, args[1:])
|
|
WriteLog("\n")
|
|
}
|
|
}
|