girt-core 2.2.1

Core modules for git-interactive-rebase-tool
Documentation
mod action;
mod argument_tokenizer;
mod external_editor_state;

#[cfg(all(unix, test))]
mod tests;

use anyhow::{anyhow, Result};
use input::InputOptions;
use lazy_static::lazy_static;
use todo_file::{Line, TodoFile};
use view::{RenderContext, ViewData, ViewLine, ViewSender};

use self::{action::Action, argument_tokenizer::tokenize, external_editor_state::ExternalEditorState};
use crate::{
	components::choice::{Choice, INPUT_OPTIONS as CHOICE_INPUT_OPTIONS},
	events::{Event, MetaEvent},
	module::{ExitStatus, Module, ProcessResult, State},
};

lazy_static! {
	static ref INPUT_OPTIONS: InputOptions = InputOptions::RESIZE;
}

pub(crate) struct ExternalEditor {
	editor: String,
	empty_choice: Choice<Action>,
	error_choice: Choice<Action>,
	external_command: (String, Vec<String>),
	lines: Vec<Line>,
	state: ExternalEditorState,
	view_data: ViewData,
}

impl Module for ExternalEditor {
	fn activate(&mut self, todo_file: &TodoFile, _: State) -> ProcessResult {
		let result = ProcessResult::new();
		if let Err(err) = todo_file.write_file() {
			return result.error(err).state(State::List);
		}

		if self.lines.is_empty() {
			self.lines = todo_file.get_lines_owned();
		}

		match self.get_command(todo_file) {
			Ok(external_command) => self.external_command = external_command,
			Err(err) => return result.error(err).state(State::List),
		}
		self.set_state(result, ExternalEditorState::Active)
	}

	fn deactivate(&mut self) {
		self.lines.clear();
		self.view_data.update_view_data(|updater| updater.clear());
	}

	fn build_view_data(&mut self, _: &RenderContext, _: &TodoFile) -> &ViewData {
		match self.state {
			ExternalEditorState::Active => {
				self.view_data.update_view_data(|updater| {
					updater.clear();
					updater.push_leading_line(ViewLine::from("Editing..."));
				});
				&self.view_data
			},
			ExternalEditorState::Empty => self.empty_choice.get_view_data(),
			ExternalEditorState::Error(ref error) => {
				self.error_choice
					.set_prompt(error.chain().map(|c| ViewLine::from(format!("{:#}", c))).collect());
				self.error_choice.get_view_data()
			},
		}
	}

	fn input_options(&self) -> &InputOptions {
		match self.state {
			ExternalEditorState::Active => &INPUT_OPTIONS,
			ExternalEditorState::Empty | ExternalEditorState::Error(_) => &CHOICE_INPUT_OPTIONS,
		}
	}

	fn handle_event(&mut self, event: Event, view_sender: &ViewSender, todo_file: &mut TodoFile) -> ProcessResult {
		let mut result = ProcessResult::new();
		match self.state {
			ExternalEditorState::Active => {
				result = result.event(event);
				match event {
					Event::MetaEvent(MetaEvent::ExternalCommandSuccess) => {
						match todo_file.load_file() {
							Ok(_) => {
								if todo_file.is_empty() || todo_file.is_noop() {
									result = self.set_state(result, ExternalEditorState::Empty);
								}
								else {
									result = result.state(State::List);
								}
							},
							Err(e) => result = self.set_state(result, ExternalEditorState::Error(e)),
						}
					},
					Event::MetaEvent(MetaEvent::ExternalCommandError) => {
						result = self.set_state(
							result,
							ExternalEditorState::Error(anyhow!("Editor returned a non-zero exit status")),
						);
					},
					_ => {},
				}
			},
			ExternalEditorState::Empty => {
				let choice = self.empty_choice.handle_event(event, view_sender);
				result = result.event(event);
				if let Some(action) = choice {
					match *action {
						Action::AbortRebase => result = result.exit_status(ExitStatus::Good),
						Action::EditRebase => result = self.set_state(result, ExternalEditorState::Active),
						Action::UndoAndEdit => {
							todo_file.set_lines(self.lines.clone());
							result = self.undo_and_edit(result, todo_file);
						},
						Action::RestoreAndAbortEdit => {},
					}
				}
			},
			ExternalEditorState::Error(_) => {
				let choice = self.error_choice.handle_event(event, view_sender);
				result = result.event(event);
				if let Some(action) = choice {
					match *action {
						Action::AbortRebase => {
							todo_file.set_lines(vec![]);
							result = result.exit_status(ExitStatus::Good);
						},
						Action::EditRebase => result = self.set_state(result, ExternalEditorState::Active),
						Action::RestoreAndAbortEdit => {
							todo_file.set_lines(self.lines.clone());
							result = result.state(State::List);
							if let Err(err) = todo_file.write_file() {
								result = result.error(err);
							}
						},
						Action::UndoAndEdit => {
							todo_file.set_lines(self.lines.clone());
							result = self.undo_and_edit(result, todo_file);
						},
					}
				}
			},
		}
		result
	}
}

