diff --git a/book/src/editor.md b/book/src/editor.md index 393a7b76..8c38efbf 100644 --- a/book/src/editor.md +++ b/book/src/editor.md @@ -164,7 +164,32 @@ The following statusline elements can be configured: | Key | Description | Default | | ------- | ------------------------------------------ | ------- | -| `blame` | Show git blame output for the current line | `false` | +| `inline-blame` | Show git blame output for the current line | `false` | +| `inline-blame-format` | The format in which to show the inline blame | `"{author}, {date} • {message} • {commit}"` | + +For `inline-blame-format`, you can use specific variables like so: `{variable}`. + +These are the available variables: + +- `author`: The author of the commit +- `date`: When the commit was made +- `message`: The message of the commit, excluding the body +- `body`: The body of the commit +- `commit`: The short hex SHA1 hash of the commit +- `email`: The email of the author of the commit + +Any of the variables can potentially be empty. +In this case, the content before the variable will not be included in the string. +If the variable is at the beginning of the string, the content after the variable will not be included. + +Some examples, using the default value `inline-blame-format` value: + +- If `author` is empty: `"{date} • {message} • {commit}"` +- If `date` is empty: `"{author} • {message} • {commit}"` +- If `message` is empty: `"{author}, {date} • {commit}"` +- If `commit` is empty: `"{author}, {date} • {message}"` +- If `date` and `message` is empty: `"{author} • {commit}"` +- If `author` and `message` is empty: `"{date} • {commit}"` ### `[editor.cursor-shape]` Section diff --git a/helix-vcs/src/git/blame.rs b/helix-vcs/src/git/blame.rs index 7debae99..3e02c3cb 100644 --- a/helix-vcs/src/git/blame.rs +++ b/helix-vcs/src/git/blame.rs @@ -5,17 +5,19 @@ use std::{collections::HashMap, ops::Range, path::Path}; use super::{get_repo_dir, open_repo}; pub struct BlameInformation { - pub commit_hash: String, - pub author_name: String, - pub author_email: String, - pub commit_date: String, - pub commit_message: String, + pub commit_hash: Option, + pub author_name: Option, + pub author_email: Option, + pub commit_date: Option, + pub commit_message: Option, + pub commit_body: Option, } impl BlameInformation { /// Parse the user's blame format pub fn parse_format(&self, format: &str) -> String { - let mut formatted = String::with_capacity(format.len() * 2); + let mut formatted = String::new(); + let mut content_before_variable = String::new(); let variables = HashMap::from([ ("commit", &self.commit_hash), @@ -23,17 +25,21 @@ impl BlameInformation { ("date", &self.commit_date), ("message", &self.commit_message), ("email", &self.author_email), + ("body", &self.commit_body), ]); let mut chars = format.chars().peekable(); + // in all cases, when any of the variables is empty we exclude the content before the variable + // However, if the variable is the first and it is empty - then exclude the content after the variable + let mut exclude_content_after_variable = false; while let Some(ch) = chars.next() { // "{{" => '{' if ch == '{' && chars.next_if_eq(&'{').is_some() { - formatted.push('{'); + content_before_variable.push('{'); } // "}}" => '}' else if ch == '}' && chars.next_if_eq(&'}').is_some() { - formatted.push('}'); + content_before_variable.push('}'); } else if ch == '{' { let mut variable = String::new(); // eat all characters until the end @@ -42,10 +48,16 @@ impl BlameInformation { } // eat the '}' if it was found let has_closing = chars.next().is_some(); - let res = variables - .get(variable.as_str()) - .map(|s| s.to_string()) - .unwrap_or_else(|| { + + #[derive(PartialEq, Eq, PartialOrd, Ord)] + enum Variable { + Valid(String), + Invalid(String), + Empty, + } + + let variable_value = variables.get(variable.as_str()).map_or_else( + || { // Invalid variable. So just add whatever we parsed before let mut result = String::with_capacity(variable.len() + 2); result.push('{'); @@ -53,12 +65,49 @@ impl BlameInformation { if has_closing { result.push('}'); } - result - }); + Variable::Invalid(result) + }, + |s| { + s.as_ref() + .map(|s| Variable::Valid(s.to_string())) + .unwrap_or(Variable::Empty) + }, + ); - formatted.push_str(&res); + match variable_value { + Variable::Valid(value) => { + if exclude_content_after_variable { + // don't push anything. + exclude_content_after_variable = false; + } else { + formatted.push_str(&content_before_variable); + } + formatted.push_str(&value); + } + Variable::Invalid(value) => { + if exclude_content_after_variable { + // don't push anything. + exclude_content_after_variable = false; + } else { + formatted.push_str(&content_before_variable); + } + formatted.push_str(&value); + } + Variable::Empty => { + if formatted.is_empty() { + // exclude content AFTER this variable (at next iteration of the loop, + // we'll exclude the content before a valid variable) + exclude_content_after_variable = true; + } else { + // exclude content BEFORE this variable + // also just don't add anything. + } + } + } + + content_before_variable.drain(..); } else { - formatted.push(ch); + content_before_variable.push(ch); } } @@ -125,14 +174,20 @@ pub fn blame( .context("No commits found")? .commit_id; - let commit = repo.find_commit(latest_commit_id)?; - let author = commit.author()?; + let commit = repo.find_commit(latest_commit_id).ok(); + let message = commit.as_ref().and_then(|c| c.message().ok()); + let author = commit.as_ref().and_then(|c| c.author().ok()); Ok(BlameInformation { - commit_hash: commit.short_id()?.to_string(), - author_name: author.name.to_string(), - author_email: author.email.to_string(), - commit_date: author.time.format(gix::date::time::format::SHORT), - commit_message: commit.message()?.title.to_string(), + commit_hash: commit + .as_ref() + .and_then(|c| c.short_id().map(|id| id.to_string()).ok()), + author_name: author.map(|a| a.name.to_string()), + author_email: author.map(|a| a.email.to_string()), + commit_date: author.map(|a| a.time.format(gix::date::time::format::SHORT)), + commit_message: message.as_ref().map(|msg| msg.title.to_string()), + commit_body: message + .as_ref() + .and_then(|msg| msg.body.map(|body| body.to_string())), }) }