11//! Various functions that involve launching the editor.
22use anyhow:: Result ;
3+ use std:: collections:: HashMap ;
34
45/// Launches the user's preferred text editor to edit some initial text,
56/// identified by a unique identifier (to avoid temp file collisions).
@@ -43,41 +44,33 @@ pub fn from_editor(identifier: &str, initial_text: &str) -> Result<String> {
4344 Ok ( edited_text)
4445}
4546
46- /// Checks if the terminal is dumb (TERM environment variable is "dumb")
47- fn is_terminal_dumb ( ) -> bool {
48- std:: env:: var ( "TERM" )
49- . map ( |term| term == "dumb" )
50- . unwrap_or ( false )
51- }
52-
53- /// Implement get_editor_command to match Git's C implementation of `git_editor`.
54- ///
55- /// - Check GIT_EDITOR environment variable first
56- /// - Check git config `core.editor` (editor_program)
57- /// - Check VISUAL if terminal is not dumb
58- /// - Check EDITOR
59- /// - Return error if terminal is dumb and no editor found
60- /// - Fall back to platform defaults (vi on Unix, notepad on Windows)
61- /// - Add comprehensive unit tests for all scenarios
47+ /// Gets the editor command following git's logic:
48+ /// 1. Check GIT_EDITOR environment variable (handy if you want to skip invoking git)
49+ /// 2. Run `git var GIT_EDITOR`, git logic makes use of:
50+ /// - git config (core.editor)
51+ /// - env vars ($GIT_EDITOR, $VISUAL, $EDITOR)
52+ /// - built-in fallback (usually "vi", but "editor" on debian-based distros)
53+ /// 3. We fallback to notepad (Windows) or vi otherwise just in case we don't get something
54+ /// usable from `git var`.
6255fn get_editor_command ( ) -> Result < String > {
63- get_editor_command_impl ( & |key| std:: env:: var ( key) , is_terminal_dumb ( ) )
56+ let env: HashMap < String , String > = std:: env:: vars ( ) . collect ( ) ;
57+ get_editor_command_impl ( & env)
6458}
6559
66- /// Internal get_editor_command_implementation that can be tested without modifying environment
67- fn get_editor_command_impl < F > ( env_var : & F , terminal_is_dumb : bool ) -> Result < String >
68- where
69- F : Fn ( & str ) -> Result < String , std:: env:: VarError > ,
70- {
71- // Try $GIT_EDITOR first
72- if let Ok ( editor) = env_var ( "GIT_EDITOR" )
60+ /// Internal implementation that can be tested with controlled environment
61+ fn get_editor_command_impl ( env : & HashMap < String , String > ) -> Result < String > {
62+ // Check GIT_EDITOR from the env map
63+ if let Some ( editor) = env. get ( "GIT_EDITOR" )
7364 && !editor. is_empty ( )
7465 {
75- return Ok ( editor) ;
66+ return Ok ( editor. clone ( ) ) ;
7667 }
7768
78- // Try git config `core.editor` (editor_program)
69+ // Run git var with the controlled environment
7970 if let Ok ( output) = std:: process:: Command :: new ( gix:: path:: env:: exe_invocation ( ) )
80- . args ( [ "config" , "--get" , "core.editor" ] )
71+ . args ( [ "var" , "GIT_EDITOR" ] )
72+ . env_clear ( )
73+ . envs ( env)
8174 . output ( )
8275 && output. status . success ( )
8376 {
@@ -87,139 +80,74 @@ where
8780 }
8881 }
8982
90- // Try $VISUAL if terminal is not dumb
91- if !terminal_is_dumb
92- && let Ok ( editor) = env_var ( "VISUAL" )
93- && !editor. is_empty ( )
94- {
95- return Ok ( editor) ;
96- }
97-
98- if let Ok ( editor) = env_var ( "EDITOR" )
99- && !editor. is_empty ( )
100- {
101- return Ok ( editor) ;
102- }
103-
104- // If terminal is dumb and no editor was found, return an error
105- if terminal_is_dumb {
106- return Err ( anyhow:: anyhow!(
107- "Terminal is dumb, but no editor specified in GIT_EDITOR, core.editor, or EDITOR"
108- ) ) ;
109- }
110-
111- // Fallback to platform defaults (DEFAULT_EDITOR)
112- let default_editor = if cfg ! ( windows) { "notepad" } else { "vi" } ;
113- Ok ( default_editor. to_string ( ) )
83+ // Simple fallback to platform defaults
84+ Ok ( if cfg ! ( windows) { "notepad" } else { "vi" } . to_string ( ) )
11485}
11586
11687#[ cfg( test) ]
11788mod tests {
11889 use super :: * ;
119- use std:: collections:: HashMap ;
120-
121- // Helper to create a mock environment function from a hashmap
122- fn mock_env ( vars : HashMap < & str , & str > ) -> impl Fn ( & str ) -> Result < String , std:: env:: VarError > {
123- move |key : & str | {
124- vars. get ( key)
125- . map ( |v| v. to_string ( ) )
126- . ok_or ( std:: env:: VarError :: NotPresent )
127- }
128- }
129-
130- #[ test]
131- fn git_editor_takes_precedence ( ) {
132- let env = mock_env ( HashMap :: from ( [
133- ( "GIT_EDITOR" , "git-editor" ) ,
134- ( "VISUAL" , "visual-editor" ) ,
135- ( "EDITOR" , "editor" ) ,
136- ] ) ) ;
137- let actual = get_editor_command_impl ( & env, false ) . unwrap ( ) ;
138- assert_eq ! ( actual, "git-editor" ) ;
139- }
90+ use std:: sync:: LazyLock ;
14091
141- #[ test]
142- fn visual_when_terminal_not_dumb ( ) {
143- let env = mock_env ( visual_and_editor ( ) ) ;
144- let actual = get_editor_command_impl ( & env, false ) . unwrap ( ) ;
145- assert_eq ! ( actual, "visual-editor" ) ;
146- }
147-
148- #[ test]
149- fn skips_visual_when_terminal_dumb ( ) {
150- let env = mock_env ( visual_and_editor ( ) ) ;
151- let actual = get_editor_command_impl ( & env, true ) . unwrap ( ) ;
152- assert_eq ! ( actual, "editor" , "Should skip VISUAL and use EDITOR" ) ;
153- }
92+ const OUR_PLATFORM_DEFAULT : & str = if cfg ! ( windows) { "notepad" } else { "vi" } ;
15493
155- #[ test]
156- fn uses_editor ( ) {
157- let env = mock_env ( HashMap :: from ( [ ( "EDITOR" , "editor" ) ] ) ) ;
158- let actual = get_editor_command_impl ( & env, false ) . unwrap ( ) ;
159- assert_eq ! ( actual, "editor" ) ;
160- }
161-
162- #[ test]
163- fn fails_when_terminal_dumb_and_no_editor ( ) {
164- let env = mock_env ( HashMap :: new ( ) ) ;
165- let actual = get_editor_command_impl ( & env, true ) ;
166- assert ! ( actual. unwrap_err( ) . to_string( ) . contains( "Terminal is dumb" ) ) ;
167- }
94+ static BASIC_ENV : LazyLock < HashMap < String , String > > = LazyLock :: new ( || {
95+ HashMap :: from ( [
96+ ( "GIT_EDITOR" . to_string ( ) , "from-GIT_EDITOR" . to_string ( ) ) ,
97+ ( "EDITOR" . to_string ( ) , "from-EDITOR" . to_string ( ) ) ,
98+ ( "VISUAL" . to_string ( ) , "from-VISUAL" . to_string ( ) ) ,
99+ // Just an example value, otherwise git thinks the terminal is "dumb" and follows other logic
100+ ( "TERM" . to_string ( ) , "xterm-256color" . to_string ( ) ) ,
101+ ] )
102+ } ) ;
168103
169104 #[ test]
170- fn fails_when_terminal_dumb_with_only_visual ( ) {
171- let env = mock_env ( HashMap :: from ( [ ( "VISUAL" , "visual-editor" ) ] ) ) ;
172- let actual = get_editor_command_impl ( & env, true ) ;
173- assert ! (
174- actual. unwrap_err( ) . to_string( ) . contains( "Terminal is dumb" ) ,
175- "VISUAL isn't used in dumb terminals"
105+ fn git_editor_takes_precedence ( ) {
106+ let actual = get_editor_command_impl ( & BASIC_ENV . clone ( ) ) . unwrap ( ) ;
107+ assert_eq ! (
108+ actual, "from-GIT_EDITOR" ,
109+ "GIT_EDITOR should take precedence"
176110 ) ;
177111 }
178112
179113 #[ test]
180- fn ignores_empty_git_editor ( ) {
181- let env = mock_env ( HashMap :: from ( [
182- ( "GIT_EDITOR" , "" ) ,
183- ( "VISUAL" , "visual-editor" ) ,
184- ( "EDITOR" , "editor" ) ,
185- ] ) ) ;
186- let actual = get_editor_command_impl ( & env, false ) . unwrap ( ) ;
114+ fn falls_through_to_git_var_with_editor ( ) {
115+ let mut env = BASIC_ENV . clone ( ) ;
116+ env. remove ( "GIT_EDITOR" ) ; // Ensure GIT_EDITOR is not set
117+ let actual = get_editor_command_impl ( & env) . unwrap ( ) ;
187118 assert_eq ! (
188- actual, "visual-editor " ,
189- "Empty GIT_EDITOR should be ignored, fall through to VISUAL "
119+ actual, "from-VISUAL " ,
120+ "git var should respect VISUAL env var "
190121 ) ;
191122 }
192123
193124 #[ test]
194- fn ignores_empty_visual ( ) {
195- let env = mock_env ( HashMap :: from ( [ ( "VISUAL" , "" ) , ( "EDITOR" , "editor" ) ] ) ) ;
196- let actual = get_editor_command_impl ( & env, false ) . unwrap ( ) ;
125+ fn falls_through_to_git_fallback_when_nothing_set ( ) {
126+ let mut env = HashMap :: new ( ) ;
127+ env. insert ( "TERM" . to_string ( ) , "xterm-256color" . to_string ( ) ) ;
128+ let actual = get_editor_command_impl ( & env) . unwrap ( ) ;
129+ // Our CI uses debian/ubuntu linux, which compiles git with "editor" as fallback
130+ // Other platforms should have the default fallback from editor.c, which is "vi"
131+ let expected = if cfg ! ( target_os = "linux" ) {
132+ "editor"
133+ } else {
134+ "vi"
135+ } ;
197136 assert_eq ! (
198- actual, "editor" ,
199- "Empty VISUAL should be ignored, fall through to EDITOR "
137+ actual, expected ,
138+ "git fallback should be used without any other env vars "
200139 ) ;
201140 }
202141
203142 #[ test]
204- fn ignores_empty_editor ( ) {
205- let env = mock_env ( HashMap :: from ( [ ( "EDITOR" , "" ) ] ) ) ;
206- let actual = get_editor_command_impl ( & env, false ) . unwrap ( ) ;
143+ fn falls_back_when_nothing_set ( ) {
144+ // Empty environment, git considers this "dumb terminal" and `git var` will return empty string
145+ // so our own fallback will be used
146+ let mut env = HashMap :: new ( ) ;
147+ let actual = get_editor_command_impl ( & env) . unwrap ( ) ;
207148 assert_eq ! (
208- actual, PLATFORM_DEFAULT ,
209- "Empty EDITOR should be ignored, fall back to default "
149+ actual, OUR_PLATFORM_DEFAULT ,
150+ "Should fall back to vi/notepad when nothing is set "
210151 ) ;
211152 }
212-
213- #[ test]
214- fn falls_back_to_default_when_no_vars_set ( ) {
215- let env = mock_env ( HashMap :: new ( ) ) ;
216- let actual = get_editor_command_impl ( & env, false ) . unwrap ( ) ;
217- assert_eq ! ( actual, PLATFORM_DEFAULT ) ;
218- }
219-
220- const PLATFORM_DEFAULT : & str = if cfg ! ( windows) { "notepad" } else { "vi" } ;
221-
222- fn visual_and_editor ( ) -> HashMap < & ' static str , & ' static str > {
223- HashMap :: from ( [ ( "VISUAL" , "visual-editor" ) , ( "EDITOR" , "editor" ) ] )
224- }
225153}
0 commit comments