impl ExternalEditor {
	pub(crate) fn new(editor: &str) -> Self {
		let view_data = ViewData::new(|updater| {
			updater.set_show_title(true);
		});

		let mut empty_choice = Choice::new(vec![
			(Action::AbortRebase, '1', String::from("Abort rebase")),
			(Action::EditRebase, '2', String::from("Edit rebase file")),
			(
				Action::UndoAndEdit,
				'3',
				String::from("Undo modifications and edit rebase file"),
			),
		]);
		empty_choice.set_prompt(vec![ViewLine::from("The rebase file is empty.")]);

		let error_choice = Choice::new(vec![
			(Action::AbortRebase, '1', String::from("Abort rebase")),
			(Action::EditRebase, '2', String::from("Edit rebase file")),
			(
				Action::RestoreAndAbortEdit,
				'3',
				String::from("Restore rebase file and abort edit"),
			),
			(
				Action::UndoAndEdit,
				'4',
				String::from("Undo modifications and edit rebase file"),
			),
		]);

		Self {
			editor: String::from(editor),
			empty_choice,
			error_choice,
			external_command: (String::from(""), vec![]),
			lines: vec![],
			state: ExternalEditorState::Active,
			view_data,
		}
	}

	fn set_state(&mut self, result: ProcessResult, new_state: ExternalEditorState) -> ProcessResult {
		self.state = new_state;
		match self.state {
			ExternalEditorState::Active => {
				result.external_command(self.external_command.0.clone(), self.external_command.1.clone())
			},
			ExternalEditorState::Empty | ExternalEditorState::Error(_) => result,
		}
	}

	fn undo_and_edit(&mut self, result: ProcessResult, todo_file: &mut TodoFile) -> ProcessResult {
		todo_file.set_lines(self.lines.clone());
		if let Err(err) = todo_file.write_file() {
			return result.error(err).state(State::List);
		}
		self.set_state(result, ExternalEditorState::Active)
	}

	fn get_command(&mut self, todo_file: &TodoFile) -> Result<(String, Vec<String>)> {
		let mut parameters = tokenize(self.editor.as_str())
			.map_or(Err(anyhow!("Invalid editor: \"{}\"", self.editor)), |args| {
				if args.is_empty() {
					Err(anyhow!("No editor configured"))
				}
				else {
					Ok(args.into_iter())
				}
			})
			.map_err(|e| anyhow!("Please see the git \"core.editor\" configuration for details").context(e))?;

		let filepath = todo_file.get_filepath();
		let mut file_pattern_found = false;
		let command = parameters.next().unwrap_or_else(|| String::from("false"));
		let mut arguments = parameters
			.map(|a| {
				if a.as_str() == "%" {
					file_pattern_found = true;
					String::from(filepath)
				}
				else {
					a
				}
			})
			.collect::<Vec<String>>();
		if !file_pattern_found {
			arguments.push(String::from(filepath));
		}
		Ok((command, arguments))
	}
}