28e0e20651
* Improve buffer view relocation after jumping to a far-away location When the cursor is moved to a location which is far away from the current location (e.g. after a search or a goto line), the buffer view is always relocated in such a way that the cursor is at the bottom or at the top (minus scrollmargin), i.e. as if we just scrolled to this location. It's not like in other editors, and IMHO it's annoying. When we jump to a new location far away, we usually want to see more of its context, so the cursor should be placed closer to the center of the view, not near its edges. This change implements the behavior similar to other editors: - If the distance between the new and the old location is less than one frame (i.e. the view either doesn't change or just slightly "shifts") then the current behavior remains unchanged. - Otherwise the current line is placed at 25% of the window height. * Postpone calling onBufPaneOpen until the initial resize It is currently not possible to find out the geometry of a newly created bufpane in onBufPaneOpen lua callback: bp:GetView() returns {0,0,0,0} instead of the actual window. The reason is that the bufpane view is not properly initialized yet when the bufpane is created and the callback is triggered. It is initialized a bit later, at the initial resize. So postpone calling onBufPaneOpen until after the initial resize. * Improve buffer view relocation when opening a file at a far-away location When a file is opened with the initial cursor location at a given line which is far away from the beginning of the file, the buffer view is relocated so that the cursor is at the bottom (minus scrollmargin) as if we just scrolled to this line, which is annoying since we'd rather like to see more of the context of this initial location. So implement the behavior similar to the earlier commit (which addresses a similar issue about jumping far away after a search or goto): - If the initial cursor location is less than one frame away from the beginning of the buffer, keep the existing behavior i.e. just display the beginning of the buffer. - Otherwise place the cursor location at 25% of the window height.
983 lines
23 KiB
Go
983 lines
23 KiB
Go
package action
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"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},
|
|
"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.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...)
|
|
func (h *BufPane) ReloadCmd(args []string) {
|
|
ReloadConfig()
|
|
}
|
|
|
|
func ReloadConfig() {
|
|
config.InitRuntimeFiles()
|
|
err := config.ReadSettings()
|
|
if err != nil {
|
|
screen.TermMessage(err)
|
|
}
|
|
err = config.InitGlobalSettings()
|
|
if err != nil {
|
|
screen.TermMessage(err)
|
|
}
|
|
InitBindings()
|
|
InitCommands()
|
|
|
|
err = config.InitColorscheme()
|
|
if err != nil {
|
|
screen.TermMessage(err)
|
|
}
|
|
|
|
for _, b := range buffer.OpenBuffers {
|
|
b.UpdateRules()
|
|
}
|
|
}
|
|
|
|
// 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.Buf.ReOpen()
|
|
} else if !canceled {
|
|
h.Buf.ReOpen()
|
|
}
|
|
})
|
|
} else {
|
|
h.Buf.ReOpen()
|
|
}
|
|
}
|
|
|
|
func (h *BufPane) openHelp(page string) error {
|
|
if data, err := config.FindRuntimeFile(config.RTHelp, page).Data(); err != nil {
|
|
return errors.New(fmt.Sprint("Unable to load help text", page, "\n", err))
|
|
} else {
|
|
helpBuffer := buffer.NewBufferFromString(string(data), page+".md", buffer.BTHelp)
|
|
helpBuffer.SetName("Help " + page)
|
|
|
|
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 SetGlobalOptionNative(option string, nativeValue interface{}) error {
|
|
local := false
|
|
for _, s := range config.LocalSettings {
|
|
if s == option {
|
|
local = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !local {
|
|
config.GlobalSettings[option] = nativeValue
|
|
config.ModifiedSettings[option] = true
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, b := range buffer.OpenBuffers {
|
|
b.SetOptionNative(option, nativeValue)
|
|
}
|
|
|
|
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]
|
|
|
|
defaultGlobals := config.DefaultGlobalSettings()
|
|
defaultLocals := config.DefaultCommonSettings()
|
|
|
|
if _, ok := defaultGlobals[option]; ok {
|
|
SetGlobalOptionNative(option, defaultGlobals[option])
|
|
return
|
|
}
|
|
if _, ok := defaultLocals[option]; ok {
|
|
h.Buf.SetOptionNative(option, defaultLocals[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)
|
|
}
|
|
|
|
// 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(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(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(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) {
|
|
if len(args) <= 0 {
|
|
InfoBar.Error("Not enough arguments")
|
|
} else {
|
|
h.RemoveAllMultiCursors()
|
|
if strings.Contains(args[0], ":") {
|
|
parts := strings.SplitN(args[0], ":", 2)
|
|
line, err := strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
InfoBar.Error(err)
|
|
return
|
|
}
|
|
col, err := strconv.Atoi(parts[1])
|
|
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.GotoLoc(buffer.Loc{col, line})
|
|
} else {
|
|
line, err := strconv.Atoi(args[0])
|
|
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)
|
|
h.GotoLoc(buffer.Loc{0, line})
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
} 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)
|
|
|
|
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")
|
|
}
|
|
}
|