-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Inline Git Blame #13133
base: master
Are you sure you want to change the base?
feat: Inline Git Blame #13133
Conversation
that's really neat! good job :3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome work, I've been wanting this for a long time, and tried looking into it two weeks ago, but my rust skills just weren't up for it!
I wonder if opening the repo is the slow step here, which could mean that the following isn't necessary, see my comment below.
Could it would make sense to get the blame for the whole file at once instead of doing it on-demand for every line? That would be much faster, as you could just look up the stored info of what the blame is for a given line. But then we would ideally need to keep count of added/deleted lines in order to "keep count" when querying for the currently selected line.
The current behavior of this PR is basically that though - if I type something on a new line, the blame mentions the previous author, not me/"Not comitted yet" (not even after save, which is interesting, but beside my point) and the count is off, I think.
I notice that the blame info is computed on moving left and right. That isn't necessary.
Stuff like not updating when moving left and right, or get the diff for the files are definitely optimizations we can look into But the biggest one is figuring out how to compute the blame off the main thread. Even if you compute it just once per file, it can take 10 seconds for that to happen and the UI will be frozen in that time which isn't acceptable |
Yeah this shouldn't be difficult to fix, once I figure out how to stop the freezes I'll also implement this |
Hey this looks great thank you ! Would it be possible to map it on a key press to show ? We'd avoid performing the computation every time the cursor change line, and it would reduced the visual pollution |
Yes, that is possible to do however the UI would still freeze for e.g. 10 seconds which isn't really good edit: this message doesn't apply anymore, the problem was fixed. I implemented the keybinding to get the git blame for the current line |
YES! I finally figured it out. Now we're not doing the git blame on the main thread anymore :) The solution is basically that we have a worker and we check it every 0.05 seconds if its done. If not, then try again later. If it is, then get its result. The reason why this works now is that I am not doing any computation while having a |
I think having a default keybinding to trigger this once is a good idea. Will add that to the TODO edit: implemented |
54a0c02
to
f830c76
Compare
Update:
Customizing the inline git blame
|
Key | Description | Default |
---|---|---|
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 commitdate
: When the commit was mademessage
: The message of the commit, excluding the bodybody
: The body of the commitcommit
: The short hex SHA1 hash of the commitemail
: 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
andmessage
is empty:"{author} • {commit}"
- If
author
andmessage
is empty:"{date} • {commit}"
Tests
side note: For the tests, probably wrote my most complicated declarative macro ever, but at least we can have easily readable test syntax for git blame:
Test Syntax for Git Blame
#[test]
pub fn blamed_lines() {
assert_line_blame_progress! {
1 =>
"fn main() {" 1,
"" 1,
"}" 1;
// modifying a line works
2 =>
"fn main() {" 1,
" one" 2,
"}" 1;
// inserting a line works
3 =>
"fn main() {" 1,
" one" 2,
" two" 3,
"}" 1;
// deleting a line works
4 =>
"fn main() {" 1,
" two" 3,
"}" 1;
// when a line is inserted in-between the blame order is preserved
5 no_commit =>
"fn main() {" 1,
" hello world" insert,
" two" 3,
"}" 1;
// Having a bunch of random lines interspersed should not change which lines
// have blame for which commits
6 no_commit =>
" six" insert,
" three" insert,
"fn main() {" 1,
" five" insert,
" four" insert,
" two" 3,
" five" insert,
" four" insert,
"}" 1,
" five" insert,
" four" insert;
};
}
Would it be possible to open the git blame into a scratch buffer with git diff language style applied? |
it should be doable, but it would be out of scope for this PR. |
I think the following could be desirable, but don't worry if you feel it is out of scope:
|
for 5., there is That being said, these items would be good to add but I don't want to put everything into 1 PR as that just means it could take a very long time to get reviewed. I would rather just have the core functionality for now and then make a new PR to add the niceties |
I got this panic when using the latest commit, on doing
|
Ah yes, I just fixed it |
I only see the commits and comments, but even if it doesn't affect the main thread, the performance is still a critical point, helix should stay light. Is a update every 150milliseconds necessary ? |
Update: We don't do updates every N seconds anymore Git blame for every line whenever you move is expensive especially on a large repo, so it's guaranteed Helix will consume more resources I'll try my best to improve performance. I hope to at the very least achieve the performance that Zed has for its inline git blame implementation. It should be doable because from what I can tell Zed spawns a process for So far, I've done some experimentation on the Helix repo itself which has around 6,000 commits. Specifically these are the steps that run in the background every time we request the git blame
Originally I was going to focus on caching the
|
Ok, I just fixed it 👁️ 👁️ impl Decoration for InlineBlame {
fn render_virt_lines(
&mut self,
renderer: &mut TextRenderer,
pos: LinePos,
virt_off: Position,
) -> Position {
let blame = match &self.lines {
LineBlame::OneLine((line, blame)) => {
--- if line != &pos.doc_line {
+++ if line == &pos.doc_line {
// do not draw inline blame for lines that have no content in them
blame |
Previously, we rendereded the inline blame for lines in 3X the range of the viewport This is not necessary because when we scroll down, the rendering will occur before we see the new content
Co-authored-by: Sebastian Klähn <[email protected]>
I have been experiencing an issue where I would be typing and then helix would freeze up. The final log saved was:
After changing the config for inline blame from this: [editor.inline-blame]
behaviour = "cursor-line"
compute = "background" to this: [editor.inline-blame]
behaviour = "hidden"
compute = "on-demand" I seem to have stopped getting the issue (I was getting it every time editing some documentation, about 10 times in a row doing the same thing, and the moment I changed these settings, I didn't get it anymore, at least so far ). Has anyone else experienced this using this branch? |
I have been experiencing this as well, I have to close my terminal and restart Helix afterwards, it's completely unresponsive. I assumed it was an upstream bug from master |
the only place where we ask the diff for anything is in /// Get the line blame for this view
pub fn line_blame(&self, cursor_line: u32, format: &str) -> Result<String, LineBlameError> {
// how many lines were inserted and deleted before the cursor line
let (inserted_lines, deleted_lines) = self
.diff_handle()
.map_or(
// in theory there can be situations where we don't have the diff for a file
// but we have the blame. In this case, we can just act like there is no diff
Some((0, 0)),
|diff_handle| {
// Compute the amount of lines inserted and deleted before the `line`
// This information is needed to accurately transform the state of the
// file in the file system into what gix::blame knows about (gix::blame only
// knows about commit history, it does not know about uncommitted changes)
diff_handle
.load()
.hunks_intersecting_line_ranges(std::iter::once((0, cursor_line as usize)))
.try_fold(
(0, 0),
|(total_inserted_lines, total_deleted_lines), hunk| {
// check if the line intersects the hunk's `after` (which represents
// inserted lines)
(hunk.after.start > cursor_line || hunk.after.end <= cursor_line)
.then_some((
total_inserted_lines + (hunk.after.end - hunk.after.start),
total_deleted_lines + (hunk.before.end - hunk.before.start),
))
},
)
},
)
.ok_or(LineBlameError::NotCommittedYet)?; the last INFO message leads me to believe that it's due to something being off which calls if you can reproduce this consistently you could try adding |
not sure if this will work as I can't reproduce this but let's see!
I have published a fix which might work in c74fec4 Try it out and if you don't experience this again then it means it works! |
Checking in to say that I haven't experience the issue yet with the fix. Not sure how others have faired. |
Same here, sorry for not checking in sooner, everything's been smooth sailing! |
So I tried my hand at making a more performant let value = String::from("16");
let unit = "days";
let label = "ago";
print!("{}", stdx::format!("{} {} {}", value, unit, label)); I benchmarked against the normal stdx::format time: [58.652 ns 58.714 ns 58.797 ns]
Found 7 outliers among 100 measurements (7.00%)
3 (3.00%) high mild
4 (4.00%) high severe
nik::concat time: [43.223 ns 43.261 ns 43.298 ns]
Found 5 outliers among 100 measurements (5.00%)
1 (1.00%) low mild
3 (3.00%) high mild
1 (1.00%) high severe
std::format time: [124.08 ns 124.13 ns 124.18 ns]
Found 12 outliers among 100 measurements (12.00%)
9 (9.00%) high mild
3 (3.00%) high severe So slightly slower than the It basically functions as a I could maybe shave on some more time, but going to be hard to beat just pushing str's in |
Nice, that's pretty good! I feel like using a custom whilst with |
This PR adds Inline Git Blame.
While moving around in the editor, virtual text will appear to the right showing the latest commit information for this line. Showcase:
space + B
to show blame for the current line in the status line. Useful if you don't haveinline-blame.enable
and just want to blame a single lineCloses #3035
Documentation
[editor.inline-blame]
SectionInline blame is virtual text that appears at the end of a line, displaying information about the most recent commit that affected this line.
behaviour
"hidden"
compute
"on-demand"
format
"{author}, {time-ago} • {message} • {commit}"
The
behaviour
can be one of the following:"all-lines"
: Inline blame is on every line."cursor-line"
: Inline blame is only on the line of the primary cursor."hidden"
: Inline blame is not shown.Inline blame will only show if the blame for the file has already been computed.
The
compute
key determines under which circumstances the blame is computed, and can be one of the following:"on-demand"
: Blame for the file is computed only when explicitly requested, such as when usingspace + B
to blame the line of the cursor. There may be a little delay when loading the blame. When opening new files, even withbehaviour
not set to"hidden"
, the inline blame won't show. It needs to be computed first in order to become available. This computation can be manually triggered by requesting it withspace + B
."background"
: Blame for the file is loaded in the background. This will have zero effect on performance of the Editor, but will use a little bit extra resources. Directly requesting the blame withspace + B
will be instant. Inline blame will show as soon as the blame is available when loading new files.inline-blame-format
allows customization of the blame message, and can be set to any string. Variables can be used like so:{variable}
. These are the available variables:author
: The author of the commitdate
: When the commit was madetime-ago
: How long ago the commit was mademessage
: The message of the commit, excluding the bodybody
: The body of the commitcommit
: The short hex SHA1 hash of the commitemail
: The email of the author of the commitAny 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
format
value:author
is empty:"{time-ago} • {message} • {commit}"
time-ago
is empty:"{author} • {message} • {commit}"
message
is empty:"{author}, {time-ago} • {commit}"
commit
is empty:"{author}, {time-ago} • {message}"
time-ago
andmessage
is empty:"{author} • {commit}"
author
andmessage
is empty:"{time-ago} • {commit}"
Notes for the reviewer
helix-term/src/handlers/blame.rs
: I created a newBlameHandler
that requests that a document'sblame
field is updated. This field has the information on which line numbers represent a specific commithelix-term/src/ui/text_decrations/blame.rs
: This module hosts aDecoration
impl that is responsible for drawing a string (the blame output) at the end of a given linehelix-vcs/src/git/blame.rs
: Holds theFileBlame
struct that contains the logic on how blame is retrieved for a file and a specific line in the document, as well as how this blame is formatted into a string.Run this PR using
--release
mode otherwise you may run into a panic caused by a failingdebug_assert!
ingix_blame
. This was tested to not have any user impact. The issue for this is known: GitoxideLabs/gitoxide#1847