diff --git a/CMDREF.md b/CMDREF.md index 508f2dea..17c4d460 100644 --- a/CMDREF.md +++ b/CMDREF.md @@ -33,6 +33,9 @@ Command This is shortcut of: volt profile rm -current {repository} [{repository2} ...] + edit [-e|--editor {editor}] {repository} [{repository2} ...] + Open the plugconf file(s) of one or more {repository} for editing. + profile set {name} Set profile name @@ -112,6 +115,23 @@ Description volt profile rm {current profile} {repository} [{repository2} ...] ``` +# volt edit + +``` +Usage + volt edit [-help] [-e|--editor {editor}] {repository} [{repository2} ...] + +Quick example + $ volt edit tyru/caw.vim # will open the plugconf file for tyru/caw.vim for editing + +Description + Open the plugconf file(s) of one or more {repository} for editing. + + If the -e option was given, use the given editor for editing those files (unless it cannot be found) + + It also calls "volt build" afterwards if modifications were made to the plugconf file(s). +``` + # volt enable ``` diff --git a/README.md b/README.md index e43552c3..92314d8b 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,13 @@ create_skeleton_plugconf = true # installed, it tries to execute "git clone" or "git pull" as a fallback # * false: "volt get" or "volt get -u" won't try to execute fallback commands fallback_git_cmd = true + +[edit] +# If you ever wanted to use emacs to edit your vim plugin config, you can +# do so with the following. If not specified, volt will try to use +# vim/nvim, $VISUAL, sensible-editor, or $EDITOR in this order until a usable +# one is found. +editor = "emacs" ``` ## Features diff --git a/_contrib/completion/bash b/_contrib/completion/bash index e8d60763..2cc4693a 100644 --- a/_contrib/completion/bash +++ b/_contrib/completion/bash @@ -26,7 +26,7 @@ this_plug() { volt list -f "{{ range .Profiles }}{{ if eq \"$1\" .Name }}{{ range .ReposPath }}{{ println . }}{{ end }}{{ end }}{{ end }}" | sed -E 's@^(www\.)?github\.com/@@' | sort -u } -CMDS="get rm list enable disable profile build migrate self-upgrade version" +CMDS="get rm list enable disable edit profile build migrate self-upgrade version" PROFILE_CMDS="set show list new destroy rename add rm" MIGRATE_CMDS="lockjson plugconf/config-func" @@ -53,7 +53,7 @@ _volt() { elif [[ "${first}" == "profile" && "${last}" == "profile" ]] ; then COMPREPLY=( $(compgen -W "${PROFILE_CMDS}" -- ${cur}) ) - elif [[ "${first}" =~ ^(rm|disable)$ ]] ; then + elif [[ "${first}" =~ ^(rm|disable|edit)$ ]] ; then local profile=$(get_profile) plugs=$(get_plugs "$profile" "this") COMPREPLY=( $(compgen -W "${plugs}" -- ${cur}) ) diff --git a/config/config.go b/config/config.go index fc8feedf..b390be7c 100644 --- a/config/config.go +++ b/config/config.go @@ -12,6 +12,7 @@ type Config struct { Alias map[string][]string `toml:"alias"` Build configBuild `toml:"build"` Get configGet `toml:"get"` + Edit configEdit `toml:"edit"` } // configBuild is a config for 'volt build'. @@ -25,6 +26,11 @@ type configGet struct { FallbackGitCmd *bool `toml:"fallback_git_cmd"` } +// configEdit is a config for 'volt edit'. +type configEdit struct { + Editor string `toml:"editor"` +} + const ( // SymlinkBuilder creates symlinks when 'volt build'. SymlinkBuilder = "symlink" @@ -43,6 +49,9 @@ func initialConfigTOML() *Config { CreateSkeletonPlugconf: &trueValue, FallbackGitCmd: &falseValue, }, + Edit: configEdit{ + Editor: "", + }, } } @@ -76,6 +85,9 @@ func merge(cfg, initCfg *Config) { if cfg.Get.FallbackGitCmd == nil { cfg.Get.FallbackGitCmd = initCfg.Get.FallbackGitCmd } + if cfg.Edit.Editor == "" { + cfg.Edit.Editor = initCfg.Edit.Editor + } } func validate(cfg *Config) error { diff --git a/subcmd/edit.go b/subcmd/edit.go new file mode 100644 index 00000000..85740065 --- /dev/null +++ b/subcmd/edit.go @@ -0,0 +1,221 @@ +package subcmd + +import ( + "errors" + "flag" + "fmt" + "os" + "os/exec" + + "github.com/vim-volt/volt/config" + "github.com/vim-volt/volt/lockjson" + "github.com/vim-volt/volt/logger" + "github.com/vim-volt/volt/pathutil" + "github.com/vim-volt/volt/subcmd/builder" +) + +func init() { + cmdMap["edit"] = &editCmd{} +} + +type editCmd struct { + helped bool + editor string +} + +func (cmd *editCmd) ProhibitRootExecution(args []string) bool { return true } + +func (cmd *editCmd) FlagSet() *flag.FlagSet { + fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) + fs.SetOutput(os.Stdout) + fs.Usage = func() { + fmt.Print(` +Usage + volt edit [-help] [-e|--editor {editor}] {repository} [{repository2} ...] + +Quick example + $ volt edit tyru/caw.vim # will open the plugconf file for tyru/caw.vim for editing + +Description + Open the plugconf file(s) of one or more {repository} for editing. + + If the -e option was given, use the given editor for editing those files (unless it cannot be found) + + It also calls "volt build" afterwards if modifications were made to the plugconf file(s).` + "\n\n") + //fmt.Println("Options") + //fs.PrintDefaults() + fmt.Println() + cmd.helped = true + } + fs.StringVar(&cmd.editor, "editor", "", "Use the given editor for editing the plugconf files") + fs.StringVar(&cmd.editor, "e", "", "Use the given editor for editing the plugconf files") + return fs +} + +func (cmd *editCmd) Run(args []string) *Error { + reposPathList, err := cmd.parseArgs(args) + if err == ErrShowedHelp { + return nil + } + if err != nil { + return &Error{Code: 10, Msg: "Failed to parse args: " + err.Error()} + } + + hasChanges, err := cmd.doEdit(reposPathList) + if err != nil { + return &Error{Code: 15, Msg: "Failed to edit plugconf file: " + err.Error()} + } + + // Build opt dir + if hasChanges { + err = builder.Build(false) + if err != nil { + return &Error{Code: 12, Msg: "Could not build " + pathutil.VimVoltDir() + ": " + err.Error()} + } + } + + return nil +} + +func (cmd *editCmd) doEdit(reposPathList []pathutil.ReposPath) (bool, error) { + // Read lock.json + lockJSON, err := lockjson.Read() + if err != nil { + return false, err + } + + // Read config.toml + cfg, err := config.Read() + if err != nil { + return false, errors.New("could not read config.toml: " + err.Error()) + } + + editor, err := cmd.identifyEditor(cfg) + if err != nil || editor == "" { + return false, &Error{Code: 30, Msg: "No usable editor found"} + } + + changeWasMade := false + for _, reposPath := range reposPathList { + + // Edit plugconf file + plugconfPath := reposPath.Plugconf() + + // Install a new template if none exists + if !pathutil.Exists(plugconfPath) { + getCmd := new(getCmd) + logger.Debugf("Installing new plugconf for '%s'.", reposPath) + getCmd.downloadPlugconf(reposPath) + } + + // Remember modification time before opening the editor + info, err := os.Stat(plugconfPath) + if err != nil { + return false, err + } + mTimeBefore := info.ModTime() + + // Call the editor with the plugconf file + editorCmd := exec.Command(editor, plugconfPath) + editorCmd.Stdin = os.Stdin + editorCmd.Stdout = os.Stdout + if err = editorCmd.Run(); err != nil { + logger.Error("Error calling editor for '%s': %s", reposPath, err.Error) + continue + } + + // Get modification time after closing the editor + info, err = os.Stat(plugconfPath) + if err != nil { + return false, err + } + mTimeAfter := info.ModTime() + + // A change was made if the modification time was updated + changeWasMade = changeWasMade || mTimeAfter.After(mTimeBefore) + + // Remove repository from lock.json + err = lockJSON.Repos.RemoveAllReposPath(reposPath) + err2 := lockJSON.Profiles.RemoveAllReposPath(reposPath) + if err == nil || err2 == nil { + // ignore? + } + } + + // Write to lock.json + if err = lockJSON.Write(); err != nil { + return changeWasMade, err + } + return changeWasMade, nil +} + +func (cmd *editCmd) parseArgs(args []string) (pathutil.ReposPathList, error) { + fs := cmd.FlagSet() + fs.Parse(args) + if cmd.helped { + return nil, ErrShowedHelp + } + + if len(fs.Args()) == 0 { + fs.Usage() + return nil, errors.New("repository was not given") + } + + // Normalize repos path + reposPathList := make(pathutil.ReposPathList, 0, len(fs.Args())) + for _, arg := range fs.Args() { + reposPath, err := pathutil.NormalizeRepos(arg) + if err != nil { + return nil, err + } + reposPathList = append(reposPathList, reposPath) + } + + return reposPathList, nil +} + +func (cmd *editCmd) identifyEditor(cfg *config.Config) (string, error) { + editors := make([]string, 0, 6) + + // if an editor is specified as commandline argument, consider it + // as alternative + if cmd.editor != "" { + editors = append(editors, cmd.editor) + } + + // if an editor is configured in the config.toml, consider it as + // alternative + if cfg.Edit.Editor != "" { + editors = append(editors, cfg.Edit.Editor) + } + + vimExecutable, err := pathutil.VimExecutable() + if err != nil { + logger.Debug("No vim executable found in $PATH") + } else { + editors = append(editors, vimExecutable) + } + + // specifiy a fixed list of other alternatives + editors = append(editors, "$VISUAL", "sensible-editor", "$EDITOR") + + for _, editor := range editors { + // resolve content of environment variables + var editorName string + if editor[0] == '$' { + editorName = os.Getenv(editor[1:]) + } else { + editorName = editor + } + + path, err := exec.LookPath(editorName) + if err != nil { + logger.Debug(editorName + " not found in $PATH") + } else if path != "" { + logger.Debug("Using " + path + " as editor") + return editorName, nil + } + } + + return "", errors.New("No usable editor found") +} diff --git a/subcmd/help.go b/subcmd/help.go index 49bf679a..f86dceca 100644 --- a/subcmd/help.go +++ b/subcmd/help.go @@ -58,6 +58,9 @@ Command This is shortcut of: volt profile rm -current {repository} [{repository2} ...] + edit [-e|--editor {editor}] {repository} [{repository2} ...] + Open the plugconf file(s) of one or more {repository} for editing. + profile set {name} Set profile name