Plandex_backup / app /cli /lib /editor.go
google-labs-jules[bot]
Final deployment for HF with landing page
93d826e
package lib
import (
"os"
"os/exec"
"path/filepath"
"plandex-cli/api"
"plandex-cli/term"
shared "plandex-shared"
"sort"
"strings"
)
func MaybePromptAndOpen(path string, defaultConfig *shared.PlanConfig, planConfig *shared.PlanConfig) bool {
var cmd string
var args []string
var openManually bool
var checkConfig *shared.PlanConfig
if planConfig != nil {
checkConfig = planConfig
} else if defaultConfig != nil {
checkConfig = defaultConfig
} else {
term.OutputErrorAndExit("Missing config")
}
if checkConfig.EditorOpenManually {
return false
}
if checkConfig.EditorCommand == "" {
editorRes := SelectEditor(true)
cmd = editorRes.Cmd
args = editorRes.Args
openManually = editorRes.OpenManually
// update the default editor config
toUpdateDefault := *defaultConfig
toUpdateDefault.Editor = editorRes.Name
toUpdateDefault.EditorCommand = cmd
toUpdateDefault.EditorArgs = args
toUpdateDefault.EditorOpenManually = openManually
apiErr := api.Client.UpdateDefaultPlanConfig(shared.UpdateDefaultPlanConfigRequest{
Config: &toUpdateDefault,
})
if apiErr != nil {
term.OutputErrorAndExit("Error updating default config: %v", apiErr)
}
// also update the current plan config
if planConfig != nil {
toUpdate := *planConfig
toUpdate.Editor = editorRes.Name
toUpdate.EditorCommand = cmd
toUpdate.EditorArgs = args
toUpdate.EditorOpenManually = openManually
apiErr = api.Client.UpdatePlanConfig(CurrentPlanId, shared.UpdatePlanConfigRequest{
Config: &toUpdate,
})
if apiErr != nil {
term.OutputErrorAndExit("Error updating plan config: %v", apiErr)
}
}
} else {
cmd = checkConfig.EditorCommand
args = checkConfig.EditorArgs
}
if openManually {
return false
}
err := exec.Command(cmd, append(args, path)...).Start()
if err != nil {
term.OutputErrorAndExit("Error opening template: %v", err)
}
return true
}
type SelectEditorResult struct {
Name string
Cmd string
Args []string
OpenManually bool
}
func SelectEditor(includeOpenManuallyOpt bool) SelectEditorResult {
editors := detectEditors()
opts := []string{}
for _, c := range editors {
opts = append(opts, c.name)
}
const otherOpt = "Other (custom command)"
opts = append(opts, otherOpt)
const openManuallyOpt = "Open files manually"
if includeOpenManuallyOpt {
opts = append(opts, openManuallyOpt)
}
choice, err := term.SelectFromList("What's your preferred editor?", opts)
if err != nil {
term.OutputErrorAndExit("Error selecting editor: %v", err)
}
var name string
var cmd string
var args []string
if choice == otherOpt {
choice, err = term.GetRequiredUserStringInput("Enter the command to open the editor")
if err != nil {
term.OutputErrorAndExit("Error getting editor command: %v", err)
}
name = choice
parts := strings.Fields(choice)
if len(parts) == 0 {
term.OutputErrorAndExit("Invalid editor command: %s", choice)
}
cmd = parts[0]
if len(parts) > 1 {
args = parts[1:]
}
} else if choice == openManuallyOpt {
return SelectEditorResult{
Name: "Open manually",
Cmd: "",
Args: []string{},
OpenManually: true,
}
} else {
var candidate editorCandidate
for _, c := range editors {
if c.name == choice {
candidate = c
break
}
}
name = candidate.name
cmd = candidate.cmd
args = candidate.args
}
return SelectEditorResult{
Name: name,
Cmd: cmd,
Args: args,
}
}
type editorCandidate struct {
name string
cmd string
args []string
isJetBrains bool
}
const maxEditorOpts = 5
func detectEditors() []editorCandidate {
guess := []editorCandidate{
// Popular non-JetBrains launchers
{"VS Code", "code", nil, false},
{"Cursor", "cursor", nil, false},
{"Zed", "zed", nil, false},
{"Neovim", "nvim", nil, false},
// JetBrains IDE-specific launchers
{"IntelliJ IDEA", "idea", nil, true},
{"GoLand", "goland", nil, true},
{"PyCharm", "pycharm", nil, true},
{"CLion", "clion", nil, true},
{"WebStorm", "webstorm", nil, true},
{"PhpStorm", "phpstorm", nil, true},
{"DataGrip", "datagrip", nil, true},
{"RubyMine", "rubymine", nil, true},
{"Rider", "rider", nil, true},
{"DataSpell", "dataspell", nil, true},
// JetBrains universal CLI (2023.2+)
{"JetBrains (jb)", "jb", []string{"open"}, true},
{"Vim", "vim", nil, false},
{"Nano", "nano", nil, false},
{"Helix", "hx", nil, false},
{"Micro", "micro", nil, false},
{"Sublime Text", "subl", nil, false},
{"TextMate", "mate", nil, false},
{"Kakoune", "kak", nil, false},
{"Emacs", "emacs", nil, false},
{"Kate", "kate", nil, false},
}
pref := map[string]bool{}
for _, env := range []string{"VISUAL", "EDITOR"} {
if v := os.Getenv(env); v != "" {
// keep only the binary name, drop path/flags
cmd := filepath.Base(strings.Fields(v)[0])
pref[cmd] = true
}
}
_, err := exec.LookPath("jb") // true if universal launcher exists
jbOnPath := err == nil
var found []editorCandidate
for _, c := range guess {
if _, err := exec.LookPath(c.cmd); err != nil {
continue // not on PATH
}
// If jb is present, drop per-IDE launchers *unless* this exact cmd
// is marked preferred by VISUAL/EDITOR.
if jbOnPath && c.isJetBrains && !pref[c.cmd] {
continue
}
found = append(found, c)
}
for cmd := range pref {
if _, err := exec.LookPath(cmd); err == nil {
already := false
for _, c := range found {
if c.cmd == cmd {
already = true
break
}
}
if !already {
found = append(found, editorCandidate{name: cmd, cmd: cmd})
}
}
}
sort.SliceStable(found, func(i, j int) bool {
pi, pj := pref[found[i].cmd], pref[found[j].cmd]
if pi == pj {
return false // keep original order
}
return pi // true → i comes before j
})
if len(found) > maxEditorOpts {
found = found[:maxEditorOpts]
}
return found
}