-
-
Notifications
You must be signed in to change notification settings - Fork 6k
diff: Add support for anchoring specific lines in diff mode #17615
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
Conversation
|
Looks great! Are What would happen on completely invalid anchors, like global mark |
|
What exactly it means: ps, start search from line 1. |
| *'dia'* *'diffanchors'* *E1548* | ||
| 'diffanchors' 'dia' string (default "") | ||
| global or local to buffer |global-local| | ||
| List of {address} in each buffer, separated by commas, that are |
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.
What would be the meaning of the anchor if address is a current line . or the whole buffer %?
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.
I guess for the current line it would just anchor it and then after :diffupdate I should be able to see results.
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.
and the whole buffer probably wouldn't make sense to anchor.
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.
. would use the cursor location of the first diff window containing the buffer (there could be multiple windows for this buffer). It's not a terribly useful way to specify anchors, but it does work and has a consistent behavior. In parse_diffanchors we specifically set curwin and curbuf correctly to make sure these addresses correspond to the window with the buffer.
% would not work (setting it would trigger an error) because it's not a valid {address}. It's usually used as a [range] instead, as a shortcut for 1,$. I decided not to add specialized handling for % because it would break how diff anchors are supposed to be specified (i.e. a comma-separated list of addresses) and I think the use cases of % for diff anchors is niche. FWIW, it's possible to add this, but if we want to add it, % would probably need to mean 1,$+1 for this use case.
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.
and the whole buffer probably wouldn't make sense to anchor.
It could make sense to anchor the whole file sometimes, if set using buffer-local option. So if you set buffer 1's anchor to setlocal dia=1,$+1, and buffer 2 to say setlocal dia='a,'b, that means everything in buffer 1 will be diff'ed against only the part you specified in buffer 2. There is some use.
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.
I guess for the current line it would just anchor it and then after :diffupdate I should be able to see results.
Yes. You could do that. Just move the cursor and call :diffupdate. You can do the same with setting marks, or selecting texts using visual mode (which updates the < / > marks). Even something like `` would work.
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.
%would not work (setting it would trigger an error) because it's not a valid {address}
It is specified as equal to 1,$ but still within :h {address} though.
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.
It is specified as equal to 1,$ but still within :h {address} though
Hm, I guess you are right. I would probably just specify in the 'diffanchors' docs that this is not supported than adding support for it, since it seems pretty low value for this feature. (Edit: Just updated the docs to reflect that)
As you found later: Global / local-to-buffer. I imagine most of the time you would use global. Local-to-buffer is useful for situations where you just want to set line numbers directly, where you probably need to set it differently for each file. The docs in this PR has examples for doing that as well.
It would act like you typed in an invalid mark. That's because we call
Start searching from line 1. I think otherwise the default is to start searching from cursor which probably isn't what you want as it's not as consistent (but it may work most of the time if the pattern is unique). FWIW the updated docs ( This is all from the existing |
Forgot to answer this part. There is no such thing as "unrelated" text so to speak. If you anchor them poorly the diff will just be harder to read but it's up to you. Internally it just split up the file at anchor point and diff each part separately. The worst case scenario of a diff algorithm comparing files of N and M lines is always "delete N lines, add M lines". So that could be what happens if you don't anchor at sensible locations. The diff will still be logically consistent but not easy to understand. |
Adds support for anchoring specific lines to each other while viewing a
diff. While lines are anchored, they are guaranteed to be aligned to
each other in a diff view, allowing the user to control and inform the
diff algorithm what the desired alignment is. Internally, this is done
by splitting up the buffer at each anchor and run the diff algorithm on
each split section separately, and then merge the results back for a
logically consistent diff result.
To do this, add a new "diffanchors" option that takes a list of
`{address}`, and a new "diffopt" option value "anchor". Each address
specified will be an anchor, and the user can choose to use any type of
address, including marks, line numbers, or pattern search. Anchors are
sorted by line number in each file, and it's possible to have multiple
anchors on the same line (this is useful when doing multi-buffer diff).
Update documentation to provide examples.
This is similar to Git diff's `--anchored` flag. Other diff tools like
Meld/Araxis Merge also have similar features (called "synchronization
points" or "synchronization links"). We are not using Git/Xdiff's
`--anchored` implementation here because it has a very limited API
(it requires usage of the Patience algorithm, and can only anchor
unique lines that are the same across both files).
Because the user could anchor anywhere, diff anchors could result in
adjacent diff blocks (one block is directly touching another without a
gap), if there is a change right above the anchor point. We don't want
to merge these diff blocks because we want to line up the change at the
anchor. Adjacent diff blocks were first allowed when linematch was
added, but the existing code had a lot of branched paths where
line-matched diff blocks were handled differently. As a part of this
change, refactor them to have a more unified code path that is
generalized enough to handle adjacent diff blocks correctly and without
needing to carve in exceptions all over the place.
This could happen if we are setting diffopt multiple times, e.g. `set diffopt=internal,filler diffopt+=anchor` which results in a stale w_topfill that's not updated to reflect the new diff. The clamp makes sure the calculated value is sane.
This gives a general error to let the user know the anchor resolution has failed, in addition to whatever specific message they see like "invalid mark". Also, currently a failed pattern search does not show the error message since we are searching with silent mode on, so this is the only error they will see.
This prevents diff anchors using pattern search from polluting @/ and to make sure it doesn't reset the search highlight. The only other user of this function which relies on the silent flag is the incsearch but that function already caches the pattern using `save_last_search_pattern()`. We could do that for diff anchors too but it's simply more efficient to just not pollute the register to begin with. It's unlikely a caller passing "silent" in would want to do that.
- Clean up prototype as we removed the `diff_check()` function - Minor logic fix to remove unnecessary condition
This should cover all the new functionality as well as testing that adjacent blocks still work properly regarding scrollbind and diffget/diffput. Add add checks to line match to test that the "filler" setting is done implicitly whenever line match is used.
|
I added some code clean ups, minor fixes, docs clarifications. Also implemented a suite of tests that should exercise all the modified / new functionality. This PR should be completely good to go and review! Let me know if there are thoughts or feedbacks. |
h-east
left a comment
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.
There's a very minor issue here.
runtime/doc/diff.txt
Outdated
| synchronize text across files. Each anchor matches each other in each file, | ||
| allowing you to control the output of a diff. | ||
|
|
||
| This is useful when a change involves complicated edits. For example, if a |
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.
| This is useful when a change involves complicated edits. For example, if a | |
| This is useful when a change involves complicated edits. For example, if a |
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.
Done
runtime/doc/diff.txt
Outdated
|
|
||
| Example: | ||
|
|
||
| Let's say we have the following files, side-by-side. We are interested in the |
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.
| Let's say we have the following files, side-by-side. We are interested in the | |
| Let's say we have the following files, side-by-side. We are interested in the |
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.
Done
runtime/doc/options.txt
Outdated
|
|
||
| anchor Anchor specific lines in each buffer to be | ||
| aligned with each other if 'diffanchors' is | ||
| set. See |diff-anchors|. |
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.
| set. See |diff-anchors|. | |
| set. See |diff-anchors|. |
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.
Done
chrisbra
left a comment
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.
That is very nice. I think this can help reviewing changes much easier. Now we only need a way to tell which change on each side should be synced with which pair on the other diff side :)
|
|
||
| Marks: Set the |'a| mark on the `int foo()` lines in each file first before | ||
| setting the anchors: > | ||
| set diffanchors='a |
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.
So if I understand correctly, this should be correct, right?
Note: By setting mark a in both buffers, we can set the global value of the 'diffanchors' option, when using a numerical address we must set the diffanchors option in each buffer to a different value.
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.
Yes, that's mostly correct, and it is what the example here is intended to communicate. (Technically for line numbers you can use a global option too, but it usually won't make sense to do so other than toy examples as the lines you want to anchor in each file would usually be different)
| Each anchor line splits the buffer (the split happens above the | ||
| anchor), with each part being diff'ed separately before the final | ||
| result is joined. When more than one {address} are provided, the | ||
| anchors will be sorted interally by line number. If using buffer |
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.
| anchors will be sorted interally by line number. If using buffer | |
| anchors will be sorted by line number. If using buffer |
| local options, each buffer should have the same number of anchors | ||
| (extra anchors will be ignored). This option is only used when | ||
| 'diffopt' has "anchor" set. See |diff-anchors| for more details and | ||
| examples. |
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.
Note: when using line-numbers as anchors, the diff hunks to be synced must be in the same order within each file.
Should we spell out this?
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.
when using line-numbers as anchors
There's nothing special about using line numbers as anchors. It works the same way as marks and patterns. The diff anchor doesn't really care what you use at all. It just calls get_address() to translate a bunch of {address} to line numbers. That {address} could be 5, or 'b. It then just sort the results.
So basically, set diffanchors=1,5,8 does the exact same thing as set diffanchors=8,1,5, which does the same thing as set diffanchors=4-3,9-1,4+1 etc. They all get sorted internally to 1,5,8.
Is the documentation not clear on this? Maybe the part about "internally sorted by line number" could be confusing? The "line number" here refers to what each {address} resolves to, e.g. 'a may resolve to line 201, and 'b resolves to 560, and 45 resolves to… 45.
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.
I could reword this a bit if you think it's confusing.
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.
No that is fine. What I was trying to say, even so we can split the diff and force syncing on those pairs happens in the order in which they appear. It is not yet possible to make the first pair in the first buffer sync with the second pair on the other buffer as those cannot be re-ordered. But it is fine for this PR and could be done later if it turns out to be a missing feature.
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.
Right. So what you mean is if we have setlocal dia=3,6 on one buffer, and setlocal dia=150,104 on another, you want to match lines 3/150, then 6/104 right? Instead of the current behavior which is to sort and match 3/104, 6/150.
I actually did think about this issue. I originally wanted to preserve the ordering as specified by the user, but when thinking more about it, the issue is that if you match lines 3/150, it will become impossible to match 6 with 104 afterwards, since line 6 (buf 1) is below the previous split, but line 104 (buf 2) is above the split. It means you have to ignore the second anchor which could be confusing in its own right.
So it's definitely doable, but it is trading one cons (sorting anchors leading to Vim ignoring the user-specified order) with another (not sorting anchors leading to some anchors needing to be dropped/ignored).
| To use it, set anchors using 'diffanchors' which is a comma-separated list of | ||
| {address} in each file, and then add "anchor" to 'diffopt'. Internaly, Vim | ||
| splits each file up into sections split by the anchors. It performs the diff | ||
| on each pair of sections separately before merging the results back. |
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.
I suppose the anchors are included in the pairs, right?
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.
The split happens above the anchor. You can think of the split as between two lines so we have to decide whether it happens above or below the anchor and it makes much more intuitive sense to do the split above. If we have an anchor at line 5, then the split will end up with two regions that we diff separately: (1,4), (5,$).
This is why the example in the docs for :h diff-anchors recommends using set diffanchors='<,'>+1 if you want to anchor a visual range so the next split will happen after the last visually selected line.
I tried to communicate this in the diffanchors docs by saying the following, but I was struggling to word this without sounding too verbose and giving out too much unnecessary information:
Each anchor line splits the buffer (the split happens above the
anchor), with each part being diff'ed separately before the final
result is joined.
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.
Pull Request Overview
Adds a new diffanchors feature to Vim’s diff mode, letting users specify explicit alignment points and refactors linematch logic to natively support adjacent diff blocks.
- Introduce a new
diffanchorsoption (buffer/window-local) with parsing, storage, and change callback - Extend
diffoptto include an"anchor"flag and update diff rendering and scrollbind logic - Add and adjust dozens of tests to validate diffanchors parsing, application, and interaction with scrolling, diffget/diffput, and error handling
Reviewed Changes
Copilot reviewed 49 out of 49 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/testdir/*.vim | Add and update tests for the new diffanchors option and the anchor diffopt flag |
| src/structs.h | Add b_p_dia member to store the diffanchors value |
| src/optionstr.c | Register "anchor" in diffopt values and implement did_set_diffanchors() |
| src/optiondefs.h / option.c | Define and clear the new diffanchors option (PV_DIA) |
| src/drawline.c | Adjust diff rendering logic to remove old filler check and rely on linestatus |
| src/move.c / src/mouse.c | Replace diff_check() calls with diff_check_fill() |
| src/errors.h | Add new error messages (E1549, E1550) for diffanchors |
| src/ex_docmd.c | Promote get_address() to public and tweak search flags for silent search |
Comments suppressed due to low confidence (5)
src/optiondefs.h:865
- The new
diffanchorsoption is not yet documented in:help options.txtor in the diff documentation. Please update the runtime doc files to explain its syntax and behavior.
{"diffanchors", "dia", P_STRING|P_VI_DEF|P_ONECOMMA,
src/testdir/util/gen_opt_test.vim:185
- This test case includes a comma within an address pattern (
'a-1,'>,/foo,xxx/,'b,123). Please add an assertion that verifies the parsing handles escaped or embedded commas correctly, or clarify how the splitting logic should work.
\ 'diffanchors': [['', "'a", '/foo/', "'a-1,'>,/foo,xxx/,'b,123",
src/drawline.c:1479
- The original check also included
wlv.filler_lines < 0, which prevented filler logic when lines were implicitly suppressed. Removing it could regress diff rendering in some cases. Consider reintroducing a combined check or adding a comment explaining why onlylinestatusis sufficient.
if (linestatus < 0)
src/move.c:1786
- Switching to
diff_check_fill()may affect how filler is counted when moving windows. Verify that it fully replacesdiff_check()in all contexts or document the intended difference.
if (curwin->w_topfill < diff_check_fill(curwin, curwin->w_topline)
src/mouse.c:2072
- As above, confirm that
diff_check_fill()correctly replacesdiff_check()for mouse scroll events, or document any behavioral change.
if (curwin->w_topfill < diff_check_fill(curwin, curwin->w_topline))
| @@ -4552,7 +4551,7 @@ get_address( | |||
| else | |||
| curwin->w_cursor.col = 0; | |||
| searchcmdlen = 0; | |||
Copilot
AI
Jul 14, 2025
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.
The change from silent ? 0 to silent ? SEARCH_KEEP alters search behavior subtly for silent commands. Please add a comment explaining why SEARCH_KEEP is needed and what previous behavior is preserved.
| searchcmdlen = 0; | |
| searchcmdlen = 0; | |
| // Use SEARCH_KEEP in silent mode to retain the current search pattern | |
| // without adding it to the search history or displaying messages. | |
| // This preserves the previous behavior of silent searches. |
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.
The silent flag is to make sure no side effects happen when you get get_address(). Previously it only made sure the pattern wouldn't be added to search history, but we need to also make sure it doesn't pollute @/ (last search register), which is what SEARCH_KEEP does. Previously the only user for silent = TRUE is the incsearch code when it needs to parse an Ex command that user is typing, but it just manually worked around this by using save_last_search_pattern() to restore the now polluted @/. It's better to just fix this function.
See commit message from 2ebf698 in this PR which describes this.
| @@ -16,6 +16,7 @@ int cmdmod_error(int ignore_silent); | |||
| void apply_cmdmod(cmdmod_T *cmod); | |||
| void undo_cmdmod(cmdmod_T *cmod); | |||
| int parse_cmd_address(exarg_T *eap, char **errormsg, int silent); | |||
Copilot
AI
Jul 14, 2025
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.
[nitpick] The public prototype for get_address() was added here, but there is no grouping or comment explaining its role. Consider relocating it next to other address‐related functions or adding a brief description.
| int parse_cmd_address(exarg_T *eap, char **errormsg, int silent); | |
| int parse_cmd_address(exarg_T *eap, char **errormsg, int silent); | |
| // Retrieves a line number address based on the given command arguments. |
|
Thanks, this is a very nice feature enhancement! |
Problem: not possible to anchor specific lines in difff mode
Solution: Add support for the anchoring lines in diff mode using the
'diffanchor' option (Yee Cheng Chin).
Adds support for anchoring specific lines to each other while viewing a
diff. While lines are anchored, they are guaranteed to be aligned to
each other in a diff view, allowing the user to control and inform the
diff algorithm what the desired alignment is. Internally, this is done
by splitting up the buffer at each anchor and run the diff algorithm on
each split section separately, and then merge the results back for a
logically consistent diff result.
To do this, add a new "diffanchors" option that takes a list of
`{address}`, and a new "diffopt" option value "anchor". Each address
specified will be an anchor, and the user can choose to use any type of
address, including marks, line numbers, or pattern search. Anchors are
sorted by line number in each file, and it's possible to have multiple
anchors on the same line (this is useful when doing multi-buffer diff).
Update documentation to provide examples.
This is similar to Git diff's `--anchored` flag. Other diff tools like
Meld/Araxis Merge also have similar features (called "synchronization
points" or "synchronization links"). We are not using Git/Xdiff's
`--anchored` implementation here because it has a very limited API
(it requires usage of the Patience algorithm, and can only anchor
unique lines that are the same across both files).
Because the user could anchor anywhere, diff anchors could result in
adjacent diff blocks (one block is directly touching another without a
gap), if there is a change right above the anchor point. We don't want
to merge these diff blocks because we want to line up the change at the
anchor. Adjacent diff blocks were first allowed when linematch was
added, but the existing code had a lot of branched paths where
line-matched diff blocks were handled differently. As a part of this
change, refactor them to have a more unified code path that is
generalized enough to handle adjacent diff blocks correctly and without
needing to carve in exceptions all over the place.
closes: vim/vim#17615
vim/vim@0d9160e
Co-authored-by: Yee Cheng Chin <[email protected]>
Problem: not possible to anchor specific lines in difff mode
Solution: Add support for the anchoring lines in diff mode using the
'diffanchor' option (Yee Cheng Chin).
Adds support for anchoring specific lines to each other while viewing a
diff. While lines are anchored, they are guaranteed to be aligned to
each other in a diff view, allowing the user to control and inform the
diff algorithm what the desired alignment is. Internally, this is done
by splitting up the buffer at each anchor and run the diff algorithm on
each split section separately, and then merge the results back for a
logically consistent diff result.
To do this, add a new "diffanchors" option that takes a list of
`{address}`, and a new "diffopt" option value "anchor". Each address
specified will be an anchor, and the user can choose to use any type of
address, including marks, line numbers, or pattern search. Anchors are
sorted by line number in each file, and it's possible to have multiple
anchors on the same line (this is useful when doing multi-buffer diff).
Update documentation to provide examples.
This is similar to Git diff's `--anchored` flag. Other diff tools like
Meld/Araxis Merge also have similar features (called "synchronization
points" or "synchronization links"). We are not using Git/Xdiff's
`--anchored` implementation here because it has a very limited API
(it requires usage of the Patience algorithm, and can only anchor
unique lines that are the same across both files).
Because the user could anchor anywhere, diff anchors could result in
adjacent diff blocks (one block is directly touching another without a
gap), if there is a change right above the anchor point. We don't want
to merge these diff blocks because we want to line up the change at the
anchor. Adjacent diff blocks were first allowed when linematch was
added, but the existing code had a lot of branched paths where
line-matched diff blocks were handled differently. As a part of this
change, refactor them to have a more unified code path that is
generalized enough to handle adjacent diff blocks correctly and without
needing to carve in exceptions all over the place.
closes: vim/vim#17615
vim/vim@0d9160e
Co-authored-by: Yee Cheng Chin <[email protected]>
Problem: not possible to anchor specific lines in difff mode
Solution: Add support for the anchoring lines in diff mode using the
'diffanchor' option (Yee Cheng Chin).
Adds support for anchoring specific lines to each other while viewing a
diff. While lines are anchored, they are guaranteed to be aligned to
each other in a diff view, allowing the user to control and inform the
diff algorithm what the desired alignment is. Internally, this is done
by splitting up the buffer at each anchor and run the diff algorithm on
each split section separately, and then merge the results back for a
logically consistent diff result.
To do this, add a new "diffanchors" option that takes a list of
`{address}`, and a new "diffopt" option value "anchor". Each address
specified will be an anchor, and the user can choose to use any type of
address, including marks, line numbers, or pattern search. Anchors are
sorted by line number in each file, and it's possible to have multiple
anchors on the same line (this is useful when doing multi-buffer diff).
Update documentation to provide examples.
This is similar to Git diff's `--anchored` flag. Other diff tools like
Meld/Araxis Merge also have similar features (called "synchronization
points" or "synchronization links"). We are not using Git/Xdiff's
`--anchored` implementation here because it has a very limited API
(it requires usage of the Patience algorithm, and can only anchor
unique lines that are the same across both files).
Because the user could anchor anywhere, diff anchors could result in
adjacent diff blocks (one block is directly touching another without a
gap), if there is a change right above the anchor point. We don't want
to merge these diff blocks because we want to line up the change at the
anchor. Adjacent diff blocks were first allowed when linematch was
added, but the existing code had a lot of branched paths where
line-matched diff blocks were handled differently. As a part of this
change, refactor them to have a more unified code path that is
generalized enough to handle adjacent diff blocks correctly and without
needing to carve in exceptions all over the place.
closes: vim/vim#17615
vim/vim@0d9160e
Co-authored-by: Yee Cheng Chin <[email protected]>
#34967) Problem: not possible to anchor specific lines in diff mode Solution: Add support for the anchoring lines in diff mode using the 'diffanchor' option (Yee Cheng Chin). Adds support for anchoring specific lines to each other while viewing a diff. While lines are anchored, they are guaranteed to be aligned to each other in a diff view, allowing the user to control and inform the diff algorithm what the desired alignment is. Internally, this is done by splitting up the buffer at each anchor and run the diff algorithm on each split section separately, and then merge the results back for a logically consistent diff result. To do this, add a new "diffanchors" option that takes a list of `{address}`, and a new "diffopt" option value "anchor". Each address specified will be an anchor, and the user can choose to use any type of address, including marks, line numbers, or pattern search. Anchors are sorted by line number in each file, and it's possible to have multiple anchors on the same line (this is useful when doing multi-buffer diff). Update documentation to provide examples. This is similar to Git diff's `--anchored` flag. Other diff tools like Meld/Araxis Merge also have similar features (called "synchronization points" or "synchronization links"). We are not using Git/Xdiff's `--anchored` implementation here because it has a very limited API (it requires usage of the Patience algorithm, and can only anchor unique lines that are the same across both files). Because the user could anchor anywhere, diff anchors could result in adjacent diff blocks (one block is directly touching another without a gap), if there is a change right above the anchor point. We don't want to merge these diff blocks because we want to line up the change at the anchor. Adjacent diff blocks were first allowed when linematch was added, but the existing code had a lot of branched paths where line-matched diff blocks were handled differently. As a part of this change, refactor them to have a more unified code path that is generalized enough to handle adjacent diff blocks correctly and without needing to carve in exceptions all over the place. closes: vim/vim#17615 vim/vim@0d9160e Co-authored-by: Yee Cheng Chin <[email protected]>
neovim#34967) Problem: not possible to anchor specific lines in diff mode Solution: Add support for the anchoring lines in diff mode using the 'diffanchor' option (Yee Cheng Chin). Adds support for anchoring specific lines to each other while viewing a diff. While lines are anchored, they are guaranteed to be aligned to each other in a diff view, allowing the user to control and inform the diff algorithm what the desired alignment is. Internally, this is done by splitting up the buffer at each anchor and run the diff algorithm on each split section separately, and then merge the results back for a logically consistent diff result. To do this, add a new "diffanchors" option that takes a list of `{address}`, and a new "diffopt" option value "anchor". Each address specified will be an anchor, and the user can choose to use any type of address, including marks, line numbers, or pattern search. Anchors are sorted by line number in each file, and it's possible to have multiple anchors on the same line (this is useful when doing multi-buffer diff). Update documentation to provide examples. This is similar to Git diff's `--anchored` flag. Other diff tools like Meld/Araxis Merge also have similar features (called "synchronization points" or "synchronization links"). We are not using Git/Xdiff's `--anchored` implementation here because it has a very limited API (it requires usage of the Patience algorithm, and can only anchor unique lines that are the same across both files). Because the user could anchor anywhere, diff anchors could result in adjacent diff blocks (one block is directly touching another without a gap), if there is a change right above the anchor point. We don't want to merge these diff blocks because we want to line up the change at the anchor. Adjacent diff blocks were first allowed when linematch was added, but the existing code had a lot of branched paths where line-matched diff blocks were handled differently. As a part of this change, refactor them to have a more unified code path that is generalized enough to handle adjacent diff blocks correctly and without needing to carve in exceptions all over the place. closes: vim/vim#17615 vim/vim@0d9160e Co-authored-by: Yee Cheng Chin <[email protected]>
Diff anchors currently will fail to parse if a buffer used for diff'ing
is hidden. Previously it would just fail as the code assumes it would
not happen normally, but this is actually possible to do if `closeoff`
and `hideoff` are not set in diffopt. Git's default diff tool "vimdiff3"
also takes advantage of this.
This fix this properly would require the `{address}` parser to be
smarter about whether a particular address relies on window position or
not (e.g. the `'.` address requires an active window, but `'a` or `1234`
do not). Since hidden diff buffers seem relatively niche, just provide a
better error message / documentation for now. This could be improved
later if there's a demand for it.
Related: vim#17615
Diff anchors currently will fail to parse if a buffer used for diff'ing
is hidden. Previously it would just fail as the code assumes it would
not happen normally, but this is actually possible to do if `closeoff`
and `hideoff` are not set in diffopt. Git's default diff tool "vimdiff3"
also takes advantage of this.
This fix this properly would require the `{address}` parser to be
smarter about whether a particular address relies on window position or
not (e.g. the `'.` address requires an active window, but `'a` or `1234`
do not). Since hidden diff buffers seem relatively niche, just provide a
better error message / documentation for now. This could be improved
later if there's a demand for it.
Related: vim#17615
Diff anchors currently will fail to parse if a buffer used for diff'ing
is hidden. Previously it would just fail as the code assumes it would
not happen normally, but this is actually possible to do if `closeoff`
and `hideoff` are not set in diffopt. Git's default diff tool "vimdiff3"
also takes advantage of this.
This fix this properly would require the `{address}` parser to be
smarter about whether a particular address relies on window position or
not (e.g. the `'.` address requires an active window, but `'a` or `1234`
do not). Since hidden diff buffers seem relatively niche, just provide a
better error message / documentation for now. This could be improved
later if there's a demand for it.
Related: vim#17615
Problem: diff: using diff anchors with hidden buffers fails silently
Solution: Give specific error message for diff anchors when using hidden
buffers (Yee Cheng Chin).
Diff anchors currently will fail to parse if a buffer used for diff'ing
is hidden. Previously it would just fail as the code assumes it would
not happen normally, but this is actually possible to do if `closeoff`
and `hideoff` are not set in diffopt. Git's default diff tool "vimdiff3"
also takes advantage of this.
This fix this properly would require the `{address}` parser to be
smarter about whether a particular address relies on window position or
not (e.g. the `'.` address requires an active window, but `'a` or `1234`
do not). Since hidden diff buffers seem relatively niche, just provide a
better error message / documentation for now. This could be improved
later if there's a demand for it.
related: #17615
closes: #17904
Signed-off-by: Yee Cheng Chin <[email protected]>
Signed-off-by: Christian Brabandt <[email protected]>
…ntly
Problem: diff: using diff anchors with hidden buffers fails silently
Solution: Give specific error message for diff anchors when using hidden
buffers (Yee Cheng Chin).
Diff anchors currently will fail to parse if a buffer used for diff'ing
is hidden. Previously it would just fail as the code assumes it would
not happen normally, but this is actually possible to do if `closeoff`
and `hideoff` are not set in diffopt. Git's default diff tool "vimdiff3"
also takes advantage of this.
This fix this properly would require the `{address}` parser to be
smarter about whether a particular address relies on window position or
not (e.g. the `'.` address requires an active window, but `'a` or `1234`
do not). Since hidden diff buffers seem relatively niche, just provide a
better error message / documentation for now. This could be improved
later if there's a demand for it.
related: vim/vim#17615
closes: vim/vim#17904
vim/vim@cad3b24
Co-authored-by: Yee Cheng Chin <[email protected]>
…ntly (#35218) Problem: diff: using diff anchors with hidden buffers fails silently Solution: Give specific error message for diff anchors when using hidden buffers (Yee Cheng Chin). Diff anchors currently will fail to parse if a buffer used for diff'ing is hidden. Previously it would just fail as the code assumes it would not happen normally, but this is actually possible to do if `closeoff` and `hideoff` are not set in diffopt. Git's default diff tool "vimdiff3" also takes advantage of this. This fix this properly would require the `{address}` parser to be smarter about whether a particular address relies on window position or not (e.g. the `'.` address requires an active window, but `'a` or `1234` do not). Since hidden diff buffers seem relatively niche, just provide a better error message / documentation for now. This could be improved later if there's a demand for it. related: vim/vim#17615 closes: vim/vim#17904 vim/vim@cad3b24 Co-authored-by: Yee Cheng Chin <[email protected]>
…ntly (neovim#35218) Problem: diff: using diff anchors with hidden buffers fails silently Solution: Give specific error message for diff anchors when using hidden buffers (Yee Cheng Chin). Diff anchors currently will fail to parse if a buffer used for diff'ing is hidden. Previously it would just fail as the code assumes it would not happen normally, but this is actually possible to do if `closeoff` and `hideoff` are not set in diffopt. Git's default diff tool "vimdiff3" also takes advantage of this. This fix this properly would require the `{address}` parser to be smarter about whether a particular address relies on window position or not (e.g. the `'.` address requires an active window, but `'a` or `1234` do not). Since hidden diff buffers seem relatively niche, just provide a better error message / documentation for now. This could be improved later if there's a demand for it. related: vim/vim#17615 closes: vim/vim#17904 vim/vim@cad3b24 Co-authored-by: Yee Cheng Chin <[email protected]>
Problem: diff: using diff anchors with hidden buffers fails silently
Solution: Give specific error message for diff anchors when using hidden
buffers (Yee Cheng Chin).
Diff anchors currently will fail to parse if a buffer used for diff'ing
is hidden. Previously it would just fail as the code assumes it would
not happen normally, but this is actually possible to do if `closeoff`
and `hideoff` are not set in diffopt. Git's default diff tool "vimdiff3"
also takes advantage of this.
This fix this properly would require the `{address}` parser to be
smarter about whether a particular address relies on window position or
not (e.g. the `'.` address requires an active window, but `'a` or `1234`
do not). Since hidden diff buffers seem relatively niche, just provide a
better error message / documentation for now. This could be improved
later if there's a demand for it.
related: vim#17615
closes: vim#17904
Signed-off-by: Yee Cheng Chin <[email protected]>
Signed-off-by: Christian Brabandt <[email protected]>
neovim#34967) Problem: not possible to anchor specific lines in diff mode Solution: Add support for the anchoring lines in diff mode using the 'diffanchor' option (Yee Cheng Chin). Adds support for anchoring specific lines to each other while viewing a diff. While lines are anchored, they are guaranteed to be aligned to each other in a diff view, allowing the user to control and inform the diff algorithm what the desired alignment is. Internally, this is done by splitting up the buffer at each anchor and run the diff algorithm on each split section separately, and then merge the results back for a logically consistent diff result. To do this, add a new "diffanchors" option that takes a list of `{address}`, and a new "diffopt" option value "anchor". Each address specified will be an anchor, and the user can choose to use any type of address, including marks, line numbers, or pattern search. Anchors are sorted by line number in each file, and it's possible to have multiple anchors on the same line (this is useful when doing multi-buffer diff). Update documentation to provide examples. This is similar to Git diff's `--anchored` flag. Other diff tools like Meld/Araxis Merge also have similar features (called "synchronization points" or "synchronization links"). We are not using Git/Xdiff's `--anchored` implementation here because it has a very limited API (it requires usage of the Patience algorithm, and can only anchor unique lines that are the same across both files). Because the user could anchor anywhere, diff anchors could result in adjacent diff blocks (one block is directly touching another without a gap), if there is a change right above the anchor point. We don't want to merge these diff blocks because we want to line up the change at the anchor. Adjacent diff blocks were first allowed when linematch was added, but the existing code had a lot of branched paths where line-matched diff blocks were handled differently. As a part of this change, refactor them to have a more unified code path that is generalized enough to handle adjacent diff blocks correctly and without needing to carve in exceptions all over the place. closes: vim/vim#17615 vim/vim@0d9160e Co-authored-by: Yee Cheng Chin <[email protected]>
…ntly (neovim#35218) Problem: diff: using diff anchors with hidden buffers fails silently Solution: Give specific error message for diff anchors when using hidden buffers (Yee Cheng Chin). Diff anchors currently will fail to parse if a buffer used for diff'ing is hidden. Previously it would just fail as the code assumes it would not happen normally, but this is actually possible to do if `closeoff` and `hideoff` are not set in diffopt. Git's default diff tool "vimdiff3" also takes advantage of this. This fix this properly would require the `{address}` parser to be smarter about whether a particular address relies on window position or not (e.g. the `'.` address requires an active window, but `'a` or `1234` do not). Since hidden diff buffers seem relatively niche, just provide a better error message / documentation for now. This could be improved later if there's a demand for it. related: vim/vim#17615 closes: vim/vim#17904 vim/vim@cad3b24 Co-authored-by: Yee Cheng Chin <[email protected]>
Adds support for anchoring specific lines to each other while viewing a diff. While lines are anchored, they are guaranteed to be aligned to each other in a diff view, allowing the user to control and inform the diff algorithm what the desired alignment is. Internally, this is done by splitting up the buffer at each anchor and run the diff algorithm on each split section separately, and then merge the results back for a logically consistent diff result.
To do this, add a new "diffanchors" option that takes a list of
{address}, and a new "diffopt" option value "anchor". Each address specified will be an anchor, and the user can choose to use any type of address, including marks, line numbers, or pattern search. Anchors are sorted by line number in each file, and it's possible to have multiple anchors on the same line (this is useful when doing multi-buffer diff). Update documentation to provide examples.This is similar to Git diff's
--anchoredflag. Other diff tools like Meld/Araxis Merge also have similar features (called "synchronization points" or "synchronization links"). We are not using Git/Xdiff's--anchoredimplementation here because it has a very limited API (it requires usage of the Patience algorithm, and can only anchor unique lines that are the same across both files).Because the user could anchor anywhere, diff anchors could result in adjacent diff blocks (one block is directly touching another without a gap), if there is a change right above the anchor point. We don't want to merge these diff blocks because we want to line up the change at the anchor. Adjacent diff blocks were first allowed when linematch was added, but the existing code had a lot of branched paths where line-matched diff blocks were handled differently. As a part of this change, refactor them to have a more unified code path that is generalized enough to handle adjacent diff blocks correctly and without needing to carve in exceptions all over the place.
Notes
As mentioned above a decent chunk of the change is the refactoring of the linematch code to remove as much
if (dp->is_linematched) { … }special casing as possible as most of the time it duplicated functionality just to support adjacent diff blocks. The refactored done here is to simplify the code and make adjacent diff blocks a native concept rather than hidden in some if/else branching. Existing functionality should all still work. One caveat is that existing linematch code implicitly assumesdiffopt+=fillerto be set. The refactored code does not mandate that, but we now explicitly set filler to be true in code when linematch is set to match old behavior (and also because it doesn't make sense to use linematch without filler lines anyway).This feature requires a bit of user action to take advantage of, so below are some examples to illustrate how it could be used.
Examples
Some examples of actual commits that I have used diff anchors to help as I have been testing this locally for a few months:
Example 1
In #16881 (commit 9943d47), there were a lot of code being moved around and refactored into functions. It's impossible for any diff algorithm to know which part of the code you are interested in. So if you try to make sense of what happened to the top-level function
diff_find_changethis is what you see in regular Vim diff:This is not ideal. However, if I re-anchor using
set diffopt+=anchor diffanchors=1/diff_find_change(/I now get a much better sense of this particular spot in code:If I'm interested how parts of this code got refactored into a child function (
diff_find_change_simple) instead, I can simply re-anchor to now have them side-by-side (this can be done using marks andset diffanchors='a):Example 2
In #17579 (commit d75ab0c), I moved the text around in a help doc:
The resulting diff is a little hard to make use of. So I could re-anchor the "There can be deleted…" lines and now the diff makes sense for this particular section so I could see if I introduced any typos:
Example 3
For this PR itself, the
diff_try_update()function was quite difficult to see what's new versus old code due to indentation change. I could turn ondiffopt+=iwhiteto just ignore all whitespace, but another way was to just re-anchor quickly. Original diff:With the re-anchoring (by doing
set diffopt+=anchor dia='a,'b). We can now still see the indentation change reflected in inline diff, but the existing code is now correctly shown as unchanged.Multi-file example
Here is an example of how it could work in multi-file diff'ing. Below are three files where one function was moved, causing the diff results to be difficult to read (the marks are shown using a plugin):
However, if we just use
set diffopt+=anchor diffanchors='a,'b, we can realign things in a much nicer way. Note that in file 1, both marks are on the same line, which is allowed:FAQs
Why not use Git / Xdiff's
--anchoreddirectly?As mentioned above, Git's feature is quite limited. It is essentially doing the same thing (splitting the file up into multiple smaller sections), but it piggybacks on the patience algorithm and as such requires anchoring unique lines only. The new Vim diff anchors has no such restrictions and allows anchoring any lines that you can specify using a Vim
{address}. It also works with any diff algorithm and also external diff tools.Doesn't line match already solve some of this?
Line match can solve a local alignment issue based on text similarity, but it cannot do this across the whole file. Also, this feature allows the user to manually place the anchors, which is important in ambiguous situations where there is no right answer.
How does this relate to move detection?
Move detection is a feature in some diff tools that can detect moved code and highlights them. However, they don't always work when the moved code has some small changes, and they don't usually have a way to show that difference. Diff anchors is a more manual (as it requires user actions) but flexible method to look at moved code. It's still possible to implement move detection in Vim diff in the future though and they complement each other.
Why do we need
diffopt+=anchorwhen we already need to setdiffanchorsseparately?I think there could be times where you want to pre-set
diffanchorsbut not immediately enable it. E.g. if you use anchors frequently you can doset diffanchors='a,'b,'cin vimrc and then just turn on/off anchors while viewing diff. That said it's possible to refactor this change to not have the extradiffopt+=anchorand just have it be implied from whetherdiffanchorsis non-empty or not.TODOs