eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
//! Editor integration commands.
//!
//! Handles opening files in external editors with line number support.

use crate::commands::{Command, CommandResult};
use crate::services::GitService;
use crate::app::{AppState, Action, reducer};
use crate::errors::CommandError;
use tracing::instrument;

/// Open selected path in $EDITOR
pub struct OpenInEditorCommand {
    pub path: String,
    pub line: Option<usize>,
}

impl Command for OpenInEditorCommand {
    #[instrument(skip(self, _git, state), fields(path = %self.path))]
    fn execute(
        &self,
        _git: &GitService,
        state: &AppState,
    ) -> Result<CommandResult, CommandError> {
        let mut new_state = state.clone();
        if self.path.is_empty() {
            new_state = reducer(new_state, Action::SetStatusError(Some("No file selected".into())));
            return Ok(CommandResult::StateUpdate(new_state));
        }
        
        // Resolve path relative to repo root
        let file_path = if self.path.starts_with('/') {
            // Absolute path
            std::path::PathBuf::from(&self.path)
        } else {
            // Relative path - resolve from repo root
            std::path::PathBuf::from(&state.repo_path).join(&self.path)
        };
        
        // Check if file exists
        if !file_path.exists() {
            new_state = reducer(new_state, Action::SetStatusError(Some(
                format!("File not found: {}", file_path.display())
            )));
            return Ok(CommandResult::StateUpdate(new_state));
        }
        
        // Get editor command from environment or default
        // TODO: Add editor_command to AppState to support config-based editor commands
        let editor_cmd = std::env::var("EDITOR")
            .ok()
            .unwrap_or_else(|| "vi".to_string());
        
        // Build editor command with line number support
        let mut cmd = std::process::Command::new(&editor_cmd);
        
        // Try to detect editor type and add line number argument
        if let Some(line) = self.line {
            if editor_cmd.contains("code") || editor_cmd.contains("vscode") {
                // VS Code: --goto file:line
                cmd.arg("--goto").arg(format!("{}:{}", file_path.display(), line));
            } else if editor_cmd.contains("vim") || editor_cmd.contains("vi") || editor_cmd.contains("nvim") {
                // Vim/Neovim: +line_number
                cmd.arg(format!("+{}", line));
            } else if editor_cmd.contains("nano") {
                // Nano: +line_number
                cmd.arg(format!("+{}", line));
            } else if editor_cmd.contains("emacs") {
                // Emacs: +line_number
                cmd.arg(format!("+{}", line));
            } else {
                // Unknown editor - just add file path
                cmd.arg(&file_path);
            }
        } else {
            cmd.arg(&file_path);
        }
        
        // Execute editor with error recovery
        match cmd.status() {
            Ok(status) => {
                if !status.success() {
                    // Try fallback to simple editor invocation
                    let fallback_editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
                    let fallback_status = std::process::Command::new(&fallback_editor)
                        .arg(&file_path)
                        .status();
                    
                    if let Ok(fallback_status) = fallback_status {
                        if !fallback_status.success() {
                            new_state = reducer(new_state, Action::SetStatusError(Some(
                                format!("Editor exited with code {}", fallback_status.code().unwrap_or(-1))
                            )));
                        }
                    } else {
                        new_state = reducer(new_state, Action::SetStatusError(Some(
                            format!("Failed to open editor: {}", editor_cmd)
                        )));
                    }
                }
            }
            Err(e) => {
                // Try fallback editor
                let fallback_editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
                let fallback_result = std::process::Command::new(&fallback_editor)
                    .arg(&file_path)
                    .status();
                
                match fallback_result {
                    Ok(_) => {
                        // Fallback succeeded
                    }
                    Err(_) => {
                        new_state = reducer(new_state, Action::SetStatusError(Some(
                            format!("Failed to open editor '{}': {}. Tried fallback '{}' but it also failed.", 
                                editor_cmd, e, fallback_editor)
                        )));
                    }
                }
            }
        }
        
        Ok(CommandResult::StateUpdate(new_state))
    }
}