From 50b0222e35929f5fd9dfa29891431714d131d5d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Grythe=20St=C3=B8dle?= Date: Mon, 8 Jun 2020 00:12:22 +0200 Subject: [PATCH] Add support for external editor Adds support for editing commit messages in an external editor. It read the GIT_EDITOR, VISUAL, EDITOR environment variables in turn (in the same order git does natively) and tries to launch the specified editor. If no editor is found, it falls back to "vi" (same as git). If vi is not available, it will fail with a message. --- src/app.rs | 39 ++++++++++++++++++++++ src/components/changes.rs | 14 ++++++++ src/components/commit.rs | 69 +++++++++++++++++++++++++++++++++++++-- src/keys.rs | 2 ++ src/main.rs | 7 ++-- src/queue.rs | 2 ++ src/strings.rs | 10 ++++++ 7 files changed, 138 insertions(+), 5 deletions(-) diff --git a/src/app.rs b/src/app.rs index 3aac96f60e..c5ab562148 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,3 +1,4 @@ +use crate::input::InputState; use crate::{ accessors, cmdbar::CommandBar, @@ -18,6 +19,7 @@ use anyhow::{anyhow, Result}; use asyncgit::{sync, AsyncNotification, CWD}; use crossbeam_channel::Sender; use crossterm::event::{Event, KeyEvent}; +use std::cell::Cell; use std::{cell::RefCell, rc::Rc}; use strings::{commands, order}; use tui::{ @@ -28,6 +30,7 @@ use tui::{ widgets::{Block, Borders, Tabs}, Frame, }; + /// pub struct App { do_quit: bool, @@ -45,6 +48,10 @@ pub struct App { stashlist_tab: StashList, queue: Queue, theme: SharedTheme, + + // "Flags" + requires_redraw: Cell, + set_polling: bool, } // public interface @@ -85,6 +92,8 @@ impl App { stashlist_tab: StashList::new(&queue, theme.clone()), queue, theme, + requires_redraw: Cell::new(false), + set_polling: true, } } @@ -182,6 +191,17 @@ impl App { if flags.contains(NeedsUpdate::COMMANDS) { self.update_commands(); } + } else if let InputEvent::State(polling_state) = ev { + if let InputState::Paused = polling_state { + if let Err(e) = self.commit.show_editor() { + let msg = + format!("failed to launch editor:\n{}", e); + log::error!("{}", msg.as_str()); + self.msg.show_msg(msg.as_str())?; + } + self.requires_redraw.set(true); + self.set_polling = true; + } } Ok(()) @@ -230,6 +250,21 @@ impl App { || self.stashing_tab.anything_pending() || self.inspect_commit_popup.any_work_pending() } + + /// + pub fn requires_redraw(&self) -> bool { + if self.requires_redraw.get() { + self.requires_redraw.set(false); + true + } else { + false + } + } + + /// + pub const fn set_polling(&self) -> bool { + self.set_polling + } } // private impls @@ -314,6 +349,7 @@ impl App { fn process_queue(&mut self) -> Result { let mut flags = NeedsUpdate::empty(); + loop { let front = self.queue.borrow_mut().pop_front(); if let Some(e) = front { @@ -369,6 +405,9 @@ impl App { self.inspect_commit_popup.open(id)?; flags.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS) } + InternalEvent::SuspendPolling => { + self.set_polling = false; + } }; Ok(flags) diff --git a/src/components/changes.rs b/src/components/changes.rs index 6f7067c15f..45f87b5ccc 100644 --- a/src/components/changes.rs +++ b/src/components/changes.rs @@ -236,6 +236,11 @@ impl Component for ChangesComponent { some_selection, self.focused(), )); + out.push(CommandInfo::new( + commands::COMMIT_OPEN_EDITOR, + !self.is_empty(), + self.focused() || force_all, + )); out.push( CommandInfo::new( commands::COMMIT_OPEN, @@ -266,6 +271,15 @@ impl Component for ChangesComponent { .push_back(InternalEvent::OpenCommit); Ok(true) } + keys::OPEN_COMMIT_EDITOR + if !self.is_working_dir + && !self.is_empty() => + { + self.queue + .borrow_mut() + .push_back(InternalEvent::SuspendPolling); + Ok(true) + } keys::STATUS_STAGE_FILE => { try_or_popup!( self, diff --git a/src/components/commit.rs b/src/components/commit.rs index bfdae54ab0..ea1e610a78 100644 --- a/src/components/commit.rs +++ b/src/components/commit.rs @@ -2,18 +2,24 @@ use super::{ textinput::TextInputComponent, visibility_blocking, CommandBlocking, CommandInfo, Component, DrawableComponent, }; +use crate::strings::COMMIT_EDITOR_MSG; use crate::{ - keys, + get_app_config_path, keys, queue::{InternalEvent, NeedsUpdate, Queue}, strings, ui::style::SharedTheme, }; -use anyhow::Result; +use anyhow::{anyhow, Result}; use asyncgit::{ sync::{self, CommitId}, CWD, }; use crossterm::event::Event; +use std::env; +use std::fs::File; +use std::io::{Read, Write}; +use std::path::PathBuf; +use std::process::Command; use strings::commands; use sync::HookResult; use tui::{backend::Backend, layout::Rect, Frame}; @@ -121,8 +127,65 @@ impl CommitComponent { } } + pub fn show_editor(&mut self) -> Result<()> { + const COMMIT_MSG_FILE_NAME: &str = "COMMITMSG_EDITOR"; + let mut config_path: PathBuf = get_app_config_path()?; + config_path.push(COMMIT_MSG_FILE_NAME); + + let mut file = File::create(&config_path)?; + file.write_all(COMMIT_EDITOR_MSG.as_bytes())?; + drop(file); + + let mut editor = env::var("GIT_EDTIOR") + .ok() + .or_else(|| env::var("VISUAL").ok()) + .or_else(|| env::var("EDITOR").ok()) + .unwrap_or_else(|| String::from("vi")); + editor + .push_str(&format!(" {}", config_path.to_string_lossy())); + + let mut editor = editor.split_whitespace(); + + let command = editor.next().ok_or_else(|| { + anyhow!("unable to read editor command") + })?; + + Command::new(command) + .args(editor) + .status() + .map_err(|e| anyhow!("\"{}\": {}", command, e))?; + + let mut message = String::new(); + + let mut file = File::open(&config_path)?; + file.read_to_string(&mut message)?; + drop(file); + std::fs::remove_file(&config_path)?; + + let message: String = message + .lines() + .flat_map(|l| { + if l.starts_with('#') { + vec![] + } else { + vec![l, "\n"] + } + }) + .collect(); + + if !message.chars().all(char::is_whitespace) { + return self.commit_msg(message); + } + + Ok(()) + } + fn commit(&mut self) -> Result<()> { - let mut msg = self.input.get_text().clone(); + self.commit_msg(self.input.get_text().clone()) + } + + fn commit_msg(&mut self, msg: String) -> Result<()> { + let mut msg = msg; if let HookResult::NotOk(e) = sync::hooks_commit_msg(CWD, &mut msg)? { diff --git a/src/keys.rs b/src/keys.rs index 4cb6803bd0..8bd9883703 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -32,6 +32,8 @@ pub const EXIT: KeyEvent = pub const EXIT_POPUP: KeyEvent = no_mod(KeyCode::Esc); pub const CLOSE_MSG: KeyEvent = no_mod(KeyCode::Enter); pub const OPEN_COMMIT: KeyEvent = no_mod(KeyCode::Char('c')); +pub const OPEN_COMMIT_EDITOR: KeyEvent = + with_mod(KeyCode::Char('C'), KeyModifiers::SHIFT); pub const OPEN_HELP: KeyEvent = no_mod(KeyCode::Char('h')); pub const MOVE_LEFT: KeyEvent = no_mod(KeyCode::Left); pub const MOVE_RIGHT: KeyEvent = no_mod(KeyCode::Right); diff --git a/src/main.rs b/src/main.rs index e88c03e8d0..af215ecb1f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -127,8 +127,7 @@ fn main() -> Result<()> { } } - //TODO: disable input polling while external editor open - input.set_polling(!app.any_work_pending()); + input.set_polling(app.set_polling()); if needs_draw { draw(&mut terminal, &app)?; @@ -164,6 +163,10 @@ fn draw( terminal: &mut Terminal, app: &App, ) -> io::Result<()> { + if app.requires_redraw() { + terminal.resize(terminal.size()?)?; + } + terminal.draw(|mut f| { if let Err(e) = app.draw(&mut f) { log::error!("failed to draw: {:?}", e) diff --git a/src/queue.rs b/src/queue.rs index f682d08073..eea4aa3913 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -48,6 +48,8 @@ pub enum InternalEvent { TabSwitch, /// InspectCommit(CommitId), + /// + SuspendPolling, } /// diff --git a/src/strings.rs b/src/strings.rs index 5cd7e9300b..eb1070aadc 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -14,6 +14,10 @@ pub static MSG_TITLE_ERROR: &str = "Error"; pub static COMMIT_TITLE: &str = "Commit"; pub static COMMIT_TITLE_AMEND: &str = "Commit (Amend)"; pub static COMMIT_MSG: &str = "type commit message.."; +pub static COMMIT_EDITOR_MSG: &str = r##" +# Enter your commit message +# Lines starting with '#' will be ignored +# Empty commit message will abort the commit"##; pub static STASH_POPUP_TITLE: &str = "Stash"; pub static STASH_POPUP_MSG: &str = "type name (optional)"; pub static CONFIRM_TITLE_RESET: &str = "Reset"; @@ -150,6 +154,12 @@ pub mod commands { CMD_GROUP_COMMIT, ); /// + pub static COMMIT_OPEN_EDITOR: CommandText = CommandText::new( + "Commit editor [C]", + "open commit editor (available in non-empty stage)", + CMD_GROUP_COMMIT, + ); + /// pub static COMMIT_ENTER: CommandText = CommandText::new( "Commit [enter]", "commit (available when commit message is non-empty)",