feat: allow more customizability for inline blame

This commit is contained in:
Nik Revenco 2025-03-19 17:22:21 +00:00
parent 8b559da256
commit 548899d7b1
2 changed files with 104 additions and 24 deletions

View file

@ -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

View file

@ -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<String>,
pub author_name: Option<String>,
pub author_email: Option<String>,
pub commit_date: Option<String>,
pub commit_message: Option<String>,
pub commit_body: Option<String>,
}
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())),
})
}