diff --git a/core/commands/config.go b/core/commands/config.go index 7d5a5fa17..8329e972f 100644 --- a/core/commands/config.go +++ b/core/commands/config.go @@ -506,13 +506,18 @@ func setConfig(r repo.Repo, key string, value interface{}) (*ConfigField, error) return getConfig(r, key) } +// parseEditorCommand parses the EDITOR environment variable into command and arguments +func parseEditorCommand(editor string) ([]string, error) { + return shlex.Split(editor, true) +} + func editConfig(filename string) error { editor := os.Getenv("EDITOR") if editor == "" { return errors.New("ENV variable $EDITOR not set") } - editorAndArgs, err := shlex.Split(editor, true) + editorAndArgs, err := parseEditorCommand(editor) if err != nil { return fmt.Errorf("cannot parse $EDITOR value: %s", err) } diff --git a/core/commands/config_test.go b/core/commands/config_test.go index 5eb79c153..fe1660abb 100644 --- a/core/commands/config_test.go +++ b/core/commands/config_test.go @@ -14,3 +14,116 @@ func TestScrubMapInternalDelete(t *testing.T) { t.Errorf("expecting an empty map, got a non-empty map") } } + +func TestEditorParsing(t *testing.T) { + testCases := []struct { + name string + input string + expected []string + hasError bool + }{ + { + name: "simple editor", + input: "vim", + expected: []string{"vim"}, + hasError: false, + }, + { + name: "editor with single flag", + input: "emacs -nw", + expected: []string{"emacs", "-nw"}, + hasError: false, + }, + { + name: "VS Code with wait flag (issue #9375)", + input: "code --wait", + expected: []string{"code", "--wait"}, + hasError: false, + }, + { + name: "VS Code with full path and wait flag (issue #9375)", + input: "/opt/homebrew/bin/code --wait", + expected: []string{"/opt/homebrew/bin/code", "--wait"}, + hasError: false, + }, + { + name: "editor with quoted path containing spaces", + input: "\"/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code\" --wait", + expected: []string{"/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code", "--wait"}, + hasError: false, + }, + { + name: "sublime text with wait flag", + input: "subl -w", + expected: []string{"subl", "-w"}, + hasError: false, + }, + { + name: "nano editor", + input: "nano", + expected: []string{"nano"}, + hasError: false, + }, + { + name: "gedit editor", + input: "gedit", + expected: []string{"gedit"}, + hasError: false, + }, + { + name: "editor with multiple flags", + input: "vim -c 'set number' -c 'set hlsearch'", + expected: []string{"vim", "-c", "set number", "-c", "set hlsearch"}, + hasError: false, + }, + { + name: "trailing backslash (POSIX edge case)", + input: "editor\\", + expected: nil, + hasError: true, + }, + { + name: "double quoted editor name with spaces", + input: "\"code with spaces\" --wait", + expected: []string{"code with spaces", "--wait"}, + hasError: false, + }, + { + name: "single quoted editor with flags", + input: "'my editor' -flag", + expected: []string{"my editor", "-flag"}, + hasError: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := parseEditorCommand(tc.input) + + if tc.hasError { + if err == nil { + t.Errorf("Expected error for input '%s', but got none", tc.input) + } + return + } + + if err != nil { + t.Errorf("Unexpected error for input '%s': %v", tc.input, err) + return + } + + if len(result) != len(tc.expected) { + t.Errorf("Expected %d args, got %d for input '%s'", len(tc.expected), len(result), tc.input) + t.Errorf("Expected: %v", tc.expected) + t.Errorf("Got: %v", result) + return + } + + for i, expected := range tc.expected { + if result[i] != expected { + t.Errorf("Expected arg %d to be '%s', got '%s' for input '%s'", i, expected, result[i], tc.input) + } + } + }) + } +}