Skip to content

Commit 50b0222

Browse files
committed
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.
1 parent 8627d94 commit 50b0222

File tree

7 files changed

+138
-5
lines changed

7 files changed

+138
-5
lines changed

Diff for: src/app.rs

+39
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::input::InputState;
12
use crate::{
23
accessors,
34
cmdbar::CommandBar,
@@ -18,6 +19,7 @@ use anyhow::{anyhow, Result};
1819
use asyncgit::{sync, AsyncNotification, CWD};
1920
use crossbeam_channel::Sender;
2021
use crossterm::event::{Event, KeyEvent};
22+
use std::cell::Cell;
2123
use std::{cell::RefCell, rc::Rc};
2224
use strings::{commands, order};
2325
use tui::{
@@ -28,6 +30,7 @@ use tui::{
2830
widgets::{Block, Borders, Tabs},
2931
Frame,
3032
};
33+
3134
///
3235
pub struct App {
3336
do_quit: bool,
@@ -45,6 +48,10 @@ pub struct App {
4548
stashlist_tab: StashList,
4649
queue: Queue,
4750
theme: SharedTheme,
51+
52+
// "Flags"
53+
requires_redraw: Cell<bool>,
54+
set_polling: bool,
4855
}
4956

5057
// public interface
@@ -85,6 +92,8 @@ impl App {
8592
stashlist_tab: StashList::new(&queue, theme.clone()),
8693
queue,
8794
theme,
95+
requires_redraw: Cell::new(false),
96+
set_polling: true,
8897
}
8998
}
9099

@@ -182,6 +191,17 @@ impl App {
182191
if flags.contains(NeedsUpdate::COMMANDS) {
183192
self.update_commands();
184193
}
194+
} else if let InputEvent::State(polling_state) = ev {
195+
if let InputState::Paused = polling_state {
196+
if let Err(e) = self.commit.show_editor() {
197+
let msg =
198+
format!("failed to launch editor:\n{}", e);
199+
log::error!("{}", msg.as_str());
200+
self.msg.show_msg(msg.as_str())?;
201+
}
202+
self.requires_redraw.set(true);
203+
self.set_polling = true;
204+
}
185205
}
186206

187207
Ok(())
@@ -230,6 +250,21 @@ impl App {
230250
|| self.stashing_tab.anything_pending()
231251
|| self.inspect_commit_popup.any_work_pending()
232252
}
253+
254+
///
255+
pub fn requires_redraw(&self) -> bool {
256+
if self.requires_redraw.get() {
257+
self.requires_redraw.set(false);
258+
true
259+
} else {
260+
false
261+
}
262+
}
263+
264+
///
265+
pub const fn set_polling(&self) -> bool {
266+
self.set_polling
267+
}
233268
}
234269

235270
// private impls
@@ -314,6 +349,7 @@ impl App {
314349

315350
fn process_queue(&mut self) -> Result<NeedsUpdate> {
316351
let mut flags = NeedsUpdate::empty();
352+
317353
loop {
318354
let front = self.queue.borrow_mut().pop_front();
319355
if let Some(e) = front {
@@ -369,6 +405,9 @@ impl App {
369405
self.inspect_commit_popup.open(id)?;
370406
flags.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS)
371407
}
408+
InternalEvent::SuspendPolling => {
409+
self.set_polling = false;
410+
}
372411
};
373412

374413
Ok(flags)

Diff for: src/components/changes.rs

+14
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,11 @@ impl Component for ChangesComponent {
236236
some_selection,
237237
self.focused(),
238238
));
239+
out.push(CommandInfo::new(
240+
commands::COMMIT_OPEN_EDITOR,
241+
!self.is_empty(),
242+
self.focused() || force_all,
243+
));
239244
out.push(
240245
CommandInfo::new(
241246
commands::COMMIT_OPEN,
@@ -266,6 +271,15 @@ impl Component for ChangesComponent {
266271
.push_back(InternalEvent::OpenCommit);
267272
Ok(true)
268273
}
274+
keys::OPEN_COMMIT_EDITOR
275+
if !self.is_working_dir
276+
&& !self.is_empty() =>
277+
{
278+
self.queue
279+
.borrow_mut()
280+
.push_back(InternalEvent::SuspendPolling);
281+
Ok(true)
282+
}
269283
keys::STATUS_STAGE_FILE => {
270284
try_or_popup!(
271285
self,

Diff for: src/components/commit.rs

+66-3
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,24 @@ use super::{
22
textinput::TextInputComponent, visibility_blocking,
33
CommandBlocking, CommandInfo, Component, DrawableComponent,
44
};
5+
use crate::strings::COMMIT_EDITOR_MSG;
56
use crate::{
6-
keys,
7+
get_app_config_path, keys,
78
queue::{InternalEvent, NeedsUpdate, Queue},
89
strings,
910
ui::style::SharedTheme,
1011
};
11-
use anyhow::Result;
12+
use anyhow::{anyhow, Result};
1213
use asyncgit::{
1314
sync::{self, CommitId},
1415
CWD,
1516
};
1617
use crossterm::event::Event;
18+
use std::env;
19+
use std::fs::File;
20+
use std::io::{Read, Write};
21+
use std::path::PathBuf;
22+
use std::process::Command;
1723
use strings::commands;
1824
use sync::HookResult;
1925
use tui::{backend::Backend, layout::Rect, Frame};
@@ -121,8 +127,65 @@ impl CommitComponent {
121127
}
122128
}
123129

130+
pub fn show_editor(&mut self) -> Result<()> {
131+
const COMMIT_MSG_FILE_NAME: &str = "COMMITMSG_EDITOR";
132+
let mut config_path: PathBuf = get_app_config_path()?;
133+
config_path.push(COMMIT_MSG_FILE_NAME);
134+
135+
let mut file = File::create(&config_path)?;
136+
file.write_all(COMMIT_EDITOR_MSG.as_bytes())?;
137+
drop(file);
138+
139+
let mut editor = env::var("GIT_EDTIOR")
140+
.ok()
141+
.or_else(|| env::var("VISUAL").ok())
142+
.or_else(|| env::var("EDITOR").ok())
143+
.unwrap_or_else(|| String::from("vi"));
144+
editor
145+
.push_str(&format!(" {}", config_path.to_string_lossy()));
146+
147+
let mut editor = editor.split_whitespace();
148+
149+
let command = editor.next().ok_or_else(|| {
150+
anyhow!("unable to read editor command")
151+
})?;
152+
153+
Command::new(command)
154+
.args(editor)
155+
.status()
156+
.map_err(|e| anyhow!("\"{}\": {}", command, e))?;
157+
158+
let mut message = String::new();
159+
160+
let mut file = File::open(&config_path)?;
161+
file.read_to_string(&mut message)?;
162+
drop(file);
163+
std::fs::remove_file(&config_path)?;
164+
165+
let message: String = message
166+
.lines()
167+
.flat_map(|l| {
168+
if l.starts_with('#') {
169+
vec![]
170+
} else {
171+
vec![l, "\n"]
172+
}
173+
})
174+
.collect();
175+
176+
if !message.chars().all(char::is_whitespace) {
177+
return self.commit_msg(message);
178+
}
179+
180+
Ok(())
181+
}
182+
124183
fn commit(&mut self) -> Result<()> {
125-
let mut msg = self.input.get_text().clone();
184+
self.commit_msg(self.input.get_text().clone())
185+
}
186+
187+
fn commit_msg(&mut self, msg: String) -> Result<()> {
188+
let mut msg = msg;
126189
if let HookResult::NotOk(e) =
127190
sync::hooks_commit_msg(CWD, &mut msg)?
128191
{

Diff for: src/keys.rs

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ pub const EXIT: KeyEvent =
3232
pub const EXIT_POPUP: KeyEvent = no_mod(KeyCode::Esc);
3333
pub const CLOSE_MSG: KeyEvent = no_mod(KeyCode::Enter);
3434
pub const OPEN_COMMIT: KeyEvent = no_mod(KeyCode::Char('c'));
35+
pub const OPEN_COMMIT_EDITOR: KeyEvent =
36+
with_mod(KeyCode::Char('C'), KeyModifiers::SHIFT);
3537
pub const OPEN_HELP: KeyEvent = no_mod(KeyCode::Char('h'));
3638
pub const MOVE_LEFT: KeyEvent = no_mod(KeyCode::Left);
3739
pub const MOVE_RIGHT: KeyEvent = no_mod(KeyCode::Right);

Diff for: src/main.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,7 @@ fn main() -> Result<()> {
127127
}
128128
}
129129

130-
//TODO: disable input polling while external editor open
131-
input.set_polling(!app.any_work_pending());
130+
input.set_polling(app.set_polling());
132131

133132
if needs_draw {
134133
draw(&mut terminal, &app)?;
@@ -164,6 +163,10 @@ fn draw<B: Backend>(
164163
terminal: &mut Terminal<B>,
165164
app: &App,
166165
) -> io::Result<()> {
166+
if app.requires_redraw() {
167+
terminal.resize(terminal.size()?)?;
168+
}
169+
167170
terminal.draw(|mut f| {
168171
if let Err(e) = app.draw(&mut f) {
169172
log::error!("failed to draw: {:?}", e)

Diff for: src/queue.rs

+2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ pub enum InternalEvent {
4848
TabSwitch,
4949
///
5050
InspectCommit(CommitId),
51+
///
52+
SuspendPolling,
5153
}
5254

5355
///

Diff for: src/strings.rs

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ pub static MSG_TITLE_ERROR: &str = "Error";
1414
pub static COMMIT_TITLE: &str = "Commit";
1515
pub static COMMIT_TITLE_AMEND: &str = "Commit (Amend)";
1616
pub static COMMIT_MSG: &str = "type commit message..";
17+
pub static COMMIT_EDITOR_MSG: &str = r##"
18+
# Enter your commit message
19+
# Lines starting with '#' will be ignored
20+
# Empty commit message will abort the commit"##;
1721
pub static STASH_POPUP_TITLE: &str = "Stash";
1822
pub static STASH_POPUP_MSG: &str = "type name (optional)";
1923
pub static CONFIRM_TITLE_RESET: &str = "Reset";
@@ -150,6 +154,12 @@ pub mod commands {
150154
CMD_GROUP_COMMIT,
151155
);
152156
///
157+
pub static COMMIT_OPEN_EDITOR: CommandText = CommandText::new(
158+
"Commit editor [C]",
159+
"open commit editor (available in non-empty stage)",
160+
CMD_GROUP_COMMIT,
161+
);
162+
///
153163
pub static COMMIT_ENTER: CommandText = CommandText::new(
154164
"Commit [enter]",
155165
"commit (available when commit message is non-empty)",

0 commit comments

Comments
 (0)