250 lines
5.8 KiB
Go
250 lines
5.8 KiB
Go
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
|
|
}
